1: <?php
2: /**
3: * Methods for displaying presentation data in the view.
4: *
5: * PHP 5
6: *
7: * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
8: * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
9: *
10: * Licensed under The MIT License
11: * Redistributions of files must retain the above copyright notice.
12: *
13: * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
14: * @link http://cakephp.org CakePHP(tm) Project
15: * @package Cake.View
16: * @since CakePHP(tm) v 0.10.0.1076
17: * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
18: */
19:
20: App::uses('HelperCollection', 'View');
21: App::uses('AppHelper', 'View/Helper');
22: App::uses('Router', 'Routing');
23:
24: /**
25: * View, the V in the MVC triad. View interacts with Helpers and view variables passed
26: * in from the controller to render the results of the controller action. Often this is HTML,
27: * but can also take the form of JSON, XML, PDF's or streaming files.
28: *
29: * CakePHP uses a two-step-view pattern. This means that the view content is rendered first,
30: * and then inserted into the selected layout. A special `$content_for_layout` variable is available
31: * in the layout, and it contains the rendered view. This also means you can pass data from the view to the
32: * layout using `$this->set()`
33: *
34: * @package Cake.View
35: * @property CacheHelper $Cache
36: * @property FormHelper $Form
37: * @property HtmlHelper $Html
38: * @property JsHelper $Js
39: * @property NumberHelper $Number
40: * @property PaginatorHelper $Paginator
41: * @property RssHelper $Rss
42: * @property SessionHelper $Session
43: * @property TextHelper $Text
44: * @property TimeHelper $Time
45: */
46: class View extends Object {
47:
48: /**
49: * Helpers collection
50: *
51: * @var HelperCollection
52: */
53: public $Helpers;
54:
55: /**
56: * Name of the plugin.
57: *
58: * @link http://manual.cakephp.org/chapter/plugins
59: * @var string
60: */
61: public $plugin = null;
62:
63: /**
64: * Name of the controller.
65: *
66: * @var string Name of controller
67: */
68: public $name = null;
69:
70: /**
71: * Current passed params
72: *
73: * @var mixed
74: */
75: public $passedArgs = array();
76:
77: /**
78: * An array of names of built-in helpers to include.
79: *
80: * @var mixed A single name as a string or a list of names as an array.
81: */
82: public $helpers = array('Html');
83:
84: /**
85: * Path to View.
86: *
87: * @var string Path to View
88: */
89: public $viewPath = null;
90:
91: /**
92: * Variables for the view
93: *
94: * @var array
95: */
96: public $viewVars = array();
97:
98: /**
99: * Name of view to use with this View.
100: *
101: * @var string
102: */
103: public $view = null;
104:
105: /**
106: * Name of layout to use with this View.
107: *
108: * @var string
109: */
110: public $layout = 'default';
111:
112: /**
113: * Path to Layout.
114: *
115: * @var string Path to Layout
116: */
117: public $layoutPath = null;
118:
119: /**
120: * Turns on or off Cake's conventional mode of applying layout files. On by default.
121: * Setting to off means that layouts will not be automatically applied to rendered views.
122: *
123: * @var boolean
124: */
125: public $autoLayout = true;
126:
127: /**
128: * File extension. Defaults to Cake's template ".ctp".
129: *
130: * @var string
131: */
132: public $ext = '.ctp';
133:
134: /**
135: * Sub-directory for this view file. This is often used for extension based routing.
136: * Eg. With an `xml` extension, $subDir would be `xml/`
137: *
138: * @var string
139: */
140: public $subDir = null;
141:
142: /**
143: * Theme name. If you are using themes, you should remember to use ThemeView as well.
144: *
145: * @var string
146: */
147: public $theme = null;
148:
149: /**
150: * Used to define methods a controller that will be cached.
151: *
152: * @see Controller::$cacheAction
153: * @var mixed
154: */
155: public $cacheAction = false;
156:
157: /**
158: * Holds current errors for the model validation.
159: *
160: * @var array
161: */
162: public $validationErrors = array();
163:
164: /**
165: * True when the view has been rendered.
166: *
167: * @var boolean
168: */
169: public $hasRendered = false;
170:
171: /**
172: * List of generated DOM UUIDs.
173: *
174: * @var array
175: */
176: public $uuids = array();
177:
178: /**
179: * Holds View output.
180: *
181: * @var string
182: */
183: public $output = false;
184:
185: /**
186: * An instance of a CakeRequest object that contains information about the current request.
187: * This object contains all the information about a request and several methods for reading
188: * additional information about the request.
189: *
190: * @var CakeRequest
191: */
192: public $request;
193:
194: /**
195: * The Cache configuration View will use to store cached elements. Changing this will change
196: * the default configuration elements are stored under. You can also choose a cache config
197: * per element.
198: *
199: * @var string
200: * @see View::element()
201: */
202: public $elementCache = 'default';
203:
204: /**
205: * List of variables to collect from the associated controller.
206: *
207: * @var array
208: */
209: protected $_passedVars = array(
210: 'viewVars', 'autoLayout', 'ext', 'helpers', 'view', 'layout', 'name',
211: 'layoutPath', 'viewPath', 'request', 'plugin', 'passedArgs', 'cacheAction'
212: );
213:
214: /**
215: * Scripts (and/or other <head /> tags) for the layout.
216: *
217: * @var array
218: */
219: protected $_scripts = array();
220:
221: /**
222: * Holds an array of paths.
223: *
224: * @var array
225: */
226: protected $_paths = array();
227:
228: /**
229: * Indicate that helpers have been loaded.
230: *
231: * @var boolean
232: */
233: protected $_helpersLoaded = false;
234:
235: /**
236: * Constructor
237: *
238: * @param Controller $controller A controller object to pull View::_passedVars from.
239: */
240: public function __construct($controller) {
241: if (is_object($controller)) {
242: $count = count($this->_passedVars);
243: for ($j = 0; $j < $count; $j++) {
244: $var = $this->_passedVars[$j];
245: $this->{$var} = $controller->{$var};
246: }
247: }
248: $this->Helpers = new HelperCollection($this);
249: parent::__construct();
250: }
251:
252: /**
253: * Renders a piece of PHP with provided parameters and returns HTML, XML, or any other string.
254: *
255: * This realizes the concept of Elements, (or "partial layouts") and the $params array is used to send
256: * data to be used in the element. Elements can be cached improving performance by using the `cache` option.
257: *
258: * @param string $name Name of template file in the/app/View/Elements/ folder
259: * @param array $data Array of data to be made available to the rendered view (i.e. the Element)
260: * @param array $options Array of options. Possible keys are:
261: * - `cache` - Can either be `true`, to enable caching using the config in View::$elementCache. Or an array
262: * If an array, the following keys can be used:
263: * - `config` - Used to store the cached element in a custom cache configuration.
264: * - `key` - Used to define the key used in the Cache::write(). It will be prefixed with `element_`
265: * - `plugin` - Load an element from a specific plugin.
266: * - `callbacks` - Set to true to fire beforeRender and afterRender helper callbacks for this element.
267: * Defaults to false.
268: * @return string Rendered Element
269: */
270: public function element($name, $data = array(), $options = array()) {
271: $file = $plugin = $key = null;
272: $callbacks = false;
273:
274: if (isset($options['plugin'])) {
275: $plugin = Inflector::camelize($options['plugin']);
276: }
277: if (isset($this->plugin) && !$plugin) {
278: $plugin = $this->plugin;
279: }
280: if (isset($options['callbacks'])) {
281: $callbacks = $options['callbacks'];
282: }
283:
284: if (isset($options['cache'])) {
285: $underscored = null;
286: if ($plugin) {
287: $underscored = Inflector::underscore($plugin);
288: }
289: $keys = array_merge(array($underscored, $name), array_keys($options), array_keys($data));
290: $caching = array(
291: 'config' => $this->elementCache,
292: 'key' => implode('_', $keys)
293: );
294: if (is_array($options['cache'])) {
295: $defaults = array(
296: 'config' => $this->elementCache,
297: 'key' => $caching['key']
298: );
299: $caching = array_merge($defaults, $options['cache']);
300: }
301: $key = 'element_' . $caching['key'];
302: $contents = Cache::read($key, $caching['config']);
303: if ($contents !== false) {
304: return $contents;
305: }
306: }
307:
308: $file = $this->_getElementFilename($name, $plugin);
309:
310: if ($file) {
311: if (!$this->_helpersLoaded) {
312: $this->loadHelpers();
313: }
314: if ($callbacks) {
315: $this->Helpers->trigger('beforeRender', array($file));
316: }
317: $element = $this->_render($file, array_merge($this->viewVars, $data));
318: if ($callbacks) {
319: $this->Helpers->trigger('afterRender', array($file, $element));
320: }
321: if (isset($options['cache'])) {
322: Cache::write($key, $element, $caching['config']);
323: }
324: return $element;
325: }
326: $file = 'Elements' . DS . $name . $this->ext;
327:
328: if (Configure::read('debug') > 0) {
329: return "Element Not Found: " . $file;
330: }
331: }
332:
333: /**
334: * Renders view for given view file and layout.
335: *
336: * Render triggers helper callbacks, which are fired before and after the view are rendered,
337: * as well as before and after the layout. The helper callbacks are called:
338: *
339: * - `beforeRender`
340: * - `afterRender`
341: * - `beforeLayout`
342: * - `afterLayout`
343: *
344: * If View::$autoRender is false and no `$layout` is provided, the view will be returned bare.
345: *
346: * @param string $view Name of view file to use
347: * @param string $layout Layout to use.
348: * @return string Rendered Element
349: * @throws CakeException if there is an error in the view.
350: */
351: public function render($view = null, $layout = null) {
352: if ($this->hasRendered) {
353: return true;
354: }
355: if (!$this->_helpersLoaded) {
356: $this->loadHelpers();
357: }
358: $this->output = null;
359:
360: if ($view !== false && $viewFileName = $this->_getViewFileName($view)) {
361: $this->Helpers->trigger('beforeRender', array($viewFileName));
362: $this->output = $this->_render($viewFileName);
363: $this->Helpers->trigger('afterRender', array($viewFileName));
364: }
365:
366: if ($layout === null) {
367: $layout = $this->layout;
368: }
369: if ($this->output === false) {
370: throw new CakeException(__d('cake_dev', "Error in view %s, got no content.", $viewFileName));
371: }
372: if ($layout && $this->autoLayout) {
373: $this->output = $this->renderLayout($this->output, $layout);
374: }
375: $this->hasRendered = true;
376: return $this->output;
377: }
378:
379: /**
380: * Renders a layout. Returns output from _render(). Returns false on error.
381: * Several variables are created for use in layout.
382: *
383: * - `title_for_layout` - A backwards compatible place holder, you should set this value if you want more control.
384: * - `content_for_layout` - contains rendered view file
385: * - `scripts_for_layout` - contains scripts added to header
386: *
387: * @param string $content_for_layout Content to render in a view, wrapped by the surrounding layout.
388: * @param string $layout Layout name
389: * @return mixed Rendered output, or false on error
390: * @throws CakeException if there is an error in the view.
391: */
392: public function renderLayout($content_for_layout, $layout = null) {
393: $layoutFileName = $this->_getLayoutFileName($layout);
394: if (empty($layoutFileName)) {
395: return $this->output;
396: }
397: if (!$this->_helpersLoaded) {
398: $this->loadHelpers();
399: }
400: $this->Helpers->trigger('beforeLayout', array($layoutFileName));
401:
402: $this->viewVars = array_merge($this->viewVars, array(
403: 'content_for_layout' => $content_for_layout,
404: 'scripts_for_layout' => implode("\n\t", $this->_scripts),
405: ));
406:
407: if (!isset($this->viewVars['title_for_layout'])) {
408: $this->viewVars['title_for_layout'] = Inflector::humanize($this->viewPath);
409: }
410:
411: $this->output = $this->_render($layoutFileName);
412:
413: if ($this->output === false) {
414: throw new CakeException(__d('cake_dev', "Error in layout %s, got no content.", $layoutFileName));
415: }
416:
417: $this->Helpers->trigger('afterLayout', array($layoutFileName));
418: return $this->output;
419: }
420:
421: /**
422: * Render cached view. Works in concert with CacheHelper and Dispatcher to
423: * render cached view files.
424: *
425: * @param string $filename the cache file to include
426: * @param string $timeStart the page render start time
427: * @return boolean Success of rendering the cached file.
428: */
429: public function renderCache($filename, $timeStart) {
430: ob_start();
431: include ($filename);
432:
433: if (Configure::read('debug') > 0 && $this->layout != 'xml') {
434: echo "<!-- Cached Render Time: " . round(microtime(true) - $timeStart, 4) . "s -->";
435: }
436: $out = ob_get_clean();
437:
438: if (preg_match('/^<!--cachetime:(\\d+)-->/', $out, $match)) {
439: if (time() >= $match['1']) {
440: @unlink($filename);
441: unset ($out);
442: return false;
443: } else {
444: if ($this->layout === 'xml') {
445: header('Content-type: text/xml');
446: }
447: $commentLength = strlen('<!--cachetime:' . $match['1'] . '-->');
448: echo substr($out, $commentLength);
449: return true;
450: }
451: }
452: }
453:
454: /**
455: * Returns a list of variables available in the current View context
456: *
457: * @return array Array of the set view variable names.
458: */
459: public function getVars() {
460: return array_keys($this->viewVars);
461: }
462:
463: /**
464: * Returns the contents of the given View variable(s)
465: *
466: * @param string $var The view var you want the contents of.
467: * @return mixed The content of the named var if its set, otherwise null.
468: */
469: public function getVar($var) {
470: if (!isset($this->viewVars[$var])) {
471: return null;
472: } else {
473: return $this->viewVars[$var];
474: }
475: }
476:
477: /**
478: * Adds a script block or other element to be inserted in $scripts_for_layout in
479: * the `<head />` of a document layout
480: *
481: * @param string $name Either the key name for the script, or the script content. Name can be used to
482: * update/replace a script element.
483: * @param string $content The content of the script being added, optional.
484: * @return void
485: */
486: public function addScript($name, $content = null) {
487: if (empty($content)) {
488: if (!in_array($name, array_values($this->_scripts))) {
489: $this->_scripts[] = $name;
490: }
491: } else {
492: $this->_scripts[$name] = $content;
493: }
494: }
495:
496: /**
497: * Generates a unique, non-random DOM ID for an object, based on the object type and the target URL.
498: *
499: * @param string $object Type of object, i.e. 'form' or 'link'
500: * @param string $url The object's target URL
501: * @return string
502: */
503: public function uuid($object, $url) {
504: $c = 1;
505: $url = Router::url($url);
506: $hash = $object . substr(md5($object . $url), 0, 10);
507: while (in_array($hash, $this->uuids)) {
508: $hash = $object . substr(md5($object . $url . $c), 0, 10);
509: $c++;
510: }
511: $this->uuids[] = $hash;
512: return $hash;
513: }
514:
515: /**
516: * Allows a template or element to set a variable that will be available in
517: * a layout or other element. Analogous to Controller::set().
518: *
519: * @param mixed $one A string or an array of data.
520: * @param mixed $two Value in case $one is a string (which then works as the key).
521: * Unused if $one is an associative array, otherwise serves as the values to $one's keys.
522: * @return void
523: */
524: public function set($one, $two = null) {
525: $data = null;
526: if (is_array($one)) {
527: if (is_array($two)) {
528: $data = array_combine($one, $two);
529: } else {
530: $data = $one;
531: }
532: } else {
533: $data = array($one => $two);
534: }
535: if ($data == null) {
536: return false;
537: }
538: $this->viewVars = $data + $this->viewVars;
539: }
540:
541: /**
542: * Magic accessor for helpers. Provides access to attributes that were deprecated.
543: *
544: * @param string $name Name of the attribute to get.
545: * @return mixed
546: */
547: public function __get($name) {
548: if (isset($this->Helpers->{$name})) {
549: return $this->Helpers->{$name};
550: }
551: switch ($name) {
552: case 'base':
553: case 'here':
554: case 'webroot':
555: case 'data':
556: return $this->request->{$name};
557: case 'action':
558: return isset($this->request->params['action']) ? $this->request->params['action'] : '';
559: case 'params':
560: return $this->request;
561: }
562: return null;
563: }
564:
565: /**
566: * Interact with the HelperCollection to load all the helpers.
567: *
568: * @return void
569: */
570: public function loadHelpers() {
571: $helpers = HelperCollection::normalizeObjectArray($this->helpers);
572: foreach ($helpers as $name => $properties) {
573: list($plugin, $class) = pluginSplit($properties['class']);
574: $this->{$class} = $this->Helpers->load($properties['class'], $properties['settings']);
575: }
576: $this->_helpersLoaded = true;
577: }
578:
579: /**
580: * Renders and returns output for given view filename with its
581: * array of data.
582: *
583: * @param string $___viewFn Filename of the view
584: * @param array $___dataForView Data to include in rendered view. If empty the current View::$viewVars will be used.
585: * @return string Rendered output
586: */
587: protected function _render($___viewFn, $___dataForView = array()) {
588: if (empty($___dataForView)) {
589: $___dataForView = $this->viewVars;
590: }
591:
592: extract($___dataForView, EXTR_SKIP);
593: ob_start();
594:
595: include $___viewFn;
596:
597: return ob_get_clean();
598: }
599:
600: /**
601: * Loads a helper. Delegates to the `HelperCollection::load()` to load the helper
602: *
603: * @param string $helperName Name of the helper to load.
604: * @param array $settings Settings for the helper
605: * @return Helper a constructed helper object.
606: * @see HelperCollection::load()
607: */
608: public function loadHelper($helperName, $settings = array()) {
609: return $this->Helpers->load($helperName, $settings);
610: }
611:
612: /**
613: * Returns filename of given action's template file (.ctp) as a string.
614: * CamelCased action names will be under_scored! This means that you can have
615: * LongActionNames that refer to long_action_names.ctp views.
616: *
617: * @param string $name Controller action to find template filename for
618: * @return string Template filename
619: * @throws MissingViewException when a view file could not be found.
620: */
621: protected function _getViewFileName($name = null) {
622: $subDir = null;
623:
624: if (!is_null($this->subDir)) {
625: $subDir = $this->subDir . DS;
626: }
627:
628: if ($name === null) {
629: $name = $this->view;
630: }
631: $name = str_replace('/', DS, $name);
632:
633: if (strpos($name, DS) === false && $name[0] !== '.') {
634: $name = $this->viewPath . DS . $subDir . Inflector::underscore($name);
635: } elseif (strpos($name, DS) !== false) {
636: if ($name[0] === DS || $name[1] === ':') {
637: if (is_file($name)) {
638: return $name;
639: }
640: $name = trim($name, DS);
641: } else if ($name[0] === '.') {
642: $name = substr($name, 3);
643: } else {
644: $name = $this->viewPath . DS . $subDir . $name;
645: }
646: }
647: $paths = $this->_paths($this->plugin);
648:
649: $exts = $this->_getExtensions();
650: foreach ($exts as $ext) {
651: foreach ($paths as $path) {
652: if (file_exists($path . $name . $ext)) {
653: return $path . $name . $ext;
654: }
655: }
656: }
657: $defaultPath = $paths[0];
658:
659: if ($this->plugin) {
660: $pluginPaths = App::path('plugins');
661: foreach ($paths as $path) {
662: if (strpos($path, $pluginPaths[0]) === 0) {
663: $defaultPath = $path;
664: break;
665: }
666: }
667: }
668: throw new MissingViewException(array('file' => $defaultPath . $name . $this->ext));
669: }
670:
671: /**
672: * Returns layout filename for this template as a string.
673: *
674: * @param string $name The name of the layout to find.
675: * @return string Filename for layout file (.ctp).
676: * @throws MissingLayoutException when a layout cannot be located
677: */
678: protected function _getLayoutFileName($name = null) {
679: if ($name === null) {
680: $name = $this->layout;
681: }
682: $subDir = null;
683:
684: if (!is_null($this->layoutPath)) {
685: $subDir = $this->layoutPath . DS;
686: }
687: $paths = $this->_paths($this->plugin);
688: $file = 'Layouts' . DS . $subDir . $name;
689:
690: $exts = $this->_getExtensions();
691: foreach ($exts as $ext) {
692: foreach ($paths as $path) {
693: if (file_exists($path . $file . $ext)) {
694: return $path . $file . $ext;
695: }
696: }
697: }
698: throw new MissingLayoutException(array('file' => $paths[0] . $file . $this->ext));
699: }
700:
701:
702: /**
703: * Get the extensions that view files can use.
704: *
705: * @return array Array of extensions view files use.
706: */
707: protected function _getExtensions() {
708: $exts = array($this->ext);
709: if ($this->ext !== '.ctp') {
710: array_push($exts, '.ctp');
711: }
712: return $exts;
713: }
714:
715: /**
716: * Finds an element filename, returns false on failure.
717: *
718: * @param string $name The name of the element to find.
719: * @param string $plugin The plugin name the element is in.
720: * @return mixed Either a string to the element filename or false when one can't be found.
721: */
722: protected function _getElementFileName($name, $plugin = null) {
723: $paths = $this->_paths($plugin);
724: $exts = $this->_getExtensions();
725: foreach ($exts as $ext) {
726: foreach ($paths as $path) {
727: if (file_exists($path . 'Elements' . DS . $name . $ext)) {
728: return $path . 'Elements' . DS . $name . $ext;
729: }
730: }
731: }
732: return false;
733: }
734:
735: /**
736: * Return all possible paths to find view files in order
737: *
738: * @param string $plugin Optional plugin name to scan for view files.
739: * @param boolean $cached Set to true to force a refresh of view paths.
740: * @return array paths
741: */
742: protected function _paths($plugin = null, $cached = true) {
743: if ($plugin === null && $cached === true && !empty($this->_paths)) {
744: return $this->_paths;
745: }
746: $paths = array();
747: $viewPaths = App::path('View');
748: $corePaths = array_flip(App::core('View'));
749: if (!empty($plugin)) {
750: $count = count($viewPaths);
751: for ($i = 0; $i < $count; $i++) {
752: if (!isset($corePaths[$viewPaths[$i]])) {
753: $paths[] = $viewPaths[$i] . 'Plugin' . DS . $plugin . DS;
754: }
755: }
756: $paths = array_merge($paths, App::path('View', $plugin));
757: }
758:
759: $this->_paths = array_unique(array_merge($paths, $viewPaths, array_keys($corePaths)));
760: return $this->_paths;
761: }
762: }
763: