1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Form\View\Helper;
11
12use Zend\Form\ElementInterface;
13use Zend\I18n\View\Helper\AbstractTranslatorHelper as BaseAbstractHelper;
14use Zend\View\Helper\Doctype;
15use Zend\View\Helper\EscapeHtml;
16use Zend\View\Helper\EscapeHtmlAttr;
17
18/**
19 * Base functionality for all form view helpers
20 */
21abstract class AbstractHelper extends BaseAbstractHelper
22{
23    /**
24     * Standard boolean attributes, with expected values for enabling/disabling
25     *
26     * @var array
27     */
28    protected $booleanAttributes = array(
29        'autofocus'    => array('on' => 'autofocus', 'off' => ''),
30        'checked'      => array('on' => 'checked',   'off' => ''),
31        'disabled'     => array('on' => 'disabled',  'off' => ''),
32        'multiple'     => array('on' => 'multiple',  'off' => ''),
33        'readonly'     => array('on' => 'readonly',  'off' => ''),
34        'required'     => array('on' => 'required',  'off' => ''),
35        'selected'     => array('on' => 'selected',  'off' => ''),
36    );
37
38    /**
39     * Translatable attributes
40     *
41     * @var array
42     */
43    protected $translatableAttributes = array(
44        'placeholder' => true,
45        'title' => true,
46    );
47
48    /**
49     * @var Doctype
50     */
51    protected $doctypeHelper;
52
53    /**
54     * @var EscapeHtml
55     */
56    protected $escapeHtmlHelper;
57
58    /**
59     * @var EscapeHtmlAttr
60     */
61    protected $escapeHtmlAttrHelper;
62
63    /**
64     * Attributes globally valid for all tags
65     *
66     * @var array
67     */
68    protected $validGlobalAttributes = array(
69        'accesskey'          => true,
70        'class'              => true,
71        'contenteditable'    => true,
72        'contextmenu'        => true,
73        'dir'                => true,
74        'draggable'          => true,
75        'dropzone'           => true,
76        'hidden'             => true,
77        'id'                 => true,
78        'lang'               => true,
79        'onabort'            => true,
80        'onblur'             => true,
81        'oncanplay'          => true,
82        'oncanplaythrough'   => true,
83        'onchange'           => true,
84        'onclick'            => true,
85        'oncontextmenu'      => true,
86        'ondblclick'         => true,
87        'ondrag'             => true,
88        'ondragend'          => true,
89        'ondragenter'        => true,
90        'ondragleave'        => true,
91        'ondragover'         => true,
92        'ondragstart'        => true,
93        'ondrop'             => true,
94        'ondurationchange'   => true,
95        'onemptied'          => true,
96        'onended'            => true,
97        'onerror'            => true,
98        'onfocus'            => true,
99        'oninput'            => true,
100        'oninvalid'          => true,
101        'onkeydown'          => true,
102        'onkeypress'         => true,
103        'onkeyup'            => true,
104        'onload'             => true,
105        'onloadeddata'       => true,
106        'onloadedmetadata'   => true,
107        'onloadstart'        => true,
108        'onmousedown'        => true,
109        'onmousemove'        => true,
110        'onmouseout'         => true,
111        'onmouseover'        => true,
112        'onmouseup'          => true,
113        'onmousewheel'       => true,
114        'onpause'            => true,
115        'onplay'             => true,
116        'onplaying'          => true,
117        'onprogress'         => true,
118        'onratechange'       => true,
119        'onreadystatechange' => true,
120        'onreset'            => true,
121        'onscroll'           => true,
122        'onseeked'           => true,
123        'onseeking'          => true,
124        'onselect'           => true,
125        'onshow'             => true,
126        'onstalled'          => true,
127        'onsubmit'           => true,
128        'onsuspend'          => true,
129        'ontimeupdate'       => true,
130        'onvolumechange'     => true,
131        'onwaiting'          => true,
132        'role'               => true,
133        'aria-labelledby'    => true,
134        'aria-describedby'   => true,
135        'spellcheck'         => true,
136        'style'              => true,
137        'tabindex'           => true,
138        'title'              => true,
139        'xml:base'           => true,
140        'xml:lang'           => true,
141        'xml:space'          => true,
142    );
143
144    /**
145     * Attributes valid for the tag represented by this helper
146     *
147     * This should be overridden in extending classes
148     *
149     * @var array
150     */
151    protected $validTagAttributes = array(
152    );
153
154    /**
155     * Set value for doctype
156     *
157     * @param  string $doctype
158     * @return AbstractHelper
159     */
160    public function setDoctype($doctype)
161    {
162        $this->getDoctypeHelper()->setDoctype($doctype);
163        return $this;
164    }
165
166    /**
167     * Get value for doctype
168     *
169     * @return string
170     */
171    public function getDoctype()
172    {
173        return $this->getDoctypeHelper()->getDoctype();
174    }
175
176    /**
177     * Set value for character encoding
178     *
179     * @param  string $encoding
180     * @return AbstractHelper
181     */
182    public function setEncoding($encoding)
183    {
184        $this->getEscapeHtmlHelper()->setEncoding($encoding);
185        $this->getEscapeHtmlAttrHelper()->setEncoding($encoding);
186        return $this;
187    }
188
189    /**
190     * Get character encoding
191     *
192     * @return string
193     */
194    public function getEncoding()
195    {
196        return $this->getEscapeHtmlHelper()->getEncoding();
197    }
198
199    /**
200     * Create a string of all attribute/value pairs
201     *
202     * Escapes all attribute values
203     *
204     * @param  array $attributes
205     * @return string
206     */
207    public function createAttributesString(array $attributes)
208    {
209        $attributes = $this->prepareAttributes($attributes);
210        $escape     = $this->getEscapeHtmlHelper();
211        $escapeAttr = $this->getEscapeHtmlAttrHelper();
212        $strings    = array();
213
214        foreach ($attributes as $key => $value) {
215            $key = strtolower($key);
216
217            if (!$value && isset($this->booleanAttributes[$key])) {
218                // Skip boolean attributes that expect empty string as false value
219                if ('' === $this->booleanAttributes[$key]['off']) {
220                    continue;
221                }
222            }
223
224            //check if attribute is translatable
225            if (isset($this->translatableAttributes[$key]) && !empty($value)) {
226                if (($translator = $this->getTranslator()) !== null) {
227                    $value = $translator->translate($value, $this->getTranslatorTextDomain());
228                }
229            }
230
231            //@TODO Escape event attributes like AbstractHtmlElement view helper does in htmlAttribs ??
232            $strings[] = sprintf('%s="%s"', $escape($key), $escapeAttr($value));
233        }
234
235        return implode(' ', $strings);
236    }
237
238    /**
239     * Get the ID of an element
240     *
241     * If no ID attribute present, attempts to use the name attribute.
242     * If no name attribute is present, either, returns null.
243     *
244     * @param  ElementInterface $element
245     * @return null|string
246     */
247    public function getId(ElementInterface $element)
248    {
249        $id = $element->getAttribute('id');
250        if (null !== $id) {
251            return $id;
252        }
253
254        return $element->getName();
255    }
256
257    /**
258     * Get the closing bracket for an inline tag
259     *
260     * Closes as either "/>" for XHTML doctypes or ">" otherwise.
261     *
262     * @return string
263     */
264    public function getInlineClosingBracket()
265    {
266        $doctypeHelper = $this->getDoctypeHelper();
267        if ($doctypeHelper->isXhtml()) {
268            return '/>';
269        }
270        return '>';
271    }
272
273    /**
274     * Retrieve the doctype helper
275     *
276     * @return Doctype
277     */
278    protected function getDoctypeHelper()
279    {
280        if ($this->doctypeHelper) {
281            return $this->doctypeHelper;
282        }
283
284        if (method_exists($this->view, 'plugin')) {
285            $this->doctypeHelper = $this->view->plugin('doctype');
286        }
287
288        if (!$this->doctypeHelper instanceof Doctype) {
289            $this->doctypeHelper = new Doctype();
290        }
291
292        return $this->doctypeHelper;
293    }
294
295    /**
296     * Retrieve the escapeHtml helper
297     *
298     * @return EscapeHtml
299     */
300    protected function getEscapeHtmlHelper()
301    {
302        if ($this->escapeHtmlHelper) {
303            return $this->escapeHtmlHelper;
304        }
305
306        if (method_exists($this->view, 'plugin')) {
307            $this->escapeHtmlHelper = $this->view->plugin('escapehtml');
308        }
309
310        if (!$this->escapeHtmlHelper instanceof EscapeHtml) {
311            $this->escapeHtmlHelper = new EscapeHtml();
312        }
313
314        return $this->escapeHtmlHelper;
315    }
316
317    /**
318     * Retrieve the escapeHtmlAttr helper
319     *
320     * @return EscapeHtmlAttr
321     */
322    protected function getEscapeHtmlAttrHelper()
323    {
324        if ($this->escapeHtmlAttrHelper) {
325            return $this->escapeHtmlAttrHelper;
326        }
327
328        if (method_exists($this->view, 'plugin')) {
329            $this->escapeHtmlAttrHelper = $this->view->plugin('escapehtmlattr');
330        }
331
332        if (!$this->escapeHtmlAttrHelper instanceof EscapeHtmlAttr) {
333            $this->escapeHtmlAttrHelper = new EscapeHtmlAttr();
334        }
335
336        return $this->escapeHtmlAttrHelper;
337    }
338
339    /**
340     * Prepare attributes for rendering
341     *
342     * Ensures appropriate attributes are present (e.g., if "name" is present,
343     * but no "id", sets the latter to the former).
344     *
345     * Removes any invalid attributes
346     *
347     * @param  array $attributes
348     * @return array
349     */
350    protected function prepareAttributes(array $attributes)
351    {
352        foreach ($attributes as $key => $value) {
353            $attribute = strtolower($key);
354
355            if (!isset($this->validGlobalAttributes[$attribute])
356                && !isset($this->validTagAttributes[$attribute])
357                && 'data-' != substr($attribute, 0, 5)
358                && 'x-' != substr($attribute, 0, 2)
359            ) {
360                // Invalid attribute for the current tag
361                unset($attributes[$key]);
362                continue;
363            }
364
365            // Normalize attribute key, if needed
366            if ($attribute != $key) {
367                unset($attributes[$key]);
368                $attributes[$attribute] = $value;
369            }
370
371            // Normalize boolean attribute values
372            if (isset($this->booleanAttributes[$attribute])) {
373                $attributes[$attribute] = $this->prepareBooleanAttributeValue($attribute, $value);
374            }
375        }
376
377        return $attributes;
378    }
379
380    /**
381     * Prepare a boolean attribute value
382     *
383     * Prepares the expected representation for the boolean attribute specified.
384     *
385     * @param  string $attribute
386     * @param  mixed $value
387     * @return string
388     */
389    protected function prepareBooleanAttributeValue($attribute, $value)
390    {
391        if (!is_bool($value) && in_array($value, $this->booleanAttributes[$attribute])) {
392            return $value;
393        }
394
395        $value = (bool) $value;
396        return ($value
397            ? $this->booleanAttributes[$attribute]['on']
398            : $this->booleanAttributes[$attribute]['off']
399        );
400    }
401}
402