PageRenderTime 55ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/base/lib/flourishlib/fTemplating.php

https://bitbucket.org/thanhtungnguyenphp/monitos
PHP | 1512 lines | 875 code | 228 blank | 409 comment | 168 complexity | d1c31023734e8bcaaa625923e74f8d11 MD5 | raw file
  1. <?php
  2. /**
  3. * Allows for quick and flexible HTML templating
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Matt Nowack [mn] <mdnowack@gmail.com>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fTemplating
  12. *
  13. * @version 1.0.0b19
  14. * @changes 1.0.0b19 Corrected a bug in ::enablePHPShortTags() that would prevent proper translation inside of HTML tag attributes [wb, 2011-01-09]
  15. * @changes 1.0.0b18 Fixed a bug with CSS minification and black hex codes [wb, 2010-10-10]
  16. * @changes 1.0.0b17 Backwards Compatibility Break - ::delete() now returns the values of the element or elements that were deleted instead of returning the fTemplating instance [wb, 2010-09-19]
  17. * @changes 1.0.0b16 Fixed another bug with minifying JS regex literals [wb, 2010-09-13]
  18. * @changes 1.0.0b15 Fixed a bug with minifying JS regex literals that occur after a reserved word [wb, 2010-09-12]
  19. * @changes 1.0.0b14 Added documentation about `[sub-key]` syntax [wb, 2010-09-12]
  20. * @changes 1.0.0b13 Backwards Compatibility Break - ::add(), ::delete(), ::get() and ::set() now interpret `[` and `]` as array shorthand and thus they can not be used in element names, renamed ::remove() to ::filter() - added `$beginning` parameter to ::add() and added ::remove() method [wb, 2010-09-12]
  21. * @changes 1.0.0b12 Added ::enableMinification(), ::enablePHPShortTags(), the ability to be able to place child fTemplating objects via a new magic element `__main__` and the `$main_element` parameter for ::__construct() [wb, 2010-08-31]
  22. * @changes 1.0.0b11 Fixed a bug with the elements not being initialized to a blank array [wb, 2010-08-12]
  23. * @changes 1.0.0b10 Updated ::place() to ignore URL query strings when detecting an element type [wb, 2010-07-26]
  24. * @changes 1.0.0b9 Added the methods ::delete() and ::remove() [wb+mn, 2010-07-15]
  25. * @changes 1.0.0b8 Fixed a bug with placing absolute file paths on Windows [wb, 2010-07-09]
  26. * @changes 1.0.0b7 Removed `e` flag from preg_replace() calls [wb, 2010-06-08]
  27. * @changes 1.0.0b6 Changed ::set() and ::add() to return the object for method chaining, changed ::set() and ::get() to accept arrays of elements [wb, 2010-06-02]
  28. * @changes 1.0.0b5 Added ::encode() [wb, 2010-05-20]
  29. * @changes 1.0.0b4 Added ::create() and ::retrieve() for named fTemplating instances [wb, 2010-05-11]
  30. * @changes 1.0.0b3 Fixed an issue with placing relative file path [wb, 2010-04-23]
  31. * @changes 1.0.0b2 Added the ::inject() method [wb, 2009-01-09]
  32. * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
  33. */
  34. class fTemplating
  35. {
  36. const create = 'fTemplating::create';
  37. const reset = 'fTemplating::reset';
  38. const retrieve = 'fTemplating::retrieve';
  39. /**
  40. * Named fTemplating instances
  41. *
  42. * @var array
  43. */
  44. static $instances = array();
  45. /**
  46. * Creates a named template that can be accessed from any scope via ::retrieve()
  47. *
  48. * @param string $name The name for this template instance
  49. * @param string $root The filesystem path to use when accessing relative files, defaults to `$_SERVER['DOCUMENT_ROOT']`
  50. * @return fTemplating The new fTemplating instance
  51. */
  52. static public function create($name, $root=NULL)
  53. {
  54. self::$instances[$name] = new self($root);
  55. return self::$instances[$name];
  56. }
  57. /**
  58. * Resets the configuration of the class
  59. *
  60. * @internal
  61. *
  62. * @return void
  63. */
  64. static public function reset()
  65. {
  66. self::$instances = array();
  67. }
  68. /**
  69. * Retrieves a named template
  70. *
  71. * @param string $name The name of the template to retrieve
  72. * @return fTemplating The specified fTemplating instance
  73. */
  74. static public function retrieve($name)
  75. {
  76. if (!isset(self::$instances[$name])) {
  77. throw new fProgrammerException(
  78. 'The named template specified, %s, has not been created yet',
  79. $name
  80. );
  81. }
  82. return self::$instances[$name];
  83. }
  84. /**
  85. * The buffered object id, used for differentiating different instances when doing replacements
  86. *
  87. * @var integer
  88. */
  89. private $buffered_id;
  90. /**
  91. * A data store for templating
  92. *
  93. * @var array
  94. */
  95. private $elements;
  96. /**
  97. * The directory to store minified code in
  98. *
  99. * @var string
  100. */
  101. private $minification_directory;
  102. /**
  103. * The path prefix to prepend to CSS and JS paths to find them on the filesystem
  104. *
  105. * @var string
  106. */
  107. private $minification_prefix;
  108. /**
  109. * The minification mode: development or production
  110. *
  111. * @var string
  112. */
  113. private $minification_mode;
  114. /**
  115. * The directory to look for files
  116. *
  117. * @var string
  118. */
  119. protected $root;
  120. /**
  121. * The directory to store PHP files with short tags fixed
  122. *
  123. * @var string
  124. */
  125. private $short_tag_directory;
  126. /**
  127. * The short tag mode: development or production
  128. *
  129. * @var string
  130. */
  131. private $short_tag_mode;
  132. /**
  133. * Initializes this templating engine
  134. *
  135. * @param string $root The filesystem path to use when accessing relative files, defaults to `$_SERVER['DOCUMENT_ROOT']`
  136. * @param string $main_element The value for the `__main__` element - this is used when calling ::place() without an element, or when placing fTemplating objects as children
  137. * @return fTemplating
  138. */
  139. public function __construct($root=NULL, $main_element=NULL)
  140. {
  141. if ($root === NULL) {
  142. $root = $_SERVER['DOCUMENT_ROOT'];
  143. }
  144. if (!file_exists($root)) {
  145. throw new fProgrammerException(
  146. 'The root specified, %s, does not exist on the filesystem',
  147. $root
  148. );
  149. }
  150. if (!is_readable($root)) {
  151. throw new fEnvironmentException(
  152. 'The root specified, %s, is not readable',
  153. $root
  154. );
  155. }
  156. if (substr($root, -1) != '/' && substr($root, -1) != '\\') {
  157. $root .= DIRECTORY_SEPARATOR;
  158. }
  159. $this->buffered_id = NULL;
  160. $this->elements = array();
  161. $this->root = $root;
  162. if ($main_element !== NULL) {
  163. $this->set('__main__', $main_element);
  164. }
  165. }
  166. /**
  167. * Finishing placing elements if buffering was used
  168. *
  169. * @internal
  170. *
  171. * @return void
  172. */
  173. public function __destruct()
  174. {
  175. // The __destruct method can't throw unhandled exceptions intelligently, so we will always catch here just in case
  176. try {
  177. $this->placeBuffered();
  178. } catch (Exception $e) {
  179. fCore::handleException($e);
  180. }
  181. }
  182. /**
  183. * All requests that hit this method should be requests for callbacks
  184. *
  185. * @internal
  186. *
  187. * @param string $method The method to create a callback for
  188. * @return callback The callback for the method requested
  189. */
  190. public function __get($method)
  191. {
  192. return array($this, $method);
  193. }
  194. /**
  195. * Adds a value to an array element
  196. *
  197. * @param string $element The element to add to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  198. * @param mixed $value The value to add
  199. * @param boolean $beginning If the value should be added to the beginning of the element
  200. * @return fTemplating The template object, to allow for method chaining
  201. */
  202. public function add($element, $value, $beginning=FALSE)
  203. {
  204. $tip =& $this->elements;
  205. if ($bracket_pos = strpos($element, '[')) {
  206. $original_element = $element;
  207. $array_dereference = substr($element, $bracket_pos);
  208. $element = substr($element, 0, $bracket_pos);
  209. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  210. $array_keys = array_map('current', $array_keys);
  211. array_unshift($array_keys, $element);
  212. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  213. if (!isset($tip[$array_key])) {
  214. $tip[$array_key] = array();
  215. } elseif (!is_array($tip[$array_key])) {
  216. throw new fProgrammerException(
  217. '%1$s was called for an element, %2$s, which is not an array',
  218. 'add()',
  219. $original_element
  220. );
  221. }
  222. $tip =& $tip[$array_key];
  223. }
  224. $element = end($array_keys);
  225. }
  226. if (!isset($tip[$element])) {
  227. $tip[$element] = array();
  228. } elseif (!is_array($tip[$element])) {
  229. throw new fProgrammerException(
  230. '%1$s was called for an element, %2$s, which is not an array',
  231. 'add()',
  232. $element
  233. );
  234. }
  235. if ($beginning) {
  236. array_unshift($tip[$element], $value);
  237. } else {
  238. $tip[$element][] = $value;
  239. }
  240. return $this;
  241. }
  242. /**
  243. * Enables buffered output, allowing ::set() and ::add() to happen after a ::place() but act as if they were done before
  244. *
  245. * Please note that using buffered output will affect the order in which
  246. * code is executed since the elements are not actually ::place()'ed until
  247. * the destructor is called.
  248. *
  249. * If the non-template code depends on template code being executed
  250. * sequentially before it, you may not want to use output buffering.
  251. *
  252. * @return void
  253. */
  254. public function buffer()
  255. {
  256. static $id_sequence = 1;
  257. if ($this->buffered_id) {
  258. throw new fProgrammerException('Buffering has already been started');
  259. }
  260. if (!fBuffer::isStarted()) {
  261. fBuffer::start();
  262. }
  263. $this->buffered_id = $id_sequence;
  264. $id_sequence++;
  265. }
  266. /**
  267. * Deletes an element from the template
  268. *
  269. * @param string $element The element to delete - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  270. * @param mixed $default_value The value to return if the `$element` is not set
  271. * @param array |$elements The elements to delete - an array of element names or an associative array of keys being element names and the values being the default values
  272. * @return mixed The value of the `$element` that was deleted - an associative array of deleted elements will be returned if an array of `$elements` was specified
  273. */
  274. public function delete($element, $default_value=NULL)
  275. {
  276. if (is_array($element)) {
  277. $elements = $element;
  278. if (is_numeric(key($elements))) {
  279. $new_elements = array();
  280. foreach ($elements as $element) {
  281. $new_elements[$element] = NULL;
  282. }
  283. $elements = $new_elements;
  284. }
  285. $output = array();
  286. foreach ($elements as $key => $default_value) {
  287. $output[$key] = $this->delete($key, $default_value);
  288. }
  289. return $output;
  290. }
  291. $tip =& $this->elements;
  292. $value = $default_value;
  293. if ($bracket_pos = strpos($element, '[')) {
  294. $original_element = $element;
  295. $array_dereference = substr($element, $bracket_pos);
  296. $element = substr($element, 0, $bracket_pos);
  297. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  298. $array_keys = array_map('current', $array_keys);
  299. array_unshift($array_keys, $element);
  300. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  301. if (!isset($tip[$array_key])) {
  302. return $value;
  303. } elseif (!is_array($tip[$array_key])) {
  304. throw new fProgrammerException(
  305. '%1$s was called for an element, %2$s, which is not an array',
  306. 'delete()',
  307. $original_element
  308. );
  309. }
  310. $tip =& $tip[$array_key];
  311. }
  312. $element = end($array_keys);
  313. }
  314. if (isset($tip[$element])) {
  315. $value = $tip[$element];
  316. unset($tip[$element]);
  317. }
  318. return $value;
  319. }
  320. /**
  321. * Erases all output since the invocation of the template - only works if buffering is on
  322. *
  323. * @return void
  324. */
  325. public function destroy()
  326. {
  327. if (!$this->buffered_id) {
  328. throw new fProgrammerException(
  329. 'A template can only be destroyed if buffering has been enabled'
  330. );
  331. }
  332. $this->buffered_id = NULL;
  333. fBuffer::erase();
  334. fBuffer::stop();
  335. $this->__destruct();
  336. }
  337. /**
  338. * Enables minified output for CSS and JS elements
  339. *
  340. * For CSS and JS, compilation means that the file will be minified and
  341. * cached. The filename will change whenever the content change, allowing
  342. * for far-futures expire headers.
  343. *
  344. * Please note that this option requires that all CSS and JS paths be
  345. * relative to the $_SERVER['DOCUMENT_ROOT'] and start with a `/`. Also
  346. * this class will not clean up old cached files out of the cache
  347. * directory.
  348. *
  349. * This functionality will be inherited by all child fTemplating objects
  350. * that do not have their own explicit minification settings.
  351. *
  352. * @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
  353. * @param fDirectory|string $cache_directory The directory to cache the compiled files into - this needs to be inside the document root or a path added to fFilesystem::addWebPathTranslation()
  354. * @param fDirectory|string $path_prefix The directory to prepend to all CSS and JS paths to load the files from the filesystem - this defaults to `$_SERVER['DOCUMENT_ROOT']`
  355. * @return void
  356. */
  357. public function enableMinification($mode, $cache_directory, $path_prefix=NULL)
  358. {
  359. $valid_modes = array('development', 'production');
  360. if (!in_array($mode, $valid_modes)) {
  361. throw new fProgrammerException(
  362. 'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
  363. $mode,
  364. join(', ', $valid_modes)
  365. );
  366. }
  367. $cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : $cache_directory;
  368. if (!is_writable($cache_directory)) {
  369. throw new fEnvironmentException(
  370. 'The cache directory specified, %s, is not writable',
  371. $cache_directory
  372. );
  373. }
  374. $path_prefix = $path_prefix instanceof fDirectory ? $path_prefix->getPath() : $path_prefix;
  375. if ($path_prefix === NULL) {
  376. $path_prefix = $_SERVER['DOCUMENT_ROOT'];
  377. }
  378. $this->minification_mode = $mode;
  379. $this->minification_directory = fDirectory::makeCanonical($cache_directory);
  380. $this->minification_prefix = $path_prefix;
  381. }
  382. /**
  383. * Converts PHP short tags to long tags when short tags are turned off
  384. *
  385. * Please note that this only affects PHP files that are **directly**
  386. * evaluated with ::place() or ::inject(). It will not affect PHP files that
  387. * have been evaluated via `include` or `require` statements inside of the
  388. * directly evaluated PHP files.
  389. *
  390. * This functionality will be inherited by all child fTemplating objects
  391. * that do not have their own explicit short tag settings.
  392. *
  393. * @param string $mode The compilation mode - `'development'` means that file modification times will be checked on each load, `'production'` means that the cache files will only be regenerated when missing
  394. * @param fDirectory|string $cache_directory The directory to cache the compiled files into - this directory should not be accessible from the web
  395. * @return void
  396. */
  397. public function enablePHPShortTags($mode, $cache_directory)
  398. {
  399. // This does not need to be enabled if short tags are on
  400. if (ini_get('short_open_tag') && strtolower(ini_get('short_open_tag')) != 'off') {
  401. return;
  402. }
  403. $valid_modes = array('development', 'production');
  404. if (!in_array($mode, $valid_modes)) {
  405. throw new fProgrammerException(
  406. 'The mode specified, %1$s, is invalid. Must be one of: %2$s.',
  407. $mode,
  408. join(', ', $valid_modes)
  409. );
  410. }
  411. $cache_directory = $cache_directory instanceof fDirectory ? $cache_directory->getPath() : $cache_directory;
  412. if (!is_writable($cache_directory)) {
  413. throw new fEnvironmentException(
  414. 'The cache directory specified, %s, is not writable',
  415. $cache_directory
  416. );
  417. }
  418. $this->short_tag_mode = $mode;
  419. $this->short_tag_directory = fDirectory::makeCanonical($cache_directory);
  420. }
  421. /**
  422. * Gets the value of an element and runs it through fHTML::encode()
  423. *
  424. * @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  425. * @param mixed $default_value The value to return if the element has not been set
  426. * @return mixed The value of the element specified run through fHTML::encode(), or the default value if it has not been set
  427. */
  428. public function encode($element, $default_value=NULL)
  429. {
  430. return fHTML::encode($this->get($element, $default_value));
  431. }
  432. /**
  433. * Removes a value from an array element
  434. *
  435. * @param string $element The element to remove from - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  436. * @param mixed $value The value to remove - compared in a non-strict manner, such that removing `0` will remove a blank string and false also
  437. * @return fTemplating The template object, to allow for method chaining
  438. */
  439. public function filter($element, $value)
  440. {
  441. $tip =& $this->elements;
  442. if ($bracket_pos = strpos($element, '[')) {
  443. $original_element = $element;
  444. $array_dereference = substr($element, $bracket_pos);
  445. $element = substr($element, 0, $bracket_pos);
  446. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  447. $array_keys = array_map('current', $array_keys);
  448. array_unshift($array_keys, $element);
  449. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  450. if (!isset($tip[$array_key])) {
  451. return $this;
  452. } elseif (!is_array($tip[$array_key])) {
  453. throw new fProgrammerException(
  454. '%1$s was called for an element, %2$s, which is not an array',
  455. 'filter()',
  456. $original_element
  457. );
  458. }
  459. $tip =& $tip[$array_key];
  460. }
  461. $element = end($array_keys);
  462. }
  463. if (!isset($tip[$element])) {
  464. return $this;
  465. } elseif (!is_array($tip[$element])) {
  466. throw new fProgrammerException(
  467. '%1$s was called for an element, %2$s, which is not an array',
  468. 'filter()',
  469. $element
  470. );
  471. }
  472. $keys = array_keys($tip[$element], $value);
  473. if ($keys) {
  474. foreach ($keys as $key) {
  475. unset($tip[$element][$key]);
  476. }
  477. $tip[$element] = array_values($tip[$element]);
  478. }
  479. return $this;
  480. }
  481. /**
  482. * Takes an array of PHP files and caches a version with all short tags converted to regular tags
  483. *
  484. * @param array $values The file paths to the PHP files
  485. * @return array An array of file paths to the corresponding converted PHP files
  486. */
  487. private function fixShortTags($values)
  488. {
  489. $fixed_paths = array();
  490. foreach ($values as $value) {
  491. // Check to see if the element is a path relative to the template root
  492. if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $value)) {
  493. $value = $this->root . $value;
  494. }
  495. $real_value = realpath($value);
  496. $cache_path = $this->short_tag_directory . sha1($real_value) . '.php';
  497. $fixed_paths[] = $cache_path;
  498. if (file_exists($cache_path) && ($this->short_tag_mode == 'production' || filemtime($cache_path) >= filemtime($real_value))) {
  499. continue;
  500. }
  501. $code = file_get_contents($real_value);
  502. $output = '';
  503. $in_php = FALSE;
  504. do {
  505. if (!$in_php) {
  506. $token_regex = '<\?';
  507. } else {
  508. $token_regex .= '/\*|//|\\#|\'|"|<<<[a-z_]\w*|<<<\'[a-z_]\w*\'|\?>';
  509. }
  510. if (!preg_match('#' . $token_regex . '#i', $code, $match)) {
  511. $part = $code;
  512. $code = '';
  513. $token = NULL;
  514. } else {
  515. $token = $match[0];
  516. $pos = strpos($code, $token);
  517. if ($pos === FALSE) {
  518. break;
  519. }
  520. $part = substr($code, 0, $pos);
  521. $code = substr($code, $pos);
  522. }
  523. $regex = NULL;
  524. if ($token == "<?") {
  525. $output .= $part;
  526. $in_php = TRUE;
  527. continue;
  528. } elseif ($token == "?>") {
  529. $regex = NULL;
  530. $in_php = FALSE;
  531. } elseif ($token == "//") {
  532. $regex = '#^//.*(\n|$)#D';
  533. } elseif ($token == "#") {
  534. $regex = '@^#.*(\n|$)@D';
  535. } elseif ($token == "/*") {
  536. $regex = '#^.{2}.*?(\*/|$)#sD';
  537. } elseif ($token == "'") {
  538. $regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
  539. } elseif ($token == '"') {
  540. $regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
  541. } elseif ($token) {
  542. $regex = '#\A<<<\'?([a-zA-Z_]\w*)\'?.*?^\1;\n#sm';
  543. }
  544. $part = str_replace('<?=', '<?php echo', $part);
  545. $part = preg_replace('#<\?(?!php)#i', '<?php', $part);
  546. // This makes sure that __FILE__ and __DIR__ stay as the
  547. // original value since the cached file will be in a different
  548. // place with a different filename
  549. $part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__FILE__(?=[^a-zA-Z0-9]|$)#iD', "'" . $real_value . "'", $part);
  550. if (fCore::checkVersion('5.3')) {
  551. $part = preg_replace('#(?<=[^a-zA-Z0-9]|^)__DIR__(?=[^a-zA-Z0-9]|$)#iD', "'" . dirname($real_value) . "'", $part);
  552. }
  553. $output .= $part;
  554. if ($regex) {
  555. preg_match($regex, $code, $match);
  556. $output .= $match[0];
  557. $code = substr($code, strlen($match[0]));
  558. }
  559. } while (strlen($code));
  560. file_put_contents($cache_path, $output);
  561. }
  562. return $fixed_paths;
  563. }
  564. /**
  565. * Gets the value of an element
  566. *
  567. * @param string $element The element to get - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  568. * @param mixed $default_value The value to return if the element has not been set
  569. * @param array |$elements An array of elements to get, or an associative array where a string key is the element to get and the value is the default value
  570. * @return mixed The value of the element(s) specified, or the default value(s) if it has not been set
  571. */
  572. public function get($element, $default_value=NULL)
  573. {
  574. if (is_array($element)) {
  575. $elements = $element;
  576. // Turn an array of elements into an array of elements with NULL default values
  577. if (array_values($elements) === $elements) {
  578. $elements = array_combine($elements, array_fill(0, count($elements), NULL));
  579. }
  580. $output = array();
  581. foreach ($elements as $element => $default_value) {
  582. $output[$element] = $this->get($element, $default_value);
  583. }
  584. return $output;
  585. }
  586. $array_dereference = NULL;
  587. if ($bracket_pos = strpos($element, '[')) {
  588. $array_dereference = substr($element, $bracket_pos);
  589. $element = substr($element, 0, $bracket_pos);
  590. }
  591. if (!isset($this->elements[$element])) {
  592. return $default_value;
  593. }
  594. $value = $this->elements[$element];
  595. if ($array_dereference) {
  596. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  597. $array_keys = array_map('current', $array_keys);
  598. foreach ($array_keys as $array_key) {
  599. if (!is_array($value) || !isset($value[$array_key])) {
  600. $value = $default_value;
  601. break;
  602. }
  603. $value = $value[$array_key];
  604. }
  605. }
  606. return $value;
  607. }
  608. /**
  609. * Combines an array of CSS or JS files and places them as a single file
  610. *
  611. * @param string $type The type of compilation, 'css' or 'js'
  612. * @param string $element The element name
  613. * @param array $values An array of file paths
  614. * @return void
  615. */
  616. protected function handleMinified($type, $element, $values)
  617. {
  618. $paths = array();
  619. $media = NULL;
  620. foreach ($values as $value) {
  621. if (is_array($value)) {
  622. $paths[] = $this->minification_prefix . $value['path'];
  623. if ($type == 'css') {
  624. $media = !empty($value['media']) ? $value['media'] : NULL;
  625. }
  626. } else {
  627. $paths[] = $this->minification_prefix . $value;
  628. }
  629. }
  630. $hash = sha1(join('|', $paths));
  631. $cache_file = $this->minification_directory . $hash . '.' . $type;
  632. $regenerate = FALSE;
  633. $checked_paths = FALSE;
  634. if (!file_exists($cache_file)) {
  635. $regenerate = TRUE;
  636. } elseif ($this->minification_mode == 'development') {
  637. $cache_mtime = filemtime($cache_file);
  638. $checked_paths = TRUE;
  639. foreach ($paths as $path) {
  640. if (!file_exists($path)) {
  641. throw new fEnvironmentException(
  642. 'The file specified, %s, does not exist under the $path_prefix specified',
  643. preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
  644. );
  645. }
  646. if (filemtime($path) > $cache_mtime) {
  647. $regenerate = TRUE;
  648. break;
  649. }
  650. }
  651. }
  652. if ($regenerate) {
  653. $minified = '';
  654. foreach ($paths as $path) {
  655. $path_cache_file = $this->minification_directory . sha1($path) . '.' . $type;
  656. if ($checked_paths && !file_exists($path)) {
  657. throw new fEnvironmentException(
  658. 'The file specified, %s, does not exist under the $path_prefix specified',
  659. preg_replace('#^' . preg_quote($this->minification_prefix, '#') . '#', '', $path)
  660. );
  661. }
  662. // Checks if this path has been cached
  663. if (file_exists($path_cache_file) && filemtime($path_cache_file) >= filemtime($path)) {
  664. $minified_path = file_get_contents($path_cache_file);
  665. } else {
  666. $minified_path = trim($this->minify(file_get_contents($path), $type));
  667. file_put_contents($path_cache_file, $minified_path);
  668. }
  669. $minified .= "\n" . $minified_path;
  670. }
  671. file_put_contents($cache_file, substr($minified, 1));
  672. }
  673. $version = filemtime($cache_file);
  674. $compiled_value = fFilesystem::translateToWebPath($cache_file) . '?v=' . $version;
  675. if ($type == 'css' && $media) {
  676. $compiled_value = array(
  677. 'path' => $compiled_value,
  678. 'media' => $media
  679. );
  680. }
  681. $method = 'place' . strtoupper($type);
  682. $this->$method($compiled_value);
  683. }
  684. /**
  685. * Includes the file specified - this is identical to ::place() except a filename is specified instead of an element
  686. *
  687. * Please see the ::place() method for more details about functionality.
  688. *
  689. * @param string $file_path The file to place
  690. * @param string $file_type Will force the file to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
  691. * @return void
  692. */
  693. public function inject($file_path, $file_type=NULL)
  694. {
  695. $prefix = '__injected_';
  696. $num = 1;
  697. while (isset($this->elements[$prefix . $num])) {
  698. $num++;
  699. }
  700. $element = $prefix . $num;
  701. $this->set($element, $file_path);
  702. $this->place($element, $file_type);
  703. }
  704. /**
  705. * Minifies JS or CSS
  706. *
  707. * For JS, this function is based on the JSMin algorithm (not the code) from
  708. * http://www.crockford.com/javascript/jsmin.html with the addition of
  709. * preserving /*! comment blocks for things like licenses. Some other
  710. * versions of JSMin change the contents of special comment blocks, but
  711. * this version does not.
  712. *
  713. * @param string $code The code to minify
  714. * @param string $type The type of code, 'css' or 'js'
  715. * @return string The minified code
  716. */
  717. protected function minify($code, $type)
  718. {
  719. $output = '';
  720. $buffer = '';
  721. $stack = array();
  722. $token_regex = '#/\*|\'|"';
  723. if ($type == 'js') {
  724. $token_regex .= '|//';
  725. $token_regex .= '|/';
  726. } elseif ($type == 'css') {
  727. $token_regex .= '|url\(';
  728. }
  729. $token_regex .= '#i';
  730. do {
  731. if (!preg_match($token_regex, $code, $match)) {
  732. $part = $code;
  733. $code = '';
  734. $token = NULL;
  735. } else {
  736. $token = $match[0];
  737. $pos = strpos($code, $token);
  738. if ($pos === FALSE) {
  739. break;
  740. }
  741. $part = substr($code, 0, $pos);
  742. $code = substr($code, $pos);
  743. }
  744. $regex = NULL;
  745. if ($token == '/') {
  746. if (!preg_match('#([(,=:[!&|?{};\n]|\breturn)\s*$#D', $part)) {
  747. $part .= $token;
  748. $code = substr($code, 1);
  749. } else {
  750. $regex = '#^/((\\\\.)+|[^\\\\/]+)*(/|$)#sD';
  751. }
  752. } elseif ($token == "url(") {
  753. $regex = '#^url\(((\\\\.)+|[^\\\\\\)]+)*(\)|$)#sD';
  754. } elseif ($token == "//") {
  755. $regex = '#^//.*(\n|$)#D';
  756. } elseif ($token == "/*") {
  757. $regex = '#^.{2}.*?(\*/|$)#sD';
  758. } elseif ($token == "'") {
  759. $regex = '#^\'((\\\\.)+|[^\\\\\']+)*(\'|$)#sD';
  760. } elseif ($token == '"') {
  761. $regex = '#^"((\\\\.)+|[^\\\\"]+)*("|$)#sD';
  762. }
  763. $this->minifyCode($part, $buffer, $stack, $type);
  764. $output .= $buffer;
  765. $buffer = $part;
  766. if ($regex) {
  767. preg_match($regex, $code, $match);
  768. $code = substr($code, strlen($match[0]));
  769. $this->minifyLiteral($match[0], $buffer, $type);
  770. $output .= $buffer;
  771. $buffer = $match[0];
  772. } elseif (!$token) {
  773. $output .= $buffer;
  774. }
  775. } while (strlen($code));
  776. return $output;
  777. }
  778. /**
  779. * Takes a block of CSS or JS and reduces the number of characters
  780. *
  781. * @param string &$part The part of code to minify
  782. * @param string &$buffer A buffer containing the last code or literal encountered
  783. * @param array $stack A stack used to keep track of the nesting level of CSS
  784. * @param mixed $type The type of code, `'css'` or `'js'`
  785. * @return void
  786. */
  787. protected function minifyCode(&$part, &$buffer, &$stack, $type='js')
  788. {
  789. // This pulls in the end of the last match for useful context
  790. $end_buffer = substr($buffer, -1);
  791. $lookbehind = in_array($end_buffer, array(' ', "\n")) ? substr($buffer, -2) : $end_buffer;
  792. $buffer = substr($buffer, 0, 0-strlen($lookbehind));
  793. $part = $lookbehind . $part;
  794. if ($type == 'js') {
  795. // All whitespace and control characters are collapsed
  796. $part = preg_replace('#[\x00-\x09\x0B\x0C\x0E-\x20]+#', ' ', $part);
  797. $part = preg_replace('#[\n\r]+#', "\n", $part);
  798. // Whitespace is removed where not needed
  799. $part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_])[ ]+#i', '', $part);
  800. $part = preg_replace('#[ ]+(?![a-z0-9\x80-\xFF\\\\$_])#i', '', $part);
  801. $part = preg_replace('#(?<![a-z0-9\x80-\xFF\\\\$_}\\])"\'+-])\n+#i', '', $part);
  802. $part = preg_replace('#\n+(?![a-z0-9\x80-\xFF\\\\$_{[(+-])#i', '', $part);
  803. } elseif ($type == 'css') {
  804. // All whitespace is collapsed
  805. $part = preg_replace('#\s+#', ' ', $part);
  806. // Whitespace is removed where not needed
  807. $part = preg_replace('#\s*([;{},>+])\s*#', '\1', $part);
  808. // This keeps track of the current scope since some minification
  809. // rules are different if inside or outside of a rule block
  810. $new_part = '';
  811. do {
  812. if (!preg_match('#@media|\{|\}#', $part, $match)) {
  813. $chunk = $part;
  814. $part = '';
  815. $token = NULL;
  816. } else {
  817. $token = $match[0];
  818. $pos = strpos($part, $token);
  819. if ($pos === FALSE) {
  820. break;
  821. }
  822. $chunk = substr($part, 0, $pos+strlen($token));
  823. $part = substr($part, $pos+strlen($token));
  824. }
  825. if (end($stack) == 'rule_block') {
  826. // Colons don't need space inside of a block
  827. $chunk = preg_replace('#\s*:\s*#', ':', $chunk);
  828. // Useless semi-colons are removed
  829. $chunk = str_replace(';}', '}', $chunk);
  830. // All zero units are reduces to just 0
  831. $chunk = preg_replace('#((?<!\d|\.|\#)0+(\.0+)?|(?<!\d)\.0+)(?=\D|$)((%|in|cm|mm|em|ex|pt|pc|px)(\b|$))?#iD', '0', $chunk);
  832. // All .0 decimals are removed
  833. $chunk = preg_replace('#(\d+)\.0+(?=\D)#iD', '\1', $chunk);
  834. // All leading zeros are removed
  835. $chunk = preg_replace('#(?<!\d)0+(\.\d+)(?=\D)#iD', '\1', $chunk);
  836. // All measurements that contain the same value 4 times are reduced to a single
  837. $chunk = preg_replace('#(?<!\d|\.)([\d\.]+(?:%|in|cm|mm|em|ex|pt|pc|px))(\s*\1){3}#i', '\1', $chunk);
  838. // Hex color codes are reduced if possible
  839. $chunk = preg_replace('@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3(?!\d)@iD', '#\1\2\3', $chunk);
  840. $chunk = str_ireplace('! important', '!important', $chunk);
  841. } else {
  842. // This handles an IE6 edge-case
  843. $chunk = preg_replace('#(:first-l(etter|ine))\{#', '\1 {', $chunk);
  844. }
  845. $new_part .= $chunk;
  846. if ($token == '@media') {
  847. $stack[] = 'media_rule';
  848. } elseif ($token == '{' && end($stack) == 'media_rule') {
  849. $stack = array('media_block');
  850. } elseif ($token == '{') {
  851. $stack[] = 'rule_block';
  852. } elseif ($token) {
  853. array_pop($stack);
  854. }
  855. } while (strlen($part));
  856. $part = $new_part;
  857. }
  858. }
  859. /**
  860. * Takes a literal and either discards or keeps it
  861. *
  862. * @param mixed &$part The literal to process
  863. * @param mixed &$buffer The last literal or code processed
  864. * @param string $type The language the literal is in, `'css'` or `'js'`
  865. * @return void
  866. */
  867. protected function minifyLiteral(&$part, &$buffer, $type)
  868. {
  869. // Comments are skipped unless they are special
  870. if (substr($part, 0, 2) == '/*' && substr($part, 0, 3) != '/*!') {
  871. $part = $buffer . ' ';
  872. $buffer = '';
  873. }
  874. if ($type == 'js' && substr($part, 0, 2) == '//') {
  875. $part = $buffer . "\n";
  876. $buffer = '';
  877. }
  878. }
  879. /**
  880. * Includes the element specified - element must be set through ::set() first
  881. *
  882. * If the element is a file path ending in `.css`, `.js`, `.rss` or `.xml`
  883. * an appropriate HTML tag will be printed (files ending in `.xml` will be
  884. * treated as an RSS feed). If the element is a file path ending in `.inc`,
  885. * `.php` or `.php5` it will be included.
  886. *
  887. * Paths that start with `./` will be loaded relative to the current script.
  888. * Paths that start with a file or directory name will be loaded relative
  889. * to the `$root` passed in the constructor. Paths that start with `/` will
  890. * be loaded from the root of the filesystem.
  891. *
  892. * You can pass the `media` attribute of a CSS file or the `title` attribute
  893. * of an RSS feed by adding an associative array with the following formats:
  894. *
  895. * {{{
  896. * array(
  897. * 'path' => (string) {css file path},
  898. * 'media' => (string) {media type}
  899. * );
  900. * array(
  901. * 'path' => (string) {rss file path},
  902. * 'title' => (string) {feed title}
  903. * );
  904. * }}}
  905. *
  906. * @param string $element The element to place
  907. * @param string $file_type Will force the element to be placed as this type of file instead of auto-detecting the file type. Valid types include: `'css'`, `'js'`, `'php'` and `'rss'`.
  908. * @return void
  909. */
  910. public function place($element='__main__', $file_type=NULL)
  911. {
  912. // Put in a buffered placeholder
  913. if ($this->buffered_id) {
  914. echo '%%fTemplating::' . $this->buffered_id . '::' . $element . '::' . $file_type . '%%';
  915. return;
  916. }
  917. if (!isset($this->elements[$element])) {
  918. return;
  919. }
  920. $this->placeElement($element, $file_type);
  921. }
  922. /**
  923. * Prints a CSS `link` HTML tag to the output
  924. *
  925. * @param mixed $info The path or array containing the `'path'` to the CSS file. Array can also contain a key `'media'`.
  926. * @return void
  927. */
  928. protected function placeCSS($info)
  929. {
  930. if (!is_array($info)) {
  931. $info = array('path' => $info);
  932. }
  933. if (!isset($info['media'])) {
  934. $info['media'] = 'all';
  935. }
  936. echo '<link rel="stylesheet" type="text/css" href="' . $info['path'] . '" media="' . $info['media'] . '" />' . "\n";
  937. }
  938. /**
  939. * Performs the action of actually placing an element
  940. *
  941. * @param string $element The element that is being placed
  942. * @param string $file_type The file type to treat all values as
  943. * @return void
  944. */
  945. protected function placeElement($element, $file_type)
  946. {
  947. $values = $this->elements[$element];
  948. if (!is_object($values)) {
  949. settype($values, 'array');
  950. } else {
  951. $values = array($values);
  952. }
  953. $values = array_values($values);
  954. $value_groups = array();
  955. $last_type = NULL;
  956. $last_location = NULL;
  957. $last_media = NULL;
  958. foreach ($values as $i => $value) {
  959. $type = $this->verifyValue($element, $value, $file_type);
  960. $media = is_array($value) && isset($value['media']) ? $value['media'] : NULL;
  961. $path = is_array($value) ? $value['path'] : $value;
  962. $location = is_string($path) && preg_match('#^https?://#', $path) ? 'http' : 'local';
  963. if ($type != $last_type || $location != $last_location || $media != $last_media) {
  964. $value_groups[] = array(
  965. 'type' => $type,
  966. 'location' => $location,
  967. 'values' => array()
  968. );
  969. }
  970. $value_groups[count($value_groups)-1]['values'][] = $value;
  971. $last_type = $type;
  972. $last_location = $location;
  973. $last_media = $media;
  974. }
  975. foreach ($value_groups as $value_group) {
  976. if ($value_group['location'] == 'local') {
  977. if ($this->minification_directory && in_array($value_group['type'], array('js', 'css'))) {
  978. $this->handleMinified($value_group['type'], $element, $value_group['values']);
  979. continue;
  980. }
  981. if ($this->short_tag_directory && $value_group['type'] == 'php') {
  982. $value_group['values'] = $this->fixShortTags($value_group['values']);
  983. }
  984. }
  985. foreach ($value_group['values'] as $value) {
  986. switch ($value_group['type']) {
  987. case 'css':
  988. $this->placeCSS($value);
  989. break;
  990. case 'fTemplating':
  991. // This causes children to inherit settings if they aren't already set
  992. if ($value->minification_directory === NULL) {
  993. $value->minification_directory = $this->minification_directory;
  994. $value->minification_mode = $this->minification_mode;
  995. $value->minification_prefix = $this->minification_prefix;
  996. }
  997. if ($value->short_tag_directory === NULL) {
  998. $value->short_tag_directory = $this->short_tag_directory;
  999. $value->short_tag_mode = $this->short_tag_mode;
  1000. }
  1001. $value->place();
  1002. break;
  1003. case 'js':
  1004. $this->placeJS($value);
  1005. break;
  1006. case 'php':
  1007. $this->placePHP($element, $value);
  1008. break;
  1009. case 'rss':
  1010. $this->placeRSS($value);
  1011. break;
  1012. default:
  1013. throw new fProgrammerException(
  1014. 'The file type specified, %1$s, is invalid. Must be one of: %2$s.',
  1015. $type,
  1016. 'css, js, php, rss'
  1017. );
  1018. }
  1019. }
  1020. }
  1021. }
  1022. /**
  1023. * Prints a java`script` HTML tag to the output
  1024. *
  1025. * @param mixed $info The path or array containing the `'path'` to the javascript file
  1026. * @return void
  1027. */
  1028. protected function placeJS($info)
  1029. {
  1030. if (!is_array($info)) {
  1031. $info = array('path' => $info);
  1032. }
  1033. echo '<script type="text/javascript" src="' . $info['path'] . '"></script>' . "\n";
  1034. }
  1035. /**
  1036. * Includes a PHP file
  1037. *
  1038. * @param string $element The element being placed
  1039. * @param string $path The path to the PHP file
  1040. * @return void
  1041. */
  1042. protected function placePHP($element, $path)
  1043. {
  1044. // Check to see if the element is a path relative to the template root
  1045. if (!preg_match('#^(/|\\\\|[a-z]:(\\\\|/)|\\\\|//|\./|\.\\\\)#i', $path)) {
  1046. $path = $this->root . $path;
  1047. }
  1048. if (!file_exists($path)) {
  1049. throw new fProgrammerException(
  1050. 'The path specified for %1$s, %2$s, does not exist on the filesystem',
  1051. $element,
  1052. $path
  1053. );
  1054. }
  1055. if (!is_readable($path)) {
  1056. throw new fEnvironmentException(
  1057. 'The path specified for %1$s, %2$s, is not readable',
  1058. $element,
  1059. $path
  1060. );
  1061. }
  1062. include($path);
  1063. }
  1064. /**
  1065. * Prints an RSS `link` HTML tag to the output
  1066. *
  1067. * @param mixed $info The path or array containing the `'path'` to the RSS xml file. May also contain a `'title'` key for the title of the RSS feed.
  1068. * @return void
  1069. */
  1070. protected function placeRSS($info)
  1071. {
  1072. if (!is_array($info)) {
  1073. $info = array(
  1074. 'path' => $info,
  1075. 'title' => fGrammar::humanize(
  1076. preg_replace('#.*?([^/]+).(rss|xml)$#iD', '\1', $info)
  1077. )
  1078. );
  1079. }
  1080. if (!isset($info['title'])) {
  1081. throw new fProgrammerException(
  1082. 'The RSS value %s is missing the title key',
  1083. $info
  1084. );
  1085. }
  1086. echo '<link rel="alternate" type="application/rss+xml" href="' . $info['path'] . '" title="' . $info['title'] . '" />' . "\n";
  1087. }
  1088. /**
  1089. * Performs buffered replacements using a breadth-first technique
  1090. *
  1091. * @return void
  1092. */
  1093. private function placeBuffered()
  1094. {
  1095. if (!$this->buffered_id) {
  1096. return;
  1097. }
  1098. $contents = fBuffer::get();
  1099. fBuffer::erase();
  1100. // We are gonna use a regex replacement that is eval()'ed as PHP code
  1101. $regex = '/%%fTemplating::' . $this->buffered_id . '::(.*?)::(.*?)%%/';
  1102. // Remove the buffered id, thus making any nested place() calls be executed immediately
  1103. $this->buffered_id = NULL;
  1104. echo preg_replace_callback($regex, array($this, 'placeBufferedCallback'), $contents);
  1105. }
  1106. /**
  1107. * Performs a captured place of an element to use with buffer placing
  1108. *
  1109. * @param array $match A regex match from ::placeBuffered()
  1110. * @return string The output of placing the element
  1111. */
  1112. private function placeBufferedCallback($match)
  1113. {
  1114. fBuffer::startCapture();
  1115. $this->placeElement($match[1], $match[2]);
  1116. return fBuffer::stopCapture();
  1117. }
  1118. /**
  1119. * Gets the value of an element and runs it through fHTML::prepare()
  1120. *
  1121. * @param string $element The element to get - array elements can be access via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1122. * @param mixed $default_value The value to return if the element has not been set
  1123. * @return mixed The value of the element specified run through fHTML::prepare(), or the default value if it has not been set
  1124. */
  1125. public function prepare($element, $default_value=NULL)
  1126. {
  1127. return fHTML::prepare($this->get($element, $default_value));
  1128. }
  1129. /**
  1130. * Removes and returns the value from the end of an array element
  1131. *
  1132. * @param string $element The element to remove from to - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1133. * @param boolean $beginning If the value should be removed from the beginning of the element
  1134. * @return mixed The value that was removed
  1135. */
  1136. public function remove($element, $beginning=FALSE)
  1137. {
  1138. $tip =& $this->elements;
  1139. if ($bracket_pos = strpos($element, '[')) {
  1140. $original_element = $element;
  1141. $array_dereference = substr($element, $bracket_pos);
  1142. $element = substr($element, 0, $bracket_pos);
  1143. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  1144. $array_keys = array_map('current', $array_keys);
  1145. array_unshift($array_keys, $element);
  1146. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  1147. if (!isset($tip[$array_key])) {
  1148. return NULL;
  1149. } elseif (!is_array($tip[$array_key])) {
  1150. throw new fProgrammerException(
  1151. '%1$s was called for an element, %2$s, which is not an array',
  1152. 'remove()',
  1153. $original_element
  1154. );
  1155. }
  1156. $tip =& $tip[$array_key];
  1157. }
  1158. $element = end($array_keys);
  1159. }
  1160. if (!isset($tip[$element])) {
  1161. return NULL;
  1162. } elseif (!is_array($tip[$element])) {
  1163. throw new fProgrammerException(
  1164. '%1$s was called for an element, %2$s, which is not an array',
  1165. 'remove()',
  1166. $element
  1167. );
  1168. }
  1169. if ($beginning) {
  1170. return array_shift($tip[$element]);
  1171. }
  1172. return array_pop($tip[$element]);
  1173. }
  1174. /**
  1175. * Sets the value for an element
  1176. *
  1177. * @param string $element The element to set - the magic element `__main__` is used for placing the current fTemplating object as a child of another fTemplating object - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in element names
  1178. * @param mixed $value The value for the element
  1179. * @param array :$elements An associative array with the key being the `$element` to set and the value being the `$value` for that element
  1180. * @return fTemplating The template object, to allow for method chaining
  1181. */
  1182. public function set($element, $value=NULL)
  1183. {
  1184. if ($value === NULL && is_array($element)) {
  1185. foreach ($element as $key => $value) {
  1186. $this->set($key, $value);
  1187. }
  1188. return $this;
  1189. }
  1190. $tip =& $this->elements;
  1191. if ($bracket_pos = strpos($element, '[')) {
  1192. $array_dereference = substr($element, $bracket_pos);
  1193. $element = substr($element, 0, $bracket_pos);
  1194. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  1195. $array_keys = array_map('current', $array_keys);
  1196. array_unshift($array_keys, $element);
  1197. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  1198. if (!isset($tip[$array_key]) || !is_array($tip[$array_key])) {
  1199. $tip[$array_key] = array();
  1200. }
  1201. $tip =& $tip[$array_key];
  1202. }
  1203. $element = end($array_keys);
  1204. }
  1205. $tip[$element] = $value;
  1206. return $this;
  1207. }
  1208. /**
  1209. * Ensures the value is valid
  1210. *
  1211. * @param string $element The element that is being placed
  1212. * @param mixed $value A value to be placed
  1213. * @param string $file_type The file type that this element will be displayed as - skips checking file extension
  1214. * @return string The file type of the value being placed
  1215. */
  1216. protected function verifyValue($element, $value, $file_type=NULL)
  1217. {
  1218. if (!$value && !is_numeric($value)) {
  1219. throw new fProgrammerException(
  1220. 'The element specified, %s, has a value that is empty',
  1221. $value
  1222. );
  1223. }
  1224. if (is_array($value) && !isset($value['path'])) {
  1225. throw new fProgrammerException(
  1226. 'The element specified, %1$s, has a value, %2$s, that is missing the path key',
  1227. $element,
  1228. $value
  1229. );
  1230. }
  1231. if ($file_type) {
  1232. return $file_type;
  1233. }
  1234. if ($value instanceof self) {
  1235. return 'fTemplating';
  1236. }
  1237. $path = (is_array($value)) ? $value['path'] : $value;
  1238. $path = preg_replace('#\?.*$#D', '', $path);
  1239. $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
  1240. // Allow some common variations on file extensions
  1241. $extension_map = array(
  1242. 'inc' => 'php',
  1243. 'php5' => 'php',
  1244. 'xml' => 'rss'
  1245. );
  1246. if (isset($extension_map[$extension])) {
  1247. $extension = $extension_map[$extension];
  1248. }
  1249. if (!in_array($extension, array('css', 'js', 'php', 'rss'))) {
  1250. throw new fProgrammerException(
  1251. 'The element specified, %1$s, has a value whose path, %2$s, does not end with a recognized file extension: %3$s.',
  1252. $element,
  1253. $path,
  1254. '.css, .inc, .js, .php, .php5, .rss, .xml'
  1255. );
  1256. }
  1257. return $extension;
  1258. }
  1259. }
  1260. /**
  1261. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  1262. *
  1263. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1264. * of this software and associated documentation files (the "Software"), to deal
  1265. * in the Software without restriction, including without limitation the rights
  1266. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1267. * copies of the Software, and to permit persons to whom the Software is
  1268. * furnished to do so, subject to the following conditions:
  1269. *
  1270. * The above copyright notice and this permission notice shall be included in
  1271. * all copies or substantial portions of the Software.
  1272. *
  1273. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1274. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1275. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1276. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1277. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1278. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1279. * THE SOFTWARE.
  1280. */