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\View\Helper; 11 12use stdClass; 13use Zend\View; 14use Zend\View\Exception; 15 16/** 17 * Helper for setting and retrieving script elements for HTML head section 18 * 19 * Allows the following method calls: 20 * @method HeadScript appendFile($src, $type = 'text/javascript', $attrs = array()) 21 * @method HeadScript offsetSetFile($index, $src, $type = 'text/javascript', $attrs = array()) 22 * @method HeadScript prependFile($src, $type = 'text/javascript', $attrs = array()) 23 * @method HeadScript setFile($src, $type = 'text/javascript', $attrs = array()) 24 * @method HeadScript appendScript($script, $type = 'text/javascript', $attrs = array()) 25 * @method HeadScript offsetSetScript($index, $src, $type = 'text/javascript', $attrs = array()) 26 * @method HeadScript prependScript($script, $type = 'text/javascript', $attrs = array()) 27 * @method HeadScript setScript($script, $type = 'text/javascript', $attrs = array()) 28 */ 29class HeadScript extends Placeholder\Container\AbstractStandalone 30{ 31 /** 32 * Script type constants 33 * 34 * @const string 35 */ 36 const FILE = 'FILE'; 37 const SCRIPT = 'SCRIPT'; 38 39 /** 40 * Registry key for placeholder 41 * 42 * @var string 43 */ 44 protected $regKey = 'Zend_View_Helper_HeadScript'; 45 46 /** 47 * Are arbitrary attributes allowed? 48 * 49 * @var bool 50 */ 51 protected $arbitraryAttributes = false; 52 53 /** 54 * Is capture lock? 55 * 56 * @var bool 57 */ 58 protected $captureLock; 59 60 /** 61 * Capture type 62 * 63 * @var string 64 */ 65 protected $captureScriptType; 66 67 /** 68 * Capture attributes 69 * 70 * @var null|array 71 */ 72 protected $captureScriptAttrs = null; 73 74 /** 75 * Capture type (append, prepend, set) 76 * 77 * @var string 78 */ 79 protected $captureType; 80 81 /** 82 * Optional allowed attributes for script tag 83 * 84 * @var array 85 */ 86 protected $optionalAttributes = array( 87 'charset', 88 'crossorigin', 89 'defer', 90 'language', 91 'src', 92 ); 93 94 /** 95 * Required attributes for script tag 96 * 97 * @var string 98 */ 99 protected $requiredAttributes = array('type'); 100 101 /** 102 * Whether or not to format scripts using CDATA; used only if doctype 103 * helper is not accessible 104 * 105 * @var bool 106 */ 107 public $useCdata = false; 108 109 /** 110 * Constructor 111 * 112 * Set separator to PHP_EOL. 113 */ 114 public function __construct() 115 { 116 parent::__construct(); 117 118 $this->setSeparator(PHP_EOL); 119 } 120 121 /** 122 * Return headScript object 123 * 124 * Returns headScript helper object; optionally, allows specifying a script 125 * or script file to include. 126 * 127 * @param string $mode Script or file 128 * @param string $spec Script/url 129 * @param string $placement Append, prepend, or set 130 * @param array $attrs Array of script attributes 131 * @param string $type Script type and/or array of script attributes 132 * @return HeadScript 133 */ 134 public function __invoke( 135 $mode = self::FILE, 136 $spec = null, 137 $placement = 'APPEND', 138 array $attrs = array(), 139 $type = 'text/javascript' 140 ) { 141 if ((null !== $spec) && is_string($spec)) { 142 $action = ucfirst(strtolower($mode)); 143 $placement = strtolower($placement); 144 switch ($placement) { 145 case 'set': 146 case 'prepend': 147 case 'append': 148 $action = $placement . $action; 149 break; 150 default: 151 $action = 'append' . $action; 152 break; 153 } 154 $this->$action($spec, $type, $attrs); 155 } 156 157 return $this; 158 } 159 160 /** 161 * Overload method access 162 * 163 * @param string $method Method to call 164 * @param array $args Arguments of method 165 * @throws Exception\BadMethodCallException if too few arguments or invalid method 166 * @return HeadScript 167 */ 168 public function __call($method, $args) 169 { 170 if (preg_match('/^(?P<action>set|(ap|pre)pend|offsetSet)(?P<mode>File|Script)$/', $method, $matches)) { 171 if (1 > count($args)) { 172 throw new Exception\BadMethodCallException(sprintf( 173 'Method "%s" requires at least one argument', 174 $method 175 )); 176 } 177 178 $action = $matches['action']; 179 $mode = strtolower($matches['mode']); 180 $type = 'text/javascript'; 181 $attrs = array(); 182 183 if ('offsetSet' == $action) { 184 $index = array_shift($args); 185 if (1 > count($args)) { 186 throw new Exception\BadMethodCallException(sprintf( 187 'Method "%s" requires at least two arguments, an index and source', 188 $method 189 )); 190 } 191 } 192 193 $content = $args[0]; 194 195 if (isset($args[1])) { 196 $type = (string) $args[1]; 197 } 198 if (isset($args[2])) { 199 $attrs = (array) $args[2]; 200 } 201 202 switch ($mode) { 203 case 'script': 204 $item = $this->createData($type, $attrs, $content); 205 if ('offsetSet' == $action) { 206 $this->offsetSet($index, $item); 207 } else { 208 $this->$action($item); 209 } 210 break; 211 case 'file': 212 default: 213 if (!$this->isDuplicate($content)) { 214 $attrs['src'] = $content; 215 $item = $this->createData($type, $attrs); 216 if ('offsetSet' == $action) { 217 $this->offsetSet($index, $item); 218 } else { 219 $this->$action($item); 220 } 221 } 222 break; 223 } 224 225 return $this; 226 } 227 228 return parent::__call($method, $args); 229 } 230 231 /** 232 * Retrieve string representation 233 * 234 * @param string|int $indent Amount of whitespaces or string to use for indention 235 * @return string 236 */ 237 public function toString($indent = null) 238 { 239 $indent = (null !== $indent) 240 ? $this->getWhitespace($indent) 241 : $this->getIndent(); 242 243 if ($this->view) { 244 $useCdata = $this->view->plugin('doctype')->isXhtml(); 245 } else { 246 $useCdata = $this->useCdata; 247 } 248 249 $escapeStart = ($useCdata) ? '//<![CDATA[' : '//<!--'; 250 $escapeEnd = ($useCdata) ? '//]]>' : '//-->'; 251 252 $items = array(); 253 $this->getContainer()->ksort(); 254 foreach ($this as $item) { 255 if (!$this->isValid($item)) { 256 continue; 257 } 258 259 $items[] = $this->itemToString($item, $indent, $escapeStart, $escapeEnd); 260 } 261 262 return implode($this->getSeparator(), $items); 263 } 264 265 /** 266 * Start capture action 267 * 268 * @param mixed $captureType Type of capture 269 * @param string $type Type of script 270 * @param array $attrs Attributes of capture 271 * @throws Exception\RuntimeException 272 * @return void 273 */ 274 public function captureStart( 275 $captureType = Placeholder\Container\AbstractContainer::APPEND, 276 $type = 'text/javascript', 277 $attrs = array() 278 ) { 279 if ($this->captureLock) { 280 throw new Exception\RuntimeException('Cannot nest headScript captures'); 281 } 282 283 $this->captureLock = true; 284 $this->captureType = $captureType; 285 $this->captureScriptType = $type; 286 $this->captureScriptAttrs = $attrs; 287 ob_start(); 288 } 289 290 /** 291 * End capture action and store 292 * 293 * @return void 294 */ 295 public function captureEnd() 296 { 297 $content = ob_get_clean(); 298 $type = $this->captureScriptType; 299 $attrs = $this->captureScriptAttrs; 300 $this->captureScriptType = null; 301 $this->captureScriptAttrs = null; 302 $this->captureLock = false; 303 304 switch ($this->captureType) { 305 case Placeholder\Container\AbstractContainer::SET: 306 case Placeholder\Container\AbstractContainer::PREPEND: 307 case Placeholder\Container\AbstractContainer::APPEND: 308 $action = strtolower($this->captureType) . 'Script'; 309 break; 310 default: 311 $action = 'appendScript'; 312 break; 313 } 314 315 $this->$action($content, $type, $attrs); 316 } 317 318 /** 319 * Create data item containing all necessary components of script 320 * 321 * @param string $type Type of data 322 * @param array $attributes Attributes of data 323 * @param string $content Content of data 324 * @return stdClass 325 */ 326 public function createData($type, array $attributes, $content = null) 327 { 328 $data = new stdClass(); 329 $data->type = $type; 330 $data->attributes = $attributes; 331 $data->source = $content; 332 333 return $data; 334 } 335 336 /** 337 * Is the file specified a duplicate? 338 * 339 * @param string $file Name of file to check 340 * @return bool 341 */ 342 protected function isDuplicate($file) 343 { 344 foreach ($this->getContainer() as $item) { 345 if (($item->source === null) 346 && array_key_exists('src', $item->attributes) 347 && ($file == $item->attributes['src']) 348 ) { 349 return true; 350 } 351 } 352 353 return false; 354 } 355 356 /** 357 * Is the script provided valid? 358 * 359 * @param mixed $value Is the given script valid? 360 * @return bool 361 */ 362 protected function isValid($value) 363 { 364 if ((!$value instanceof stdClass) 365 || !isset($value->type) 366 || (!isset($value->source) 367 && !isset($value->attributes)) 368 ) { 369 return false; 370 } 371 372 return true; 373 } 374 375 /** 376 * Create script HTML 377 * 378 * @param mixed $item Item to convert 379 * @param string $indent String to add before the item 380 * @param string $escapeStart Starting sequence 381 * @param string $escapeEnd Ending sequence 382 * @return string 383 */ 384 public function itemToString($item, $indent, $escapeStart, $escapeEnd) 385 { 386 $attrString = ''; 387 if (!empty($item->attributes)) { 388 foreach ($item->attributes as $key => $value) { 389 if ((!$this->arbitraryAttributesAllowed() && !in_array($key, $this->optionalAttributes)) 390 || in_array($key, array('conditional', 'noescape'))) { 391 continue; 392 } 393 if ('defer' == $key) { 394 $value = 'defer'; 395 } 396 $attrString .= sprintf(' %s="%s"', $key, ($this->autoEscape) ? $this->escape($value) : $value); 397 } 398 } 399 400 $addScriptEscape = !(isset($item->attributes['noescape']) 401 && filter_var($item->attributes['noescape'], FILTER_VALIDATE_BOOLEAN)); 402 403 $type = ($this->autoEscape) ? $this->escape($item->type) : $item->type; 404 $html = '<script type="' . $type . '"' . $attrString . '>'; 405 if (!empty($item->source)) { 406 $html .= PHP_EOL; 407 408 if ($addScriptEscape) { 409 $html .= $indent . ' ' . $escapeStart . PHP_EOL; 410 } 411 412 $html .= $indent . ' ' . $item->source; 413 414 if ($addScriptEscape) { 415 $html .= PHP_EOL . $indent . ' ' . $escapeEnd; 416 } 417 418 $html .= PHP_EOL . $indent; 419 } 420 $html .= '</script>'; 421 422 if (isset($item->attributes['conditional']) 423 && !empty($item->attributes['conditional']) 424 && is_string($item->attributes['conditional']) 425 ) { 426 // inner wrap with comment end and start if !IE 427 if (str_replace(' ', '', $item->attributes['conditional']) === '!IE') { 428 $html = '<!-->' . $html . '<!--'; 429 } 430 $html = $indent . '<!--[if ' . $item->attributes['conditional'] . ']>' . $html . '<![endif]-->'; 431 } else { 432 $html = $indent . $html; 433 } 434 435 return $html; 436 } 437 438 /** 439 * Override append 440 * 441 * @param string $value Append script or file 442 * @throws Exception\InvalidArgumentException 443 * @return void 444 */ 445 public function append($value) 446 { 447 if (!$this->isValid($value)) { 448 throw new Exception\InvalidArgumentException( 449 'Invalid argument passed to append(); ' 450 . 'please use one of the helper methods, appendScript() or appendFile()' 451 ); 452 } 453 454 return $this->getContainer()->append($value); 455 } 456 457 /** 458 * Override prepend 459 * 460 * @param string $value Prepend script or file 461 * @throws Exception\InvalidArgumentException 462 * @return void 463 */ 464 public function prepend($value) 465 { 466 if (!$this->isValid($value)) { 467 throw new Exception\InvalidArgumentException( 468 'Invalid argument passed to prepend(); ' 469 . 'please use one of the helper methods, prependScript() or prependFile()' 470 ); 471 } 472 473 return $this->getContainer()->prepend($value); 474 } 475 476 /** 477 * Override set 478 * 479 * @param string $value Set script or file 480 * @throws Exception\InvalidArgumentException 481 * @return void 482 */ 483 public function set($value) 484 { 485 if (!$this->isValid($value)) { 486 throw new Exception\InvalidArgumentException( 487 'Invalid argument passed to set(); please use one of the helper methods, setScript() or setFile()' 488 ); 489 } 490 491 return $this->getContainer()->set($value); 492 } 493 494 /** 495 * Override offsetSet 496 * 497 * @param string|int $index Set script of file offset 498 * @param mixed $value 499 * @throws Exception\InvalidArgumentException 500 * @return void 501 */ 502 public function offsetSet($index, $value) 503 { 504 if (!$this->isValid($value)) { 505 throw new Exception\InvalidArgumentException( 506 'Invalid argument passed to offsetSet(); ' 507 . 'please use one of the helper methods, offsetSetScript() or offsetSetFile()' 508 ); 509 } 510 511 return $this->getContainer()->offsetSet($index, $value); 512 } 513 514 /** 515 * Set flag indicating if arbitrary attributes are allowed 516 * 517 * @param bool $flag Set flag 518 * @return HeadScript 519 */ 520 public function setAllowArbitraryAttributes($flag) 521 { 522 $this->arbitraryAttributes = (bool) $flag; 523 return $this; 524 } 525 526 /** 527 * Are arbitrary attributes allowed? 528 * 529 * @return bool 530 */ 531 public function arbitraryAttributesAllowed() 532 { 533 return $this->arbitraryAttributes; 534 } 535} 536