5 * Internal methods for the Helpers.
9 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10 * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
12 * Licensed under The MIT License
13 * Redistributions of files must retain the above copyright notice.
15 * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
16 * @link http://cakephp.org CakePHP(tm) Project
18 * @subpackage cake.cake.libs.view
19 * @since CakePHP(tm) v 0.2.9
20 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
26 App
::import('Core', 'Overloadable');
29 * Abstract base class for all other Helpers in CakePHP.
30 * Provides common methods and features.
33 * @subpackage cake.cake.libs.view
35 class Helper
extends Overloadable
{
38 * List of helpers used by this helper
59 * The current theme name if any.
66 * URL to current action.
77 var $params = array();
94 * POST data for models
101 * List of named arguments
105 var $namedArgs = null;
108 * URL argument separator character
112 var $argSeparator = null;
115 * Contains model validation errors of form post-backs
120 var $validationErrors = null;
123 * Holds tag templates.
131 * Holds the content to be cleaned.
136 var $__tainted = null;
139 * Holds the cleaned content.
144 var $__cleaned = null;
147 * Default overload methods
151 function get__($name) {}
152 function set__($name, $value) {}
153 function call__($method, $params) {
154 trigger_error(sprintf(__('Method %1$s::%2$s does not exist', true), get_class($this), $method), E_USER_WARNING
);
158 * Parses tag templates into $this->tags.
160 * @param $name file name inside app/config to load.
161 * @return array merged tags from config/$name.php
164 function loadConfig($name = 'tags') {
165 if (file_exists(CONFIGS
. $name .'.php')) {
166 require(CONFIGS
. $name .'.php');
168 $this->tags
= array_merge($this->tags
, $tags);
175 * Finds URL for specified action.
177 * Returns a URL pointing at the provided parameters.
179 * @param mixed $url Either a relative string url like `/products/view/23` or
180 * an array of url parameters. Using an array for urls will allow you to leverage
181 * the reverse routing features of CakePHP.
182 * @param boolean $full If true, the full base URL will be prepended to the result
183 * @return string Full translated URL with base path.
185 * @link http://book.cakephp.org/view/1448/url
187 function url($url = null, $full = false) {
188 return h(Router
::url($url, $full));
192 * Checks if a file exists when theme is used, if no file is found default location is returned
194 * @param string $file The file to create a webroot path to.
195 * @return string Web accessible path to file.
198 function webroot($file) {
199 $asset = explode('?', $file);
200 $asset[1] = isset($asset[1]) ?
'?' . $asset[1] : null;
201 $webPath = "{$this->webroot}" . $asset[0];
204 if (!empty($this->theme
)) {
205 $file = trim($file, '/');
206 $theme = $this->theme
. '/';
209 $file = str_replace('/', '\\', $file);
212 if (file_exists(Configure
::read('App.www_root') . 'theme' . DS
. $this->theme
. DS
. $file)) {
213 $webPath = "{$this->webroot}theme/" . $theme . $asset[0];
215 $viewPaths = App
::path('views');
217 foreach ($viewPaths as $viewPath) {
218 $path = $viewPath . 'themed'. DS
. $this->theme
. DS
. 'webroot' . DS
. $file;
220 if (file_exists($path)) {
221 $webPath = "{$this->webroot}theme/" . $theme . $asset[0];
227 if (strpos($webPath, '//') !== false) {
228 return str_replace('//', '/', $webPath . $asset[1]);
230 return $webPath . $asset[1];
234 * Adds a timestamp to a file based resource based on the value of `Asset.timestamp` in
235 * Configure. If Asset.timestamp is true and debug > 0, or Asset.timestamp == 'force'
236 * a timestamp will be added.
238 * @param string $path The file path to timestamp, the path must be inside WWW_ROOT
239 * @return string Path with a timestamp added, or not.
242 function assetTimestamp($path) {
243 $timestampEnabled = (
244 (Configure
::read('Asset.timestamp') === true && Configure
::read() > 0) ||
245 Configure
::read('Asset.timestamp') === 'force'
247 if (strpos($path, '?') === false && $timestampEnabled) {
248 $filepath = preg_replace('/^' . preg_quote($this->webroot
, '/') . '/', '', $path);
249 $webrootPath = WWW_ROOT
. str_replace('/', DS
, $filepath);
250 if (file_exists($webrootPath)) {
251 return $path . '?' . @filemtime
($webrootPath);
253 $segments = explode('/', ltrim($filepath, '/'));
254 if ($segments[0] === 'theme') {
255 $theme = $segments[1];
256 unset($segments[0], $segments[1]);
257 $themePath = App
::themePath($theme) . 'webroot' . DS
. implode(DS
, $segments);
258 return $path . '?' . @filemtime
($themePath);
260 $plugin = $segments[0];
262 $pluginPath = App
::pluginPath($plugin) . 'webroot' . DS
. implode(DS
, $segments);
263 return $path . '?' . @filemtime
($pluginPath);
270 * Used to remove harmful tags from content. Removes a number of well known XSS attacks
271 * from content. However, is not guaranteed to remove all possiblities. Escaping
272 * content is the best way to prevent all possible attacks.
274 * @param mixed $output Either an array of strings to clean or a single string to clean.
275 * @return cleaned content for output
278 function clean($output) {
280 if (empty($output)) {
283 if (is_array($output)) {
284 foreach ($output as $key => $value) {
285 $return[$key] = $this->clean($value);
289 $this->__tainted
= $output;
291 return $this->__cleaned
;
295 * Returns a space-delimited string with items of the $options array. If a
296 * key of $options array happens to be one of:
312 * And its value is one of:
319 * Then the value will be reset to be identical with key's name.
320 * If the value is not one of these 3, the parameter is not output.
322 * 'escape' is a special option in that it controls the conversion of
323 * attributes to their html-entity encoded equivalents. Set to false to disable html-encoding.
325 * If value for any option key is set to `null` or `false`, that option will be excluded from output.
327 * @param array $options Array of options.
328 * @param array $exclude Array of options to be excluded, the options here will not be part of the return.
329 * @param string $insertBefore String to be inserted before options.
330 * @param string $insertAfter String to be inserted after options.
331 * @return string Composed attributes.
334 function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null) {
335 if (is_array($options)) {
336 $options = array_merge(array('escape' => true), $options);
338 if (!is_array($exclude)) {
341 $keys = array_diff(array_keys($options), array_merge($exclude, array('escape')));
342 $values = array_intersect_key(array_values($options), $keys);
343 $escape = $options['escape'];
344 $attributes = array();
346 foreach ($keys as $index => $key) {
347 if ($values[$index] !== false && $values[$index] !== null) {
348 $attributes[] = $this->__formatAttribute($key, $values[$index], $escape);
351 $out = implode(' ', $attributes);
355 return $out ?
$insertBefore . $out . $insertAfter : '';
359 * Formats an individual attribute, and returns the string value of the composed attribute.
360 * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked'
362 * @param string $key The name of the attribute to create
363 * @param string $value The value of the attribute to create.
364 * @return string The composed attribute.
367 function __formatAttribute($key, $value, $escape = true) {
369 $attributeFormat = '%s="%s"';
370 $minimizedAttributes = array('compact', 'checked', 'declare', 'readonly', 'disabled',
371 'selected', 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize');
372 if (is_array($value)) {
376 if (in_array($key, $minimizedAttributes)) {
377 if ($value === 1 ||
$value === true ||
$value === 'true' ||
$value === '1' ||
$value == $key) {
378 $attribute = sprintf($attributeFormat, $key, $key);
381 $attribute = sprintf($attributeFormat, $key, ($escape ?
h($value) : $value));
387 * Sets this helper's model and field properties to the dot-separated value-pair in $entity.
389 * @param mixed $entity A field name, like "ModelName.fieldName" or "ModelName.ID.fieldName"
390 * @param boolean $setScope Sets the view scope to the model specified in $tagValue
394 function setEntity($entity, $setScope = false) {
395 $view =& ClassRegistry
::getObject('view');
398 $view->modelScope
= false;
399 } elseif (!empty($view->entityPath
) && $view->entityPath
== $entity) {
403 if ($entity === null) {
405 $view->association
= null;
406 $view->modelId
= null;
407 $view->modelScope
= false;
408 $view->entityPath
= null;
412 $view->entityPath
= $entity;
413 $model = $view->model
;
414 $sameScope = $hasField = false;
415 $parts = array_values(Set
::filter(explode('.', $entity), true));
421 $count = count($parts);
425 if (is_numeric($parts[0])) {
428 $reverse = array_reverse($parts);
429 $field = array_shift($reverse);
430 while(!empty($reverse)) {
431 $subject = array_shift($reverse);
432 if (is_numeric($subject)) {
435 if (ClassRegistry
::isKeySet($subject)) {
442 if (ClassRegistry
::isKeySet($model)) {
443 $ModelObj =& ClassRegistry
::getObject($model);
444 for ($i = 0; $i < $count; $i++
) {
446 is_a($ModelObj, 'Model') &&
447 ($ModelObj->hasField($parts[$i]) ||
448 array_key_exists($parts[$i], $ModelObj->validate
))
451 if ($hasField === 0 ||
($hasField === 1 && is_numeric($parts[0]))) {
458 if ($sameScope === true && in_array($parts[0], array_keys($ModelObj->hasAndBelongsToMany
))) {
463 if (!$view->association
&& $parts[0] == $view->field
&& $view->field
!= $view->model
) {
464 array_unshift($parts, $model);
467 $view->field
= $view->modelId
= $view->fieldSuffix
= $view->association
= null;
469 switch (count($parts)) {
471 if ($view->modelScope
=== false) {
472 $view->model
= $parts[0];
474 $view->field
= $parts[0];
475 if ($sameScope === false) {
476 $view->association
= $parts[0];
481 if ($view->modelScope
=== false) {
482 list($view->model
, $view->field
) = $parts;
483 } elseif ($sameScope === true && $hasField === 0) {
484 list($view->field
, $view->fieldSuffix
) = $parts;
485 } elseif ($sameScope === true && $hasField === 1) {
486 list($view->modelId
, $view->field
) = $parts;
488 list($view->association
, $view->field
) = $parts;
492 if ($sameScope === true && $hasField === 1) {
493 list($view->modelId
, $view->field
, $view->fieldSuffix
) = $parts;
494 } elseif ($hasField === 2) {
495 list($view->association
, $view->modelId
, $view->field
) = $parts;
497 list($view->association
, $view->field
, $view->fieldSuffix
) = $parts;
501 if ($parts[0] === $view->model
) {
502 list($view->model
, $view->modelId
, $view->field
, $view->fieldSuffix
) = $parts;
504 list($view->association
, $view->modelId
, $view->field
, $view->fieldSuffix
) = $parts;
508 $reverse = array_reverse($parts);
511 $view->field
= $field;
512 if (!is_numeric($reverse[1]) && $reverse[1] != $model) {
513 $view->field
= $reverse[1];
514 $view->fieldSuffix
= $field;
517 if (is_numeric($parts[0])) {
518 $view->modelId
= $parts[0];
519 } elseif ($view->model
== $parts[0] && is_numeric($parts[1])) {
520 $view->modelId
= $parts[1];
522 $view->association
= $model;
526 if (!isset($view->model
) ||
empty($view->model
)) {
527 $view->model
= $view->association
;
528 $view->association
= null;
529 } elseif ($view->model
=== $view->association
) {
530 $view->association
= null;
534 $view->modelScope
= true;
539 * Gets the currently-used model of the rendering context.
545 $view =& ClassRegistry
::getObject('view');
546 if (!empty($view->association
)) {
547 return $view->association
;
554 * Gets the ID of the currently-used model of the rendering context.
560 $view =& ClassRegistry
::getObject('view');
561 return $view->modelId
;
565 * Gets the currently-used model field of the rendering context.
571 $view =& ClassRegistry
::getObject('view');
576 * Returns false if given FORM field has no errors. Otherwise it returns the constant set in
577 * the array Model->validationErrors.
579 * @param string $model Model name as a string
580 * @param string $field Fieldname as a string
581 * @param integer $modelID Unique index identifying this record within the form
582 * @return boolean True on errors.
584 function tagIsInvalid($model = null, $field = null, $modelID = null) {
585 $view =& ClassRegistry
::getObject('view');
586 $errors = $this->validationErrors
;
587 $entity = $view->entity();
588 if (!empty($entity)) {
589 return Set
::extract($errors, join('.', $entity));
594 * Generates a DOM ID for the selected element, if one is not set.
595 * Uses the current View::entity() settings to generate a CamelCased id attribute.
597 * @param mixed $options Either an array of html attributes to add $id into, or a string
598 * with a view entity path to get a domId for.
599 * @param string $id The name of the 'id' attribute.
600 * @return mixed If $options was an array, an array will be returned with $id set. If a string
601 * was supplied, a string will be returned.
602 * @todo Refactor this method to not have as many input/output options.
604 function domId($options = null, $id = 'id') {
605 $view =& ClassRegistry
::getObject('view');
607 if (is_array($options) && array_key_exists($id, $options) && $options[$id] === null) {
608 unset($options[$id]);
610 } elseif (!is_array($options) && $options !== null) {
611 $this->setEntity($options);
612 return $this->domId();
615 $entity = $view->entity();
616 $model = array_shift($entity);
617 $dom = $model . join('', array_map(array('Inflector', 'camelize'), $entity));
619 if (is_array($options) && !array_key_exists($id, $options)) {
620 $options[$id] = $dom;
621 } elseif ($options === null) {
628 * Gets the input field name for the current tag. Creates input name attributes
629 * using CakePHP's data[Model][field] formatting.
631 * @param mixed $options If an array, should be an array of attributes that $key needs to be added to.
632 * If a string or null, will be used as the View entity.
633 * @param string $field
634 * @param string $key The name of the attribute to be set, defaults to 'name'
635 * @return mixed If an array was given for $options, an array with $key set will be returned.
636 * If a string was supplied a string will be returned.
638 * @todo Refactor this method to not have as many input/output options.
640 function _name($options = array(), $field = null, $key = 'name') {
641 $view =& ClassRegistry
::getObject('view');
642 if ($options === null) {
644 } elseif (is_string($options)) {
649 if (!empty($field)) {
650 $this->setEntity($field);
653 if (is_array($options) && array_key_exists($key, $options)) {
662 $name = 'data[' . implode('][', $view->entity()) . ']';
666 if (is_array($options)) {
667 $options[$key] = $name;
675 * Gets the data for the current tag
677 * @param mixed $options If an array, should be an array of attributes that $key needs to be added to.
678 * If a string or null, will be used as the View entity.
679 * @param string $field
680 * @param string $key The name of the attribute to be set, defaults to 'value'
681 * @return mixed If an array was given for $options, an array with $key set will be returned.
682 * If a string was supplied a string will be returned.
684 * @todo Refactor this method to not have as many input/output options.
686 function value($options = array(), $field = null, $key = 'value') {
687 if ($options === null) {
689 } elseif (is_string($options)) {
694 if (is_array($options) && isset($options[$key])) {
698 if (!empty($field)) {
699 $this->setEntity($field);
702 $view =& ClassRegistry
::getObject('view');
705 $entity = $view->entity();
706 if (!empty($this->data
) && !empty($entity)) {
707 $result = Set
::extract($this->data
, join('.', $entity));
710 $habtmKey = $this->field();
711 if (empty($result) && isset($this->data
[$habtmKey][$habtmKey]) && is_array($this->data
[$habtmKey])) {
712 $result = $this->data
[$habtmKey][$habtmKey];
713 } elseif (empty($result) && isset($this->data
[$habtmKey]) && is_array($this->data
[$habtmKey])) {
714 if (ClassRegistry
::isKeySet($habtmKey)) {
715 $model =& ClassRegistry
::getObject($habtmKey);
716 $result = $this->__selectedArray($this->data
[$habtmKey], $model->primaryKey
);
720 if (is_array($result)) {
721 if (array_key_exists($view->fieldSuffix
, $result)) {
722 $result = $result[$view->fieldSuffix
];
726 if (is_array($options)) {
727 if ($result === null && isset($options['default'])) {
728 $result = $options['default'];
730 unset($options['default']);
733 if (is_array($options)) {
734 $options[$key] = $result;
742 * Sets the defaults for an input tag. Will set the
743 * name, value, and id attributes for an array of html attributes. Will also
744 * add a 'form-error' class if the field contains validation errors.
746 * @param string $field The field name to initialize.
747 * @param array $options Array of options to use while initializing an input field.
748 * @return array Array options for the form input.
751 function _initInputField($field, $options = array()) {
752 if ($field !== null) {
753 $this->setEntity($field);
755 $options = (array)$options;
756 $options = $this->_name($options);
757 $options = $this->value($options);
758 $options = $this->domId($options);
759 if ($this->tagIsInvalid()) {
760 $options = $this->addClass($options, 'form-error');
766 * Adds the given class to the element options
768 * @param array $options Array options/attributes to add a class to
769 * @param string $class The classname being added.
770 * @param string $key the key to use for class.
771 * @return array Array of options with $key set.
774 function addClass($options = array(), $class = null, $key = 'class') {
775 if (isset($options[$key]) && trim($options[$key]) != '') {
776 $options[$key] .= ' ' . $class;
778 $options[$key] = $class;
784 * Returns a string generated by a helper method
786 * This method can be overridden in subclasses to do generalized output post-processing
788 * @param string $str String to be output.
790 * @deprecated This method will be removed in future versions.
792 function output($str) {
797 * Before render callback. beforeRender is called before the view file is rendered.
799 * Overridden in subclasses.
804 function beforeRender() {
808 * After render callback. afterRender is called after the view file is rendered
809 * but before the layout has been rendered.
811 * Overridden in subclasses.
816 function afterRender() {
820 * Before layout callback. beforeLayout is called before the layout is rendered.
822 * Overridden in subclasses.
827 function beforeLayout() {
831 * After layout callback. afterLayout is called after the layout has rendered.
833 * Overridden in subclasses.
838 function afterLayout() {
842 * Transforms a recordset from a hasAndBelongsToMany association to a list of selected
843 * options for a multiple select element
850 function __selectedArray($data, $key = 'id') {
851 if (!is_array($data)) {
853 if (!empty($this->data
[$model][$model])) {
854 return $this->data
[$model][$model];
856 if (!empty($this->data
[$model])) {
857 $data = $this->data
[$model];
862 foreach ($data as $var) {
863 $array[$var[$key]] = $var[$key];
870 * Resets the vars used by Helper::clean() to null
876 $this->__tainted
= null;
877 $this->__cleaned
= null;
881 * Removes harmful content from output
887 if (get_magic_quotes_gpc()) {
888 $this->__cleaned
= stripslashes($this->__tainted
);
890 $this->__cleaned
= $this->__tainted
;
893 $this->__cleaned
= str_replace(array("&", "<", ">"), array("&amp;", "&lt;", "&gt;"), $this->__cleaned
);
894 $this->__cleaned
= preg_replace('#(&\#*\w+)[\x00-\x20]+;#u', "$1;", $this->__cleaned
);
895 $this->__cleaned
= preg_replace('#(&\#x*)([0-9A-F]+);*#iu', "$1$2;", $this->__cleaned
);
896 $this->__cleaned
= html_entity_decode($this->__cleaned
, ENT_COMPAT
, "UTF-8");
897 $this->__cleaned
= preg_replace('#(<[^>]+[\x00-\x20\"\'\/])(on|xmlns)[^>]*>#iUu', "$1>", $this->__cleaned
);
898 $this->__cleaned
= preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*)[\\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2nojavascript...', $this->__cleaned
);
899 $this->__cleaned
= preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iUu', '$1=$2novbscript...', $this->__cleaned
);
900 $this->__cleaned
= preg_replace('#([a-z]*)[\x00-\x20]*=*([\'\"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#iUu','$1=$2nomozbinding...', $this->__cleaned
);
901 $this->__cleaned
= preg_replace('#([a-z]*)[\x00-\x20]*=([\'\"]*)[\x00-\x20]*data[\x00-\x20]*:#Uu', '$1=$2nodata...', $this->__cleaned
);
902 $this->__cleaned
= preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*expression[\x00-\x20]*\([^>]*>#iU', "$1>", $this->__cleaned
);
903 $this->__cleaned
= preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*behaviour[\x00-\x20]*\([^>]*>#iU', "$1>", $this->__cleaned
);
904 $this->__cleaned
= preg_replace('#(<[^>]+)style[\x00-\x20]*=[\x00-\x20]*([\`\'\"]*).*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*>#iUu', "$1>", $this->__cleaned
);
905 $this->__cleaned
= preg_replace('#</*\w+:\w[^>]*>#i', "", $this->__cleaned
);
907 $oldstring = $this->__cleaned
;
908 $this->__cleaned
= preg_replace('#</*(applet|meta|xml|blink|link|style|script|embed|object|iframe|frame|frameset|ilayer|layer|bgsound|title|base)[^>]*>#i', "", $this->__cleaned
);
909 } while ($oldstring != $this->__cleaned
);
910 $this->__cleaned
= str_replace(array("&", "<", ">"), array("&amp;", "&lt;", "&gt;"), $this->__cleaned
);