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