Adding tests for securing private variable inclussion on templates.
[akelos.git] / lib / AkActionController.php
blob07f7e6bea143a079222aa42fa13cc1ea294d686a
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 // +----------------------------------------------------------------------+
5 // | Akelos Framework - http://www.akelos.org |
6 // +----------------------------------------------------------------------+
7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
8 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
9 // +----------------------------------------------------------------------+
11 if(!class_exists('AkActionController')){
13 require_once(AK_LIB_DIR.DS.'AkObject.php');
15 defined('AK_HIGH_LOAD_MODE') ? null : define('AK_HIGH_LOAD_MODE', false);
16 defined('AK_APP_NAME') ? null : define('AK_APP_NAME', 'Application');
18 /**
19 * @package ActionController
20 * @subpackage Base
21 * @author Bermi Ferrer <bermi a.t akelos c.om>
22 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
23 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
26 class AkActionController extends AkObject
28 var $_high_load_mode = AK_HIGH_LOAD_MODE;
29 var $_enable_plugins = true;
30 var $_auto_instantiate_models = true;
31 var $validate_output = false;
33 var $_ssl_requirement = false;
34 var $_ssl_allowed_actions = array();
35 var $ssl_for_all_actions = true;
37 /**
38 * Determines whether the view has access to controller internals $this->Request, $this->Response, $this->session, and $this->Template.
39 * By default, it does.
41 var $_view_controller_internals = true;
43 /**
44 * Protected instance variable cache
46 var $_protected_variables_cache = array();
48 /**
49 * Prepends all the URL-generating helpers from AssetHelper.
50 * This makes it possible to easily move javascripts, stylesheets,
51 * and images to a dedicated asset server away from the main web server.
52 * Example:
53 * $this->_asset_host = 'http://assets.example.com';
55 var $asset_host = AK_ASSET_HOST;
58 var $_Logger;
60 /**
61 * Determines which template class should be used by AkActionController.
63 var $TemplateClass;
65 /**
66 * Turn on +_ignore_missing_templates+ if you want to unit test actions without
67 * making the associated templates.
69 var $_ignore_missing_templates;
71 /**
72 * Holds the Request object that's primarily used to get environment variables
74 var $Request;
76 /**
77 * Holds an array of all the GET, POST, and Url parameters passed to the action.
78 * Accessed like <tt>$this->params['post_id'];</tt>
79 * to get the post_id.
81 var $params = array();
83 /**
84 * Holds the Response object that's primarily used to set additional HTTP _headers
85 * through access like <tt>$this->Response->_headers['Cache-Control'] = 'no-cache';</tt>.
86 * Can also be used to access the final body HTML after a template
87 * has been rendered through $this->Response->body -- useful for <tt>after_filter</tt>s
88 * that wants to manipulate the output, such as a OutputCompressionFilter.
90 var $Response;
92 /**
93 * Holds an array of objects in the session. Accessed like <tt>$this->session['person']</tt>
94 * to get the object tied to the 'person' key. The session will hold any type of object
95 * as values, but the key should be a string.
97 var $session;
99 /**
100 * Holds an array of header names and values. Accessed like <tt>$this->_headers['Cache-Control']</tt>
101 * to get the value of the Cache-Control directive. Values should always be specified as strings.
103 var $_headers = array();
106 * Holds the array of variables that are passed on to the template class to be
107 * made available to the view. This array is generated by taking a snapshot of
108 * all the instance variables in the current scope just before a template is rendered.
110 var $_assigns = array();
113 * Holds the name of the action this controller is processing.
115 var $_action_name;
117 var $cookies;
119 var $helpers = 'default';
121 var $app_helpers;
122 var $plugin_helpers = 'all';
124 var $web_service;
125 var $web_services = array();
127 var $web_service_api;
128 var $web_service_apis = array();
130 var $module_name;
131 var $_module_path;
134 * Old fashioned way of dispatching requests. Please use AkDispatcher or roll your own.
136 * @deprecated
138 function handleRequest()
140 AK_LOG_EVENTS && empty($this->_Logger) ? ($this->_Logger =& Ak::getLogger()) : null;
141 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->warning('Using deprecated request dispatcher AkActionController::handleRequest. Use to AkDispatcher + AkDispatcher::dispatch instead.') : null;
142 require_once(AK_LIB_DIR.DS.'AkDispatcher.php');
143 $Dispatcher =& new AkDispatcher();
144 $Dispatcher->dispatch();
147 function process(&$Request, &$Response)
149 AK_LOG_EVENTS && empty($this->_Logger) ? ($this->_Logger =& Ak::getLogger()) : null;
151 $this->Request =& $Request;
152 $this->Response =& $Response;
153 $this->params = $this->Request->getParams();
154 $this->_action_name = $this->Request->getAction();
156 if(!method_exists($this, $this->_action_name)){
157 trigger_error(Ak::t('Controller <i>%controller_name</i> can\'t handle action %action_name',
158 array(
159 '%controller_name' => $this->getControllerName(),
160 '%action_name' => $this->_action_name,
161 )), E_USER_ERROR);
164 Ak::t('Akelos'); // We need to get locales ready
166 if($this->_high_load_mode !== true){
167 if(!empty($this->_auto_instantiate_models)){
168 $this->instantiateIncludedModelClasses();
170 if(!empty($this->_enable_plugins)){
171 $this->loadPlugins();
173 if(!empty($this->helpers)){
174 $this->instantiateHelpers();
176 }else{
177 $this->_enableLayoutOnRender = false;
180 $this->_ensureProperProtocol();
182 // After filters
183 $this->afterFilter('_handleFlashAttribute');
185 $this->_loadActionView();
187 if(isset($this->api)){
188 require_once(AK_LIB_DIR.DS.'AkActionWebService.php');
189 $this->aroundFilter(new AkActionWebService($this));
192 $this->performActionWithFilters($this->_action_name);
194 if (!$this->_hasPerformed()){
195 $this->_enableLayoutOnRender ? $this->renderWithLayout() : $this->renderWithoutLayout();
198 if(!empty($this->validate_output)){
199 $this->_validateGeneratedXhtml();
202 $this->Response->outputResults();
205 function _loadActionView()
207 empty($this->_assigns) ? ($this->_assigns = array()) : null;
208 empty($this->_default_render_status_code) ? ($this->_default_render_status_code = 200) : null;
209 $this->_enableLayoutOnRender = !isset($this->_enableLayoutOnRender) ? true : $this->_enableLayoutOnRender;
210 $this->passed_args = !isset($this->Request->pass)? array() : $this->Request->pass;
211 empty($this->cookies) && isset($_COOKIE) ? ($this->cookies =& $_COOKIE) : null;
213 if(empty($this->Template)){
214 require_once(AK_LIB_DIR.DS.'AkActionView.php');
215 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'AkPhpTemplateHandler.php');
216 $this->Template =& new AkActionView($this->_getTemplateBasePath(),
217 $this->Request->getParameters(),$this->Request->getController());
219 $this->Template->_controllerInstance =& $this;
220 $this->Template->_registerTemplateHandler('tpl','AkPhpTemplateHandler');
224 function loadPlugins()
226 Ak::loadPlugins();
230 * Creates an instance of each available helper and links it into into current controller.
232 * Per example, if a helper TextHelper is located into the file text_helper.php.
233 * An instance is created on current controller
234 * at $this->text_helper. This instance is also available on the view by calling $text_helper.
236 * Helpers can be found at lib/AkActionView/helpers (this might change in a future)
238 function instantiateHelpers()
240 $helpers = $this->getDefaultHelpers();
241 $helpers = array_merge($helpers, $this->getApplicationHelpers());
242 $helpers = array_merge($helpers, $this->getPluginHelpers());
244 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'AkActionViewHelper.php');
246 $current_controller_helper = $this->getControllerName();
247 $current_controller_helper_file_name = AK_HELPERS_DIR.DS.$this->_module_path.AkInflector::underscore($current_controller_helper).'_helper.php';
249 if(file_exists($current_controller_helper_file_name)){
250 $helpers[$current_controller_helper_file_name] = $current_controller_helper;
253 $available_helpers = array();
254 foreach ($helpers as $file=>$helper){
255 $helper_class_name = strstr($helper, 'Helper') ? $helper : $helper.'Helper';
256 $full_path = preg_match('/[\\\\\/]+/',$file);
257 $file_path = $full_path ? $file : AK_LIB_DIR.DS.'AkActionView'.DS.'helpers'.DS.$file;
258 include_once($file_path);
259 if(class_exists($helper_class_name)){
260 $attribute_name = $full_path ? AkInflector::underscore($helper_class_name) : substr($file,0,-4);
261 $available_helpers[] = $attribute_name;
262 $this->$attribute_name =& new $helper_class_name(&$this);
263 if(method_exists($this->$attribute_name,'setController')){
264 $this->$attribute_name->setController(&$this);
266 if(method_exists($this->$attribute_name,'init')){
267 $this->$attribute_name->init();
271 defined('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS') ? null : define('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS', join(',',$available_helpers));
274 function getDefaultHelpers()
276 if($this->helpers == 'default'){
277 $available_helpers = Ak::dir(AK_LIB_DIR.DS.'AkActionView'.DS.'helpers',array('dirs'=>false));
278 $helper_names = array();
279 foreach ($available_helpers as $available_helper){
280 $helper_names[$available_helper] = AkInflector::classify(substr($available_helper,0,-10));
282 return $helper_names;
283 }elseif (is_string($this->helpers)){
284 return Ak::stringToArray($this->helpers);
286 return $this->helpers;
289 function getApplicationHelpers()
291 $helper_names = array();
292 if ($this->app_helpers == 'all'){
293 $available_helpers = Ak::dir(AK_HELPERS_DIR,array('dirs'=>false));
294 $helper_names = array();
295 foreach ($available_helpers as $available_helper){
296 $helper_names[AK_HELPERS_DIR.DS.$available_helper] = AkInflector::classify(substr($available_helper,0,-10));
299 } elseif (!empty($this->app_helpers)){
300 foreach (Ak::toArray($this->app_helpers) as $helper_name){
301 $helper_names[AK_HELPERS_DIR.DS.AkInflector::underscore($helper_name).'_helper.php'] = AkInflector::camelize($helper_name);
304 return $helper_names;
307 function getPluginHelpers()
309 $helper_names = AkActionController::addPluginHelper(false); // Trick for getting helper names set by AkPlugin::addHelper
310 if(empty($helper_names)){
311 return array();
312 }elseif ($this->plugin_helpers == 'all'){
313 return $helper_names;
314 }else {
315 $selected_helper_names = array();
316 foreach (Ak::toArray($this->plugin_helpers) as $helper_name){
317 $helper_name = AkInflector::camelize($helper_name);
318 if($path = array_shift(array_keys($helper_names, AkInflector::camelize($helper_name)))){
319 $selected_helper_names[$path] = $helper_names[$path];
322 return $selected_helper_names;
327 * Used for adding helpers to the base class like those added by the plugins engine.
329 * @param string $helper_name Helper class name like CalendarHelper
330 * @param array $options - path: Path to the helper class, defaults to AK_PLUGINS_DIR/helper_name/lib/helper_name.php
332 function addPluginHelper($helper_name, $options = array())
334 static $helpers = array();
335 if($helper_name === false){
336 return $helpers;
338 $underscored_helper_name = AkInflector::underscore($helper_name);
339 $default_options = array(
340 'path' => AK_PLUGINS_DIR.DS.$underscored_helper_name.DS.'lib'.DS.$underscored_helper_name.'.php'
342 $options = array_merge($default_options, $options);
343 $helpers[$options['path']] = $helper_name;
346 function _validateGeneratedXhtml()
348 require_once(AK_LIB_DIR.DS.'AkXhtmlValidator.php');
349 $XhtmlValidator = new AkXhtmlValidator();
350 if($XhtmlValidator->validate($this->Response->body) === false){
351 $this->Response->sendHeaders();
352 echo '<h1>'.Ak::t('Ooops! There are some errors on current XHTML page').'</h1>';
353 echo '<small>'.Ak::t('In order to disable XHTML validation, set the <b>AK_ENABLE_STRICT_XHTML_VALIDATION</b> constant to false on your config/development.php file')."</small><hr />\n";
354 $XhtmlValidator->showErrors();
355 echo "<hr /><h2>".Ak::t('Showing XHTML code')."</h2><hr /><div style='border:5px solid red;margin:5px;padding:15px;'>".$this->Response->body."</pre>";
356 die();
362 * Methods for loading desired models into this controller
364 function setModel($model)
366 $this->instantiateIncludedModelClasses(array($model));
369 function setModels($models)
371 $this->instantiateIncludedModelClasses($models);
374 function instantiateIncludedModelClasses($models = array())
376 require_once(AK_LIB_DIR.DS.'AkActiveRecord.php');
377 require_once(AK_APP_DIR.DS.'shared_model.php');
379 empty($this->model) ? ($this->model = $this->params['controller']) : null;
380 empty($this->models) ? ($this->models = array()) : null;
382 $models = array_unique(array_merge(Ak::import($this->model), Ak::import($this->models), Ak::import($models), (empty($this->app_models)?array(): Ak::import($this->app_models))));
384 foreach ($models as $model){
385 $this->instantiateModelClass($model, (empty($this->finder_options[$model])?array():$this->finder_options[$model]));
389 function instantiateModelClass($model_class_name, $finder_options = array())
391 $underscored_model_class_name = AkInflector::underscore($model_class_name);
393 $id = empty($this->params[$underscored_model_class_name]['id']) ?
394 (empty($this->params['id']) ? false :
395 ($model_class_name == $this->getControllerName() ? $this->params['id'] : false)) :
396 $this->params[$underscored_model_class_name]['id'];
398 if(class_exists($model_class_name)){
399 $underscored_model_class_name = AkInflector::underscore($model_class_name);
401 if(!isset($this->$model_class_name) || !isset($this->$underscored_model_class_name)){
402 if($finder_options !== false && is_numeric($id)){
403 $model =& new $model_class_name();
404 if(empty($finder_options)){
405 $model =& $model->find($id);
406 }else{
407 $model =& $model->find($id, $finder_options);
409 }else{
410 $model =& new $model_class_name();
412 if(!isset($this->$model_class_name)){
413 $this->$model_class_name =& $model;
415 if(!isset($this->$underscored_model_class_name)){
416 $this->$underscored_model_class_name =& $model;
425 Rendering content
426 ====================================================================
430 * Renders the content that will be returned to the browser as the Response body.
432 * === Rendering an action
434 * Action rendering is the most common form and the type used automatically by
435 * Action Controller when nothing else is specified. By default, actions are
436 * rendered within the current layout (if one exists).
438 * * Renders the template for the action "goal" within the current controller
440 * $this->render(array('action'=>'goal'));
442 * * Renders the template for the action "short_goal" within the current controller,
443 * but without the current active layout
445 * $this->render(array('action'=>'short_goal','layout'=>false));
447 * * Renders the template for the action "long_goal" within the current controller,
448 * but with a custom layout
450 * $this->render(array('action'=>'long_goal','layout'=>'spectacular'));
452 * === Rendering partials
454 * Partial rendering is most commonly used together with Ajax calls that only update
455 * one or a few elements on a page without reloading. Rendering of partials from
456 * the controller makes it possible to use the same partial template in
457 * both the full-page rendering (by calling it from within the template) and when
458 * sub-page updates happen (from the controller action responding to Ajax calls).
459 * By default, the current layout is not used.
461 * * Renders the partial located at app/views/controller/_win.tpl
463 * $this->render(array('partial'=>'win'));
465 * * Renders the partial with a status code of 500 (internal error)
467 * $this->render(array('partial'=>'broken','status'=>500));
469 * * Renders the same partial but also makes a local variable available to it
471 * $this->render(array('partial' => 'win', 'locals' => array('name'=>'david')));
473 * * Renders a collection of the same partial by making each element of $wins available through
474 * the local variable "win" as it builds the complete Response
476 * $this->render(array('partial'=>'win','collection'=>$wins));
478 * * Renders the same collection of partials, but also renders the win_divider partial in between
479 * each win partial.
481 * $this->render(array('partial'=>'win','collection'=>$wins,'spacer_template'=>'win_divider'));
483 * === Rendering a template
485 * Template rendering works just like action rendering except that it takes a
486 * path relative to the template root.
487 * The current layout is automatically applied.
489 * * Renders the template located in app/views/weblog/show.tpl
490 * $this->render(array('template'=>'weblog/show'));
492 * === Rendering a file
494 * File rendering works just like action rendering except that it takes a
495 * filesystem path. By default, the path is assumed to be absolute, and the
496 * current layout is not applied.
498 * * Renders the template located at the absolute filesystem path
499 * $this->render(array('file'=>'/path/to/some/template.tpl'));
500 * $this->render(array('file'=>'c:/path/to/some/template.tpl'));
502 * * Renders a template within the current layout, and with a 404 status code
503 * $this->render(array('file' => '/path/to/some/template.tpl', 'layout' => true, 'status' => 404));
504 * $this->render(array('file' => 'c:/path/to/some/template.tpl', 'layout' => true, 'status' => 404));
506 * * Renders a template relative to the template root and chooses the proper file extension
507 * $this->render(array('file' => 'some/template', 'use_full_path' => true));
510 * === Rendering text
512 * Rendering of text is usually used for tests or for rendering prepared content,
513 * such as a cache. By default, text
514 * rendering is not done within the active layout.
516 * * Renders the clear text "hello world" with status code 200
517 * $this->render(array('text' => 'hello world!'));
519 * * Renders the clear text "Explosion!" with status code 500
520 * $this->render(array('text' => "Explosion!", 'status' => 500 ));
522 * * Renders the clear text "Hi there!" within the current active layout (if one exists)
523 * $this->render(array('text' => "Explosion!", 'layout' => true));
525 * * Renders the clear text "Hi there!" within the layout
526 * * placed in "app/views/layouts/special.tpl"
527 * $this->render(array('text' => "Explosion!", 'layout => "special"));
530 * === Rendering an inline template
532 * Rendering of an inline template works as a cross between text and action
533 * rendering where the source for the template
534 * is supplied inline, like text, but its evaled by PHP, like action. By default,
535 * PHP is used for rendering and the current layout is not used.
537 * * Renders "hello, hello, hello, again"
538 * $this->render(array('inline' => "<?php echo str_repeat('hello, ', 3).'again'?>" ));
540 * * Renders "hello david"
541 * $this->render(array('inline' => "<?php echo 'hello ' . $name ?>", 'locals' => array('name' => 'david')));
544 * === Rendering nothing
546 * Rendering nothing is often convenient in combination with Ajax calls that
547 * perform their effect client-side or
548 * when you just want to communicate a status code. Due to a bug in Safari, nothing
549 * actually means a single space.
551 * * Renders an empty Response with status code 200
552 * $this->render(array('nothing' => true));
554 * * Renders an empty Response with status code 401 (access denied)
555 * $this->render(array('nothing' => true, 'status' => 401));
557 function render($options = null, $status = 200)
559 if(empty($options['partial']) && $this->_hasPerformed()){
560 $this->_doubleRenderError(Ak::t("Can only render or redirect once per action"));
561 return false;
564 $this->_flash_handled ? null : $this->_handleFlashAttribute();
566 if(!is_array($options)){
567 return $this->renderFile(empty($options) ? $this->getDefaultTemplateName() : $options, $status, true);
570 if(!empty($options['text'])){
571 return $this->renderText($options['text'], @$options['status']);
572 }else{
574 if(!empty($options['file'])){
575 return $this->renderFile($options['file'], @$options['status'], @$options['use_full_path'], @(array)$options['locals']);
576 }elseif(!empty($options['template'])){
577 return $this->renderFile($options['template'], @$options['status'], true);
578 }elseif(!empty($options['inline'])){
579 return $this->renderTemplate($options['inline'], @$options['status'], @$options['type'], @(array)$options['locals']);
580 }elseif(!empty($options['action'])){
581 return $this->renderAction($options['action'], @$options['status'], @$options['layout']);
582 }elseif(!empty($options['partial'])){
583 if($options['partial'] === true){
584 $options['partial'] = !empty($options['template']) ? $options['template'] : $this->getDefaultTemplateName();
586 if(!empty($options['collection'])){
587 return $this->renderPartialCollection($options['partial'], $options['collection'], @$options['spacer_template'], @$options['locals'], @$options['status']);
588 }else{
589 return $this->renderPartial($options['partial'], @$options['object'], @$options['locals'], @$options['status']);
591 }elseif(!empty($options['nothing'])){
592 // Safari doesn't pass the _headers of the return if the Response is zero length
593 return $this->renderText(' ', @$options['status']);
594 }else{
595 return $this->renderFile($this->getDefaultTemplateName(), @$options['status'], true);
597 return true;
602 * Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead
603 * of sending it as the Response body to the browser.
605 function renderToString($options = null)
607 $result = $this->render($options);
608 $this->eraseRenderResults();
609 $this->variables_added = null;
610 $this->Template->_assigns_added = null;
611 return $result;
614 function renderAction($_action_name, $status = null, $with_layout = true)
616 $this->$_action_name();
617 $template = $this->getDefaultTemplateName($_action_name);
618 if(!empty($with_layout) && !$this->_isTemplateExemptFromLayout($template)){
619 return $this->renderWithLayout($template, $status, $with_layout);
620 }else{
621 return $this->renderWithoutLayout($template, $status);
625 function renderFile($template_path, $status = null, $use_full_path = false, $locals = array())
627 $this->_addVariablesToAssigns();
628 $locals = array_merge($locals,$this->_assigns);
630 if($use_full_path){
631 $this->_assertExistanceOfTemplateFile($template_path);
634 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->message("Rendering $this->full_template_path" . (!empty($status) ? " ($status)":'')) : null;
636 return $this->renderText($this->Template->renderFile($template_path, $use_full_path, $locals), $status);
639 function renderTemplate($template, $status = null, $type = 'tpl', $local_assigns = array())
641 $this->_addVariablesToAssigns();
642 $local_assigns = array_merge($local_assigns,$this->_assigns);
643 return $this->renderText($this->Template->renderTemplate($type, $template, null, $local_assigns), $status);
646 function renderText($text = null, $status = null)
648 $this->performed_render = true;
649 $this->Response->_headers['Status'] = !empty($status) ? $status : $this->_default_render_status_code;
650 $this->Response->body = $text;
651 return $text;
654 function renderNothing($status = null)
656 return $this->renderText(' ', $status);
659 function renderPartial($partial_path = null, $object = null, $local_assigns = null, $status = null)
661 $partial_path = empty($partial_path) ? $this->getDefaultTemplateName() : $partial_path;
662 $this->variables_added = false;
663 $this->performed_render = false;
664 $this->_addVariablesToAssigns();
665 $this->Template->controller =& $this;
666 $this->$partial_path = $this->renderText($this->Template->renderPartial($partial_path, $object, array_merge($this->_assigns, (array)$local_assigns)), $status);
667 return $this->$partial_path;
670 function renderPartialCollection($partial_name, $collection, $partial_spacer_template = null, $local_assigns = null, $status = null)
672 $this->_addVariablesToAssigns();
673 $collection_name = AkInflector::pluralize($partial_name).'_collection';
674 $result = $this->Template->renderPartialCollection($partial_name, $collection, $partial_spacer_template, $local_assigns);
675 if(empty($this->$collection_name)){
676 $this->$collection_name = $result;
678 $this->variables_added = false;
679 $this->performed_render = false;
681 return $result;
684 function renderWithLayout($template_name = null, $status = null, $layout = null)
686 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
687 return $this->renderWithALayout($template_name, $status, $layout);
690 function renderWithoutLayout($template_name = null, $status = null)
692 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
693 return $this->render($template_name, $status);
697 * Clears the rendered results, allowing for another render to be performed.
699 function eraseRenderResults()
701 $this->Response->body = '';
702 $this->performed_render = false;
703 $this->variables_added = false;
706 function _addVariablesToAssigns()
708 if(empty($this->variables_added)){
709 $this->_addInstanceVariablesToAssigns();
710 $this->variables_added = true;
714 function _addInstanceVariablesToAssigns()
716 $this->_protected_variables_cache = array_merge($this->_protected_variables_cache, $this->_getProtectedInstanceVariables());
718 foreach (array_diff(array_keys(get_object_vars($this)), $this->_protected_variables_cache) as $attribute){
719 if($attribute[0] != '_'){
720 $this->_assigns[$attribute] =& $this->$attribute;
725 function _getProtectedInstanceVariables()
727 return !empty($this->_view_controller_internals) ?
728 array('_assigns', 'performed_redirect', 'performed_render','db') :
729 array('_assigns', 'performed_redirect', 'performed_render', 'session', 'cookies',
730 'Template','db','helpers','models','layout','Response','Request',
731 'params','passed_args');
736 * Use this to translate strings in the scope of your controller
738 * @see Ak::t
740 function t($string, $array = null)
742 return Ak::t($string, $array, AkInflector::underscore($this->getControllerName()));
748 Redirects
749 ====================================================================
753 * Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
755 * * <tt>Array</tt>: The URL will be generated by calling $this->UrlFor with the +options+.
756 * * <tt>String starting with protocol:// (like http://)</tt>: Is passed straight through
757 * as the target for redirection.
758 * * <tt>String not containing a protocol</tt>: The current protocol and host is prepended to the string.
759 * * <tt>back</tt>: Back to the page that issued the Request-> Useful for forms that are
760 * triggered from multiple places.
761 * Short-hand for redirectTo(Request->env["HTTP_REFERER"])
763 * Examples:
764 * redirectTo(array('action' => 'show', 'id' => 5));
765 * redirectTo('http://www.akelos.com');
766 * redirectTo('/images/screenshot.jpg');
767 * redirectTo('back');
769 * The redirection happens as a "302 Moved" header.
771 function redirectTo($options = array(), $parameters_for_method_reference = null)
773 if(is_string($options)) {
774 if(preg_match('/^\w+:\/\/.*/',$options)){
775 if($this->_hasPerformed()){
776 $this->_doubleRenderError();
778 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->message('Redirected to '.$options) : null;
779 $this->_handleFlashAttribute();
780 $this->Response->redirect($options);
781 $this->Response->redirected_to = $options;
782 $this->performed_redirect = true;
783 }elseif ($options == 'back'){
784 $this->redirectTo($this->Request->env['HTTP_REFERER']);
785 }else{
786 $this->redirectTo($this->Request->getProtocol(). $this->Request->getHostWithPort(). $options);
788 }else{
789 if(empty($parameters_for_method_reference)){
790 $this->redirectTo($this->UrlFor($options));
791 $this->Response->redirected_to = $options;
792 }else{
793 $this->redirectTo($this->UrlFor($options, $parameters_for_method_reference));
794 $this->Response->redirected_to = $options;
795 $this->Response->redirected_to_method_params = $parameters_for_method_reference;
800 function redirectToAction($action, $options = array())
802 $this->redirectTo(array_merge(array('action'=>$action), $options));
807 * This methods are required for retrieving available controllers for URL Routing
809 function rewriteOptions($options)
811 $defaults = $this->defaultUrlOptions($options);
812 if(!empty($this->module_name)){
813 $defaults['module'] = $this->getModuleName();
815 if(!empty($options['controller']) && strstr($options['controller'], '/')){
816 $defaults['module'] = substr($options['controller'], 0, strrpos($options['controller'], '/'));
817 $options['controller'] = substr($options['controller'], strrpos($options['controller'], '/') + 1);
819 $options = !empty($defaults) ? array_merge($defaults, $options) : $options;
820 $options['controller'] = empty($options['controller']) ? AkInflector::underscore($this->getControllerName()) : $options['controller'];
821 return $options;
824 function getControllerName()
827 if(!isset($this->controller_name)){
828 $current_class_name = str_replace('_', '::', get_class($this));
830 $included_controllers = $this->_getIncludedControllerNames();
831 $lowercase_included_controllers = array_map('strtolower', $included_controllers);
832 $key = array_search(strtolower($current_class_name), $lowercase_included_controllers, true);
833 $found_controller = substr($included_controllers[$key], 0, -10);
834 $this->controller_name = $found_controller;
835 $this->_removeModuleNameFromControllerName();
838 return $this->controller_name;
841 function getModuleName()
843 return $this->module_name;
847 * Removes the modules name from the controller if exists.
849 * Additionally it sets the default url_options for this controller and the module name scope.
851 function _removeModuleNameFromControllerName()
853 if(strstr($this->controller_name, '::')){
854 $module_parts = substr($this->controller_name, 0, strrpos($this->controller_name, '::'));
855 $this->module_name = join('/', array_map(array('AkInflector','underscore'), strstr($module_parts, '::') ? explode('::', $module_parts) : array($module_parts)));
856 $this->controller_name = substr($this->controller_name, strrpos($this->controller_name, '::')+2);
861 function _getTemplateBasePath()
863 return AK_APP_DIR.DS.'views'.DS.(empty($this->_module_path)?'':$this->_module_path).$this->Request->getController();
866 function _getIncludedControllerNames()
868 $controllers = array();
869 foreach (get_included_files() as $file_name){
870 if(strstr($file_name,AK_CONTROLLERS_DIR)){
871 $controllers[] = AkInflector::classify(str_replace(array(AK_CONTROLLERS_DIR.DS,'.php', DS, '//'),array('','','/', '/'),$file_name));
874 return $controllers;
881 URL generation/rewriting
882 ====================================================================
887 * Overwrite to implement a number of default options that all urlFor-based methods will use.
888 * The default options should come in
889 * the form of a an array, just like the one you would use for $this->UrlFor directly. Example:
891 * function defaultUrlOptions($options)
893 * return array('project' => ($this->Project->isActive() ? $this->Project->url_name : 'unknown'));
896 * As you can infer from the example, this is mostly useful for situations where you want to
897 * centralize dynamic decisions about the urls as they stem from the business domain.
898 * Please note that any individual $this->UrlFor call can always override the defaults set
899 * by this method.
901 function defaultUrlOptions($options)
907 * Returns a URL that has been rewritten according to the options array and the defined Routes.
908 * (For doing a complete redirect, use redirectTo).
910 * <tt>$this->UrlFor</tt> is used to:
912 * All keys given to $this->UrlFor are forwarded to the Route module, save for the following:
913 * * <tt>anchor</tt> -- specifies the anchor name to be appended to the path. For example,
914 * <tt>$this->UrlFor(array('controller' => 'posts', 'action' => 'show', 'id' => 10, 'anchor' => 'comments'</tt>
915 * will produce "/posts/show/10#comments".
916 * * <tt>only_path</tt> -- if true, returns the absolute URL (omitting the protocol, host name, and port)
917 * * <tt>trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
918 * is currently not recommended since it breaks caching.
919 * * <tt>host</tt> -- overrides the default (current) host if provided
920 * * <tt>protocol</tt> -- overrides the default (current) protocol if provided
922 * The URL is generated from the remaining keys in the array. A URL contains two key parts: the <base> and a query string.
923 * Routes composes a query string as the key/value pairs not included in the <base>.
925 * The default Routes setup supports a typical Akelos Framework path of "controller/action/id"
926 * where action and id are optional, with
927 * action defaulting to 'index' when not given. Here are some typical $this->UrlFor statements
928 * and their corresponding URLs:
930 * $this->UrlFor(array('controller'=>'posts','action'=>'recent')); // 'proto://host.com/posts/recent'
931 * $this->UrlFor(array('controller'=>'posts','action'=>'index')); // 'proto://host.com/posts'
932 * $this->UrlFor(array('controller'=>'posts','action'=>'show','id'=>10)); // 'proto://host.com/posts/show/10'
934 * When generating a new URL, missing values may be filled in from the current
935 * Request's parameters. For example,
936 * <tt>$this->UrlFor(array('action'=>'some_action'));</tt> will retain the current controller,
937 * as expected. This behavior extends to other parameters, including <tt>controller</tt>,
938 * <tt>id</tt>, and any other parameters that are placed into a Route's path.
940 * The URL helpers such as <tt>$this->UrlFor</tt> have a limited form of memory:
941 * when generating a new URL, they can look for missing values in the current Request's parameters.
942 * Routes attempts to guess when a value should and should not be
943 * taken from the defaults. There are a few simple rules on how this is performed:
945 * * If the controller name begins with a slash, no defaults are used: <tt>$this->UrlFor(array('controller'=>'/home'));</tt>
946 * * If the controller changes, the action will default to index unless provided
948 * The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
949 * route given by <tt>map->connect('people/:last/:first/:action', array('action' => 'bio', 'controller' => 'people'))</tt>.
951 * Suppose that the current URL is "people/hh/david/contacts". Let's consider a few
952 * different cases of URLs which are generated from this page.
954 * * <tt>$this->UrlFor(array('action'=>'bio'));</tt> -- During the generation of this URL,
955 * default values will be used for the first and
956 * last components, and the action shall change. The generated URL will be, "people/hh/david/bio".
957 * * <tt>$this->UrlFor(array('first'=>'davids-little-brother'));</tt> This
958 * generates the URL 'people/hh/davids-little-brother' -- note
959 * that this URL leaves out the assumed action of 'bio'.
961 * However, you might ask why the action from the current Request, 'contacts', isn't
962 * carried over into the new URL. The answer has to do with the order in which
963 * the parameters appear in the generated path. In a nutshell, since the
964 * value that appears in the slot for <tt>first</tt> is not equal to default value
965 * for <tt>first</tt> we stop using defaults. On it's own, this rule can account
966 * for much of the typical Akelos Framework URL behavior.
968 * Although a convienence, defaults can occasionaly get in your way. In some cases
969 * a default persists longer than desired.
970 * The default may be cleared by adding <tt>'name' => null</tt> to <tt>$this->UrlFor</tt>'s options.
971 * This is often required when writing form helpers, since the defaults in play
972 * may vary greatly depending upon where the helper is used from. The following line
973 * will redirect to PostController's default action, regardless of the page it is
974 * displayed on:
976 * $this->UrlFor(array('controller' => 'posts', 'action' => null));
978 * If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the
979 * overwrite_params options. Say for your posts you have different views for showing and printing them.
980 * Then, in the show view, you get the URL for the print view like this
982 * $this->UrlFor(array('overwrite_params' => array('action' => 'print')));
984 * This takes the current URL as is and only exchanges the action. In contrast,
985 * <tt>$this->UrlFor(array('action'=>'print'));</tt>
986 * would have slashed-off the path components after the changed action.
988 function urlFor($options = array(), $parameters_for_method_reference = null)
990 return $this->rewrite($this->rewriteOptions($options));
993 function addToUrl($options = array(), $options_to_exclude = array())
995 $options_to_exclude = array_merge(array('ak','lang',AK_SESSION_NAME,'AK_SESSID','PHPSESSID'), $options_to_exclude);
996 $options = array_merge(array_merge(array('action'=>$this->Request->getAction()),$this->params),$options);
997 foreach ($options_to_exclude as $option_to_exclude){
998 unset($options[$option_to_exclude]);
1000 return $this->urlFor($options);
1003 function getActionName()
1005 return $this->Request->getAction();
1009 function _doubleRenderError($message = null)
1011 trigger_error(!empty($message) ? $message : Ak::t("Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and only once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirectTo(...); return;\". Finally, note that to cause a before filter to halt execution of the rest of the filter chain, the filter must return false, explicitly, so \"render(...); return; false\"."),E_USER_ERROR);
1014 function _hasPerformed()
1016 return !empty($this->performed_render) || !empty($this->performed_redirect);
1019 function _getRequestOrigin()
1021 return $this->Request->remote_ip.' at '.Ak::getDate();
1024 function _getCompleteRequestUri()
1026 return $this->Request->protocol . $this->Request->host . $this->Request->request_uri;
1029 function _closeSession()
1031 !empty($this->session) ? session_write_close() : null;
1035 function _hasTemplate($template_name = null)
1037 return file_exists(empty($template_name) ? $this->getDefaultTemplateName() : $template_name);
1040 function _templateIsPublic($template_name = null)
1042 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
1043 return $this->Template->fileIsPublic($template_name);
1046 function _isTemplateExemptFromLayout($template_name = null)
1048 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
1049 return $this->Template->_javascriptTemplateExists($template_name);
1052 function _assertExistanceOfTemplateFile($template_name)
1054 $extension = $this->Template->delegateTemplateExists($template_name);
1055 $this->full_template_path = $this->Template->getFullTemplatePath($template_name, $extension ? $extension : 'tpl');
1056 if(!$this->_hasTemplate($this->full_template_path)){
1057 if(!empty($this->_ignore_missing_templates) && $this->_ignore_missing_templates === true){
1058 return;
1060 $template_type = strstr($template_name,'layouts') ? 'layout' : 'template';
1061 trigger_error(Ak::t('Missing %template_type %full_template_path',array('%template_type'=>$template_type, '%full_template_path'=>$this->full_template_path)), E_USER_WARNING);
1065 function getDefaultTemplateName($default_action_name = null)
1067 return empty($default_action_name) ? (empty($this->_default_template_name) ? $this->_action_name : $this->_default_template_name) : $default_action_name;
1070 function setDefaultTemplateName($template_name)
1072 $this->_default_template_name = $template_name;
1077 function rewrite($options = array())
1079 return $this->_rewriteUrl($this->_rewritePath($options), $options);
1083 function toString()
1085 return $this->Request->getProtocol().$this->Request->getHostWithPort().
1086 $this->Request->getPath().@$this->parameters['controller'].
1087 @$this->parameters['action'].@$this->parameters['inspect'];
1091 * Given a path and options, returns a rewritten URL string
1093 function _rewriteUrl($path, $options)
1095 $rewritten_url = '';
1096 if(empty($options['only_path'])){
1097 $rewritten_url .= !empty($options['protocol']) ? $options['protocol'] : $this->Request->getProtocol();
1098 $rewritten_url .= empty($rewritten_url) || strpos($rewritten_url,'://') ? '' : '://';
1099 $rewritten_url .= $this->_rewriteAuthentication($options);
1100 $rewritten_url .= !empty($options['host']) ? $options['host'] : $this->Request->getHostWithPort();
1101 $options = Ak::delete($options, array('user','password','host','protocol'));
1104 $rewritten_url .= empty($options['skip_relative_url_root']) ? $this->Request->getRelativeUrlRoot() : '';
1106 if(empty($options['skip_url_locale'])){
1107 $locale = $this->Request->getLocaleFromUrl();
1108 if(empty($options['lang'])){
1109 $rewritten_url .= (empty($locale) ? '' : '/').$locale;
1114 $rewritten_url .= (substr($rewritten_url,-1) == '/' ? '' : (AK_URL_REWRITE_ENABLED ? '' : (!empty($path[0]) && $path[0] != '/' ? '/' : '')));
1115 $rewritten_url .= $path;
1116 $rewritten_url .= empty($options['trailing_slash']) ? '' : '/';
1117 $rewritten_url .= empty($options['anchor']) ? '' : '#'.$options['anchor'];
1119 return $rewritten_url;
1122 function _rewriteAuthentication($options)
1124 if(!isset($options['user']) && isset($options['password'])){
1125 return urlencode($options['user']).':'.urlencode($options['password']).'@';
1126 }else{
1127 return '';
1131 function _rewritePath($options)
1133 if(!empty($options['params'])){
1134 foreach ($options['params'] as $k=>$v){
1135 $options[$k] = $v;
1137 unset($options['params']);
1139 if(!empty($options['overwrite_params'])){
1140 foreach ($options['overwrite_params'] as $k=>$v){
1141 $options[$k] = $v;
1143 unset($options['overwrite_params']);
1145 foreach (array('anchor', 'params', 'only_path', 'host', 'protocol', 'trailing_slash', 'skip_relative_url_root') as $k){
1146 unset($options[$k]);
1148 $path = Ak::toUrl($options);
1149 return $path;
1153 * Returns a query string with escaped keys and values from the passed array. If the passed
1154 * array contains an 'id' it'll
1155 * be added as a path element instead of a regular parameter pair.
1157 function buildQueryString($array, $only_keys = null)
1159 $array = !empty($only_keys) ? array_keys($array) : $array;
1160 return Ak::toUrl($array);
1167 Layouts
1168 ====================================================================
1170 * Layouts reverse the common pattern of including shared headers and footers in many templates
1171 * to isolate changes in repeated setups. The inclusion pattern has pages that look like this:
1173 * <?php echo $controller->render('shared/header') ?>
1174 * Hello World
1175 * <?php echo $controller->render('shared/footer') ?>
1177 * This approach is a decent way of keeping common structures isolated from the
1178 * changing content, but it's verbose and if( you ever want to change the structure
1179 * of these two includes, you'll have to change all the templates.
1181 * With layouts, you can flip it around and have the common structure know where
1182 * to insert changing content. This means that the header and footer are only
1183 * mentioned in one place, like this:
1185 * <!-- The header part of this layout -->
1186 * <?php echo $content_for_layout ?>
1187 * <!-- The footer part of this layout -->
1189 * And then you have content pages that look like this:
1191 * hello world
1193 * Not a word about common structures. At rendering time, the content page is
1194 * computed and then inserted in the layout,
1195 * like this:
1197 * <!-- The header part of this layout -->
1198 * hello world
1199 * <!-- The footer part of this layout -->
1201 * == Accessing shared variables
1203 * Layouts have access to variables specified in the content pages and vice versa.
1204 * This allows you to have layouts with references that won't materialize before
1205 * rendering time:
1207 * <h1><?php echo $page_title ?></h1>
1208 * <?php echo $content_for_layout ?>
1210 * ...and content pages that fulfill these references _at_ rendering time:
1212 * <?php $page_title = 'Welcome'; ?>
1213 * Off-world colonies offers you a chance to start a new life
1215 * The result after rendering is:
1217 * <h1>Welcome</h1>
1218 * Off-world colonies offers you a chance to start a new life
1220 * == Automatic layout assignment
1222 * If there is a template in <tt>app/views/layouts/</tt> with the same name as
1223 * the current controller then it will be automatically
1224 * set as that controller's layout unless explicitly told otherwise. Say you have
1225 * a WeblogController, for example. If a template named <tt>app/views/layouts/weblog.tpl</tt>
1226 * exists then it will be automatically set as the layout for your WeblogController.
1227 * You can create a layout with the name <tt>application.tpl</tt>
1228 * and this will be set as the default controller if there is no layout with
1229 * the same name as the current controller and there is no layout explicitly
1230 * assigned on the +layout+ attribute. Setting a layout explicitly will always
1231 * override the automatic behaviour
1232 * for the controller where the layout is set. Explicitly setting the layout
1233 * in a parent class, though, will not override the
1234 * child class's layout assignement if the child class has a layout with the same name.
1236 * == Inheritance for layouts
1238 * Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
1240 * class BankController extends AkActionController
1242 * var $layout = 'bank_standard';
1245 * class InformationController extends BankController
1249 * class VaultController extends BankController
1251 * var $layout = 'access_level_layout';
1254 * class EmployeeController extends BankController
1256 * var $layout = null;
1259 * The InformationController uses 'bank_standard' inherited from the BankController, the VaultController
1260 * and picks the layout 'access_level_layout', and the EmployeeController doesn't want to use a layout at all.
1262 * == Types of layouts
1264 * Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
1265 * you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
1266 * be done either by an inline method.
1268 * The method reference is the preferred approach to variable layouts and is used like this:
1270 * class WeblogController extends AkActionController
1272 * function __construct()
1274 * $this->setLayout(array(&$this, '_writersAndReaders'));
1277 * function index()
1279 * // fetching posts
1282 * function _writersAndReaders()
1284 * return is_logged_in() ? 'writer_layout' : 'reader_layout';
1288 * Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
1289 * is logged in or not.
1291 * The most common way of specifying a layout is still just as a plain template name:
1293 * class WeblogController extends AkActionController
1295 * var $layout = 'weblog_standard';
1298 * If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
1300 * == Conditional layouts
1302 * If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
1303 * a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
1304 * <tt>only</tt> and <tt>except</tt> options can be passed to the layout call. For example:
1306 * class WeblogController extends AkActionController
1308 * function __construct()
1310 * $this->setLayout('weblog_standard', array('except' => 'rss'));
1313 * // ...
1317 * This will assign 'weblog_standard' as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
1318 * around the rendered view.
1320 * Both the <tt>only</tt> and <tt>except</tt> condition can accept an arbitrary number of method names, so
1321 * <tt>'except' => array('rss', 'text_only')</tt> is valid, as is <tt>'except' => 'rss'</tt>.
1323 * == Using a different layout in the action render call
1325 * If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
1326 * Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
1327 * This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
1328 * qualified template and layout names as this example shows:
1330 * class WeblogController extends AkActionController
1332 * function help()
1334 * $this->render(array('action'=>'help/index','layout'=>'help'));
1340 * If a layout is specified, all actions rendered through render and render_action will have their result assigned
1341 * to <tt>$this->content_for_layout</tt>, which can then be used by the layout to insert their contents with
1342 * <tt><?php echo $$this->content_for_layout ?></tt>. This layout can itself depend on instance variables assigned during action
1343 * performance and have access to them as any normal template would.
1345 function setLayout($template_name, $conditions = array())
1347 $this->_addLayoutConditions($conditions);
1348 $this->layout = $template_name;
1351 function getLayoutConditions()
1353 return empty($this->_layout_conditions) ? array() : $this->_layout_conditions;
1356 function _addLayoutConditions($conditions)
1358 $this->_layout_conditions = $conditions;
1364 * Returns the name of the active layout. If the layout was specified as a method reference, this method
1365 * is called and the return value is used. Likewise if( the layout was specified as an inline method (through a method
1366 * object). If the layout was defined without a directory, layouts is assumed. So <tt>setLayout('weblog/standard')</tt> will return
1367 * weblog/standard, but <tt>setLayout('standard')</tt> will return layouts/standard.
1369 function getActiveLayout($passed_layout = null)
1371 if(empty($passed_layout)){
1372 $layout = !isset($this->layout) ? AkInflector::underscore($this->getControllerName()) : $this->layout;
1373 }else{
1374 $layout =& $passed_layout;
1376 if(is_array($layout) && is_object($layout[0]) && method_exists($layout[0], $layout[1])){
1377 $this->active_layout = $layout[0]->{$layout[1]}();
1378 }elseif(method_exists($this,$layout) && strtolower(get_class($this)) !== strtolower($layout)){
1379 $this->active_layout = $this->$layout();
1380 }else{
1381 $this->active_layout = $layout;
1384 if(!empty($this->active_layout)){
1385 return strstr($this->active_layout,DS) ? $this->active_layout : 'layouts'.DS.$this->active_layout;
1390 function renderWithALayout($options = null, $status = null, $layout = null)
1392 $template_with_options = !empty($options) && is_array($options);
1394 if($this->_canApplyLayout($template_with_options, $options) && ($layout = $this->_pickLayout($template_with_options, $options, $layout))){
1396 $options = $template_with_options? array_merge((array)$options,array('layout'=>false)) : $options;
1398 $this->content_for_layout = $this->render($options, $status);
1400 if($template_with_options){
1401 $status = empty($options['status']) ? $status : $options['status'];
1404 $this->eraseRenderResults();
1405 $this->_addVariablesToAssigns();
1407 return $this->renderText($this->Template->renderFile($layout, true, &$this->_assigns), $status);
1408 }else{
1409 return $this->render($options, $status, &$this->_assigns);
1413 function _canApplyLayout($template_with_options, $options)
1415 return !empty($template_with_options) ? $this->_isCandidateForLayout($options) : !$this->_isTemplateExemptFromLayout();
1418 function _isCandidateForLayout($options)
1420 return !empty($options['layout']) ||
1421 (empty($options['text']) && empty($options['file']) && empty($options['inline']) && empty($options['partial']) && empty($options['nothing'])) &&
1422 !$this->_isTemplateExemptFromLayout($this->_getDefaultTemplateName(empty($options['action']) ? $options['template'] : $options['action']));
1425 function _pickLayout($template_with_options, $options, $layout = null)
1427 if(!empty($template_with_options)){
1428 $layout = empty($options['layout']) ? ($this->_doesActionHasLayout() ? $this->getActiveLayout(): false) : $this->getActiveLayout($options['layout']);
1429 }elseif(empty($layout) || $layout === true){
1430 $layout = $this->_doesActionHasLayout() ? $this->getActiveLayout() : false;
1432 if(!empty($layout)){
1433 $layout = strstr($layout,'/') || strstr($layout,DS) ? $layout : 'layouts'.DS.$layout;
1434 $layout = substr($layout,0,7) === 'layouts' ?
1435 (empty($this->_module_path) ? AK_VIEWS_DIR.DS.$layout.'.tpl' : AK_VIEWS_DIR.DS.'layouts'.DS.trim($this->_module_path, DS).'.tpl') :
1436 $layout.'.tpl';
1437 if (file_exists($layout)) {
1438 return $layout;
1440 $layout = null;
1442 if(empty($layout) && $layout !== false && defined('AK_DEFAULT_LAYOUT')){
1443 $layout = AK_VIEWS_DIR.DS.'layouts'.DS.AK_DEFAULT_LAYOUT.'.tpl';
1445 return file_exists($layout) ? $layout : false;
1448 function _doesActionHasLayout()
1450 $conditions = $this->getLayoutConditions();
1452 $action_name = $this->Request->getAction();
1453 if(!empty($conditions['only']) && ((is_array($conditions['only']) && in_array($action_name,$conditions['only'])) ||
1454 (is_string($conditions['only']) && $action_name == $conditions['only']))){
1455 return true;
1456 }elseif (!empty($conditions['only'])){
1457 return false;
1459 if(!empty($conditions['except']) && ((is_array($conditions['except']) && in_array($action_name,$conditions['except'])) ||
1460 (is_string($conditions['except']) && $action_name == $conditions['except']))){
1461 return false;
1464 return true;
1471 Filters
1472 ====================================================================
1474 * Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
1475 * authentication, caching, or auditing before the intended action is performed. Or to do localization or output
1476 * compression after the action has been performed.
1478 * Filters have access to the request, response, and all the instance variables set by other filters in the chain
1479 * or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>beforeFilter</tt>
1480 * to halt the processing before the intended action is processed by returning false or performing a redirect or render.
1481 * This is especially useful for filters like authentication where you're not interested in allowing the action to be
1482 * performed if the proper credentials are not in order.
1484 * == Filter inheritance
1486 * Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
1487 * affecting the superclass. For example:
1489 * class BankController extends AkActionController
1491 * function __construct()
1493 * $this->beforeFilter('_audit');
1496 * function _audit(&$controller)
1498 * // record the action and parameters in an audit log
1502 * class VaultController extends BankController
1504 * function __construct()
1506 * $this->beforeFilter('_verifyCredentials');
1509 * function _verifyCredentials(&$controller)
1511 * // make sure the user is allowed into the vault
1515 * Now any actions performed on the BankController will have the audit method called before. On the VaultController,
1516 * first the audit method is called, then the _verifyCredentials method. If the _audit method returns false, then
1517 * _verifyCredentials and the intended action are never called.
1519 * == Filter types
1521 * A filter can take one of three forms: method reference, external class, or inline method. The first
1522 * is the most common and works by referencing a method somewhere in the inheritance hierarchy of
1523 * the controller by use of a method name. In the bank example above, both BankController and VaultController use this form.
1525 * Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
1526 * are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
1528 * class OutputCompressionFilter
1530 * function filter(&$controller)
1532 * $controller->response->body = compress($controller->response->body);
1536 * class NewspaperController extends AkActionController
1538 * function __construct()
1540 * $this->afterFilter(new OutputCompressionFilter());
1544 * The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
1545 * manipulate them as it sees fit.
1548 * == Filter chain ordering
1550 * Using <tt>beforeFilter</tt> and <tt>afterFilter</tt> appends the specified filters to the existing chain. That's usually
1551 * just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
1552 * can use <tt>prependBeforeFilter</tt> and <tt>prependAfterFilter</tt>. Filters added by these methods will be put at the
1553 * beginning of their respective chain and executed before the rest. For example:
1555 * class ShoppingController extends AkActionController
1557 * function __construct()
1559 * $this->beforeFilter('verifyOpenShop');
1564 * class CheckoutController extends AkActionController
1566 * function __construct()
1568 * $this->prependBeforeFilter('ensureItemsInCart', 'ensureItemsInStock');
1572 * The filter chain for the CheckoutController is now <tt>ensureItemsInCart, ensureItemsInStock,</tt>
1573 * <tt>verifyOpenShop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
1574 * is open or not.
1576 * You may pass multiple filter arguments of each type.
1578 * == Around filters
1580 * In addition to the individual before and after filters, it's also possible to specify that a single object should handle
1581 * both the before and after call. That's especially useful when you need to keep state active between the before and after,
1582 * such as the example of a benchmark filter below:
1584 * class WeblogController extends AkActionController
1586 * function __construct()
1588 * $this->aroundFilter(new BenchmarkingFilter());
1591 * // Before this action is performed, BenchmarkingFilter->before($controller) is executed
1592 * function index()
1595 * // After this action has been performed, BenchmarkingFilter->after($controller) is executed
1598 * class BenchmarkingFilter
1600 * function before(&$controller)
1602 * start_timer();
1605 * function after(&$controller)
1607 * stop_timer();
1608 * report_result();
1612 * == Filter chain skipping
1614 * Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
1615 * subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
1616 * they would like to be relieved of. Examples
1618 * class ApplicationController extends AkActionController
1620 * function __construct()
1622 * $this->beforeFilter('authenticate');
1626 * class WeblogController extends ApplicationController
1628 * // will run the authenticate filter
1631 * class SignupController extends AkActionController
1633 * function __construct()
1635 * $this->skipBeforeFilter('authenticate');
1637 * // will not run the authenticate filter
1640 * == Filter conditions
1642 * Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
1643 * exclude or the actions to include when executing the filter. Available conditions are +only+ or +except+, both
1644 * of which accept an arbitrary number of method references. For example:
1646 * class Journal extends AkActionController
1648 * function __construct()
1649 * { // only require authentication if the current action is edit or delete
1650 * $this->beforeFilter(array('_authorize'=>array('only'=>array('edit','delete')));
1653 * function _authorize(&$controller)
1655 * // redirect to login unless authenticated
1660 var $_includedActions = array(), $_beforeFilters = array(), $_afterFilters = array(), $_excludedActions = array();
1662 * The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
1663 * on this controller are performed.
1665 function appendBeforeFilter()
1667 $filters = array_reverse(func_get_args());
1668 foreach (array_keys($filters) as $k){
1669 $conditions = $this->_extractConditions(&$filters[$k]);
1670 $this->_addActionConditions($filters[$k], $conditions);
1671 $this->_appendFilterToChain('before', $filters[$k]);
1676 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
1677 * on this controller are performed.
1679 function prependBeforeFilter()
1681 $filters = array_reverse(func_get_args());
1682 foreach (array_keys($filters) as $k){
1683 $conditions = $this->_extractConditions(&$filters[$k]);
1684 $this->_addActionConditions($filters[$k], $conditions);
1685 $this->_prependFilterToChain('before', $filters[$k]);
1690 * Short-hand for appendBeforeFilter since that's the most common of the two.
1692 function beforeFilter()
1694 $filters = func_get_args();
1695 foreach (array_keys($filters) as $k){
1696 $this->appendBeforeFilter($filters[$k]);
1701 * The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
1702 * on this controller are performed.
1704 function appendAfterFilter()
1706 $filters = array_reverse(func_get_args());
1707 foreach (array_keys($filters) as $k){
1708 $conditions = $this->_extractConditions(&$filters[$k]);
1709 $this->_addActionConditions(&$filters[$k], $conditions);
1710 $this->_appendFilterToChain('after', &$filters[$k]);
1716 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
1717 * on this controller are performed.
1719 function prependAfterFilter()
1721 $filters = array_reverse(func_get_args());
1722 foreach (array_keys($filters) as $k){
1723 $conditions = $this->_extractConditions(&$filters[$k]);
1724 $this->_addActionConditions(&$filters[$k], $conditions);
1725 $this->_prependFilterToChain('after', &$filters[$k]);
1730 * Short-hand for appendAfterFilter since that's the most common of the two.
1731 * */
1732 function afterFilter()
1734 $filters = func_get_args();
1735 foreach (array_keys($filters) as $k){
1736 $this->appendAfterFilter($filters[$k]);
1741 * The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
1742 * on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
1743 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like:
1745 * B::before()
1746 * A::before()
1747 * A::after()
1748 * B::after()
1750 function appendAroundFilter()
1752 $filters = func_get_args();
1753 foreach (array_keys($filters) as $k){
1754 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]);
1755 $this->appendBeforeFilter(array(&$filters[$k],'before'));
1757 $filters = array_reverse($filters);
1758 foreach (array_keys($filters) as $k){
1759 $this->prependAfterFilter(array(&$filters[$k],'after'));
1764 * The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
1765 * on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
1766 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like:
1768 * A::before()
1769 * B::before()
1770 * B::after()
1771 * A::after()
1773 function prependAroundFilter()
1775 $filters = func_get_args();
1776 foreach (array_keys($filters) as $k){
1777 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]);
1778 $this->prependBeforeFilter(array(&$filters[$k],'before'));
1780 $filters = array_reverse($filters);
1781 foreach (array_keys($filters) as $k){
1782 $this->appendAfterFilter(array(&$filters[$k],'after'));
1787 * Short-hand for appendAroundFilter since that's the most common of the two.
1789 function aroundFilter()
1791 $filters = func_get_args();
1792 call_user_func_array(array(&$this,'appendAroundFilter'), $filters);
1796 * Removes the specified filters from the +before+ filter chain.
1797 * This is especially useful for managing the chain in inheritance hierarchies where only one out
1798 * of many sub-controllers need a different hierarchy.
1800 function skipBeforeFilter($filters)
1802 $filters = func_get_args();
1803 $this->_skipFilter($filters, 'before');
1807 * Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
1808 * filters, not instances. This is especially useful for managing the chain in inheritance hierarchies where only one out
1809 * of many sub-controllers need a different hierarchy.
1811 function skipAfterFilter($filters)
1813 $filters = func_get_args();
1814 $this->_skipFilter($filters, 'after');
1817 function _skipFilter(&$filters, $type)
1819 $_filters =& $this->{'_'.$type.'Filters'};
1820 // array_diff doesn't play nice with some PHP5 releases when it comes to
1821 // Objects as it only diff equal references, not object types
1822 foreach (array_keys($filters) as $k){
1823 if(AK_PHP5){
1824 if(is_object($filters[$k])){
1825 foreach (array_keys($_filters) as $k2){
1826 if(is_object($_filters[$k2]) && get_class($_filters[$k2]) == get_class($filters[$k])){
1827 $pos = $k2;
1828 break;
1831 }else{
1832 $pos = array_search($filters[$k], $_filters);
1835 array_splice($_filters, $pos, 1, null);
1836 return ;
1838 $_filters = array_diff($_filters,array($filters[$k]));
1843 * Returns all the before filters for this class.
1845 function beforeFilters()
1847 return $this->_beforeFilters;
1851 * Returns all the after filters for this class and all its ancestors.
1853 function afterFilters()
1855 return $this->_afterFilters;
1859 * Returns a mapping between filters and the actions that may run them.
1861 function includedActions()
1863 return $this->_includedActions;
1867 * Returns a mapping between filters and actions that may not run them.
1869 function excludedActions()
1871 return $this->_excludedActions;
1875 function _appendFilterToChain($condition, $filters)
1877 $this->{"_{$condition}Filters"}[] =& $filters;
1880 function _prependFilterToChain($condition, $filters)
1882 array_unshift($this->{"_{$condition}Filters"}, $filters);
1885 function _ensureFilterRespondsToBeforeAndAfter(&$filter_object)
1887 if(!method_exists(&$filter_object,'before') && !method_exists(&$filter_object,'after')){
1888 trigger_error(Ak::t('Filter object must respond to both before and after'), E_USER_ERROR);
1892 function _extractConditions(&$filters)
1894 if(is_array($filters) && !isset($filters[0])){
1895 $keys = array_keys($filters);
1896 $conditions = $filters[$keys[0]];
1897 $filters = $keys[0];
1898 return $conditions;
1902 function _addActionConditions($filters, $conditions)
1904 if(!empty($conditions['only'])){
1905 $this->_includedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_includedActions, $conditions['only']);
1907 if(!empty($conditions['except'])){
1908 $this->_excludedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_excludedActions, $conditions['except']);
1912 function _conditionArray($actions, $filter_actions)
1914 $filter_actions = is_array($filter_actions) ? $filter_actions : array($filter_actions);
1915 $filter_actions = array_map(array(&$this,'_filterId'),$filter_actions);
1916 return array_unique(array_merge($actions, $filter_actions));
1920 function _filterId($filters)
1922 return is_string($filters) ? $filters : md5(serialize($filters));
1925 function performActionWithoutFilters($action)
1927 if(method_exists(&$this, $action)){
1928 call_user_func_array(array(&$this, $action), @(array)$this->passed_args);
1932 function performActionWithFilters($method = '')
1934 if ($this->beforeAction($method) !== false || empty($this->_performed)){
1935 $this->performActionWithoutFilters($method);
1936 $this->afterAction($method);
1937 return true;
1939 return $this->performActionWithoutFilters($method);
1942 function performAction($method = '')
1944 $this->performActionWithFilters($method);
1949 * Calls all the defined before-filter filters, which are added by using "beforeFilter($method)".
1950 * If any of the filters return false, no more filters will be executed and the action is aborted.
1952 function beforeAction($method = '')
1954 Ak::profile('Running before controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__);
1955 return $this->_callFilters($this->_beforeFilters, $method);
1959 * Calls all the defined after-filter filters, which are added by using "afterFilter($method)".
1960 * If any of the filters return false, no more filters will be executed.
1962 function afterAction($method = '')
1964 Ak::profile('Running after controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__);
1965 return $this->_callFilters(&$this->_afterFilters, $method);
1969 function _callFilters(&$filters, $method = '')
1971 $filter_result = null;
1972 foreach (array_keys($filters) as $k){
1973 $filter =& $filters[$k];
1974 if(!$this->_actionIsExempted($filter, $method)){
1975 if(is_array($filter) && is_object($filter[0]) && method_exists($filter[0], $filter[1])){
1976 $filter_result = $filter[0]->$filter[1]($this);
1977 }elseif(!is_object($filter) && method_exists($this, $filter)){
1978 $filter_result = $this->$filter($this);
1979 }elseif(is_object($filter) && method_exists($filter, 'filter')){
1980 $filter_result = $filter->filter($this);
1981 }else{
1982 trigger_error(Ak::t('Invalid filter %filter. Filters need to be a method name or a class implementing a static filter method', array('%filter'=>$filter)), E_USER_WARNING);
1986 if($filter_result === false){
1987 !empty($this->_Logger) ? $this->_Logger->info(Ak::t('Filter chain halted as '.$filter.' returned false')) : null;
1988 return false;
1991 return $filter_result;
1995 function _actionIsExempted($filter, $method = '')
1997 $method_id = is_string($method) ? $method : $this->_filterId($method);
1998 $filter_id = $this->_filterId($filter);
2000 if((!empty($this->_includedActions[$filter_id]) && !in_array($method_id, $this->_includedActions[$filter_id])) ||
2001 (!empty($this->_excludedActions[$filter_id]) && in_array($method_id, $this->_excludedActions[$filter_id]))){
2002 return true;
2005 return false;
2011 Flash communication between actions
2012 ====================================================================
2014 * The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
2015 * to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
2016 * that sets <tt>flash['notice] = 'Successfully created'</tt> before redirecting to a display action that can then expose
2017 * the flash to its template. Actually, that exposure is automatically done. Example:
2019 * class WeblogController extends ActionController
2021 * function create()
2023 * // save post
2024 * $this->flash['notice] = 'Successfully created post';
2025 * $this->redirectTo(array('action'=>'display','params' => array('id' =>$Post->id)));
2028 * function display()
2030 * // doesn't need to assign the flash notice to the template, that's done automatically
2034 * display.tpl
2035 * <?php if($flash['notice']) : ?><div class='notice'><?php echo $flash['notice'] ?></div><?php endif; ?>
2037 * This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
2038 * as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
2040 * ==flash_now
2042 * Sets a flash that will not be available to the next action, only to the current.
2044 * $this->flash_now['message] = 'Hello current action';
2046 * This method enables you to use the flash as a central messaging system in your app.
2047 * When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
2048 * When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
2049 * vanish when the current action is done.
2051 * Entries set via <tt>flash_now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
2053 var $flash = array();
2054 var $flash_now = array();
2055 var $flash_options = array();
2056 var $_flash_handled = false;
2058 function _handleFlashAttribute()
2060 $this->_flash_handled = true;
2062 $next_flash = empty($this->flash) ? false : $this->flash;
2063 $this->flash = array();
2064 if(isset($_SESSION['__flash'])){
2065 $this->flash = $_SESSION['__flash'];
2067 $_SESSION['__flash'] = $next_flash;
2068 if(!empty($this->flash_now)){
2069 $this->flash = array_merge((array)$this->flash,(array)$this->flash_now);
2071 $this->_handleFlashOptions();
2074 function _handleFlashOptions()
2076 $next_flash_options = empty($this->flash_options) ? false : $this->flash_options;
2077 $this->flash_options = array();
2078 if(isset($_SESSION['__flash_options'])){
2079 $this->flash_options = $_SESSION['__flash_options'];
2081 $_SESSION['__flash_options'] = $next_flash_options;
2082 if(!empty($this->flash_now_options)){
2083 $this->flash_options = array_merge((array)$this->flash_options,(array)$this->flash_now_options);
2088 function _mergeFlashOnFlashNow()
2090 $this->flash_now = array_merge($this->flash_now,$this->flash);
2095 Pagination for Active Record collections
2096 ====================================================================
2098 * The Pagination module aids in the process of paging large collections of
2099 * Active Record objects. It offers macro-style automatic fetching of your
2100 * model for multiple views, or explicit fetching for single actions. And if
2101 * the magic isn't flexible enough for your needs, you can create your own
2102 * paginators with a minimal amount of code.
2104 * The Pagination module can handle as much or as little as you wish. In the
2105 * controller, have it automatically query your model for pagination; or,
2106 * if you prefer, create Paginator objects yourself
2108 * Pagination is included automatically for all controllers.
2110 * For help rendering pagination links, see
2111 * Helpers/PaginationHelper.
2113 * ==== Automatic pagination for every action in a controller
2115 * class PersonController extends ApplicationController
2117 * var $model = 'person';
2118 * var $paginate = array('people'=>array('order' => 'last_name, first_name',
2119 * 'per_page' => 20));
2122 * Each action in this controller now has access to a <tt>$this->people</tt>
2123 * instance variable, which is an ordered collection of model objects for the
2124 * current page (at most 20, sorted by last name and first name), and a
2125 * <tt>$this->person_pages</tt> Paginator instance. The current page is determined
2126 * by the <tt>$params['page']</tt> variable.
2128 * ==== Pagination for a single action
2130 * function show_all()
2132 * list($this->person_pages, $this->people) =
2133 * $this->paginate('people', array('order' => 'last_name, first_name'));
2136 * Like the previous example, but explicitly creates <tt>$this->person_pages</tt>
2137 * and <tt>$this->people</tt> for a single action, and uses the default of 10 items
2138 * per page.
2140 * ==== Custom/"classic" pagination
2142 * function list()
2144 * $this->person_pages = new AkPaginator(&$this, $Person->count(), 10, $params['page']);
2145 * $this->people = $this->Person->find('all', array(
2146 * 'order'=> 'last_name, first_name',
2147 * 'limit' => $this->person_pages->items_per_page,
2148 * 'offset' => $this->person_pages->getOffset()));
2151 * Explicitly creates the paginator from the previous example and uses
2152 * AkPaginator::toSql to retrieve <tt>$this->people</tt> from the model.
2154 // An array holding options for controllers using macro-style pagination
2155 var $_pagination_options = array(
2156 'class_name' => null,
2157 'singular_name' => null,
2158 'per_page' => 10,
2159 'conditions' => null,
2160 'order_by' => null,
2161 'order' => null,
2162 'join' => null,
2163 'joins' => null,
2164 'include' => null,
2165 'select' => null,
2166 'parameter' => 'page'
2169 // The default options for pagination
2170 var $_pagination_default_options = array(
2171 'class_name' => null,
2172 'singular_name' => null,
2173 'per_page' => 10,
2174 'conditions' => null,
2175 'order_by' => null,
2176 'order' => null,
2177 'join' => null,
2178 'joins' => null,
2179 'include' => null,
2180 'select' => null,
2181 'parameter' => 'page'
2184 var $_pagination_actions = array();
2186 function _paginationValidateOptions($collection_id, $options = array(), $in_action)
2188 $this->_pagination_options = array_merge($this->_pagination_default_options, $this->_pagination_options);
2189 $valid_options = array_keys($this->_pagination_default_options);
2191 $valid_options = !in_array($in_action, $valid_options) ? array_merge($valid_options, $this->_pagination_actions) : $valid_options;
2193 $unknown_option_keys = array_diff(array_keys($this->_pagination_options) , $valid_options);
2195 if(!empty($unknown_option_keys)){
2196 trigger_error(Ak::t('Unknown options for pagination: %unknown_option',array('%unknown_option'=>join(', ',$unknown_option_keys))), E_USER_WARNING);
2199 $this->_pagination_options['singular_name'] = !empty($this->_pagination_options['singular_name']) ? $this->_pagination_options['singular_name'] : AkInflector::singularize($collection_id);
2200 $this->_pagination_options['class_name'] = !empty($this->_pagination_options['class_name']) ? $this->_pagination_options['class_name'] : AkInflector::camelize($this->_pagination_options['singular_name']);
2204 * Returns a paginator and a collection of Active Record model instances
2205 * for the paginator's current page. This is designed to be used in a
2206 * single action.
2208 * +options+ are:
2209 * <tt>singular_name</tt>:: the singular name to use, if it can't be inferred by
2210 * singularizing the collection name
2211 * <tt>class_name</tt>:: the class name to use, if it can't be inferred by
2212 * camelizing the singular name
2213 * <tt>per_page</tt>:: the maximum number of items to include in a
2214 * single page. Defaults to 10
2215 * <tt>conditions</tt>:: optional conditions passed to Model::find('all', $this->params); and
2216 * Model::count()
2217 * <tt>order</tt>:: optional order parameter passed to Model::find('all', $this->params);
2218 * <tt>order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model::find('all', $this->params)
2219 * <tt>joins</tt>:: optional joins parameter passed to Model::find('all', $this->params)
2220 * and Model::count()
2221 * <tt>join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model::find('all', $this->params)
2222 * and Model::count()
2223 * <tt>include</tt>:: optional eager loading parameter passed to Model::find('all', $this->params)
2224 * and Model::count()
2226 * Creates a +before_filter+ which automatically paginates an Active
2227 * Record model for all actions in a controller (or certain actions if
2228 * specified with the <tt>actions</tt> option).
2230 * +options+ are the same as PaginationHelper::paginate, with the addition
2231 * of:
2232 * <tt>actions</tt>:: an array of actions for which the pagination is
2233 * active. Defaults to +null+ (i.e., every action)
2235 function paginate($collection_id, $options = array())
2237 $this->_paginationValidateOptions($collection_id, $options, true);
2238 $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options);
2239 $this->beforeFilter('_paginationCreateAndRetrieveCollections');
2243 function _paginationCreateAndRetrieveCollections()
2245 foreach($this->_pagination_options[$this->class] as $collection_id=>$options){
2246 if(!empty($options['actions']) && in_array($options['actions'], $action_name)){
2247 continue;
2250 list($paginator, $collection) = $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options);
2252 $this->{$options['singular_name'].'_pages'} =& $paginator;
2253 $this->$collection_name =& $collection;
2258 * Returns the total number of items in the collection to be paginated for
2259 * the +model+ and given +conditions+. Override this method to implement a
2260 * custom counter.
2262 function _paginationCountCollection(&$model, $conditions, $joins)
2264 return $model->count($conditions, $joins);
2268 * Returns a collection of items for the given +$model+ and +$options['conditions']+,
2269 * ordered by +$options['order']+, for the current page in the given +$paginator+.
2270 * Override this method to implement a custom finder.
2272 function _paginationFindCollection(&$model, $options, &$paginator)
2274 return $model->find('all', array(
2275 'conditions' => $this->_pagination_options['conditions'],
2276 'order' => !empty($options['order_by']) ? $options['order_by'] : $options['order'],
2277 'joins' => !empty($options['join']) ? $options['join'] : $options['joins'],
2278 'include' => $this->_pagination_options['include'],
2279 'select' => $this->_pagination_options['select'],
2280 'limit' => $this->_pagination_options['per_page'],
2281 'offset' => $paginator->getOffset()));
2285 * @todo Fix this function
2287 function _paginationLoadPaginatorAndCollection($collection_id, $options)
2289 $page = $this->params[$options['parameter']];
2290 $count = $this->_paginationCountCollection($klass, $options['conditions'],
2291 empty($options['join']) ? $options['join'] : $options['joins']);
2293 require_once(AK_LIB_DIR.DS.'AkActionController'.DS.'AkPaginator.php');
2294 $paginator =& new AkPaginator($this, $count, $options['per_page'], $page);
2295 $collection =& $this->_paginationFindCollection($options['class_name'], $options, $paginator);
2297 return array(&$paginator, &$collection);
2301 Protocol conformance
2302 ====================================================================
2306 * Specifies that the named actions requires an SSL connection to be performed (which is enforced by ensure_proper_protocol).
2308 function setSslRequiredActions($actions)
2310 $this->_ssl_required_actions = empty($this->_ssl_required_actions) ?
2311 (is_string($actions) ? Ak::stringToArray($actions) : $actions) :
2312 array_merge($this->_ssl_required_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions));
2315 function setSslAllowedActions($actions)
2317 $this->_ssl_allowed_actions = empty($this->_ssl_allowed_actions) ?
2318 (is_string($actions) ? Ak::stringToArray($actions) : $actions) :
2319 array_merge($this->_ssl_allowed_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions));
2323 * Returns true if the current action is supposed to run as SSL
2325 function _isSslRequired()
2327 return !empty($this->_ssl_required_actions) && is_array($this->_ssl_required_actions) && isset($this->_action_name) ?
2328 in_array($this->_action_name, $this->_ssl_required_actions) : false;
2331 function _isSslAllowed()
2333 return (!empty($this->ssl_for_all_actions) && empty($this->_ssl_allowed_actions)) ||
2334 (!empty($this->_ssl_allowed_actions) && is_array($this->_ssl_allowed_actions) && isset($this->_action_name) ?
2335 in_array($this->_action_name, $this->_ssl_allowed_actions) : false);
2338 function _ensureProperProtocol()
2340 if($this->_isSslAllowed()){
2341 return true;
2343 if ($this->_isSslRequired() && !$this->Request->isSsl()){
2344 $this->redirectTo(substr_replace(AK_CURRENT_URL,'s:',4,1));
2345 return false;
2346 }elseif($this->Request->isSsl() && !$this->_isSslRequired()){
2347 $this->redirectTo(substr_replace(AK_CURRENT_URL,'',4,1));
2348 return false;
2353 Account Location
2354 ====================================================================
2356 * Account location is a set of methods that supports the account-key-as-subdomain
2357 * way of identifying the current scope. These methods allow you to easily produce URLs that
2358 * match this style and to get the current account key from the subdomain.
2360 * The methods are: getAccountUrl, getAccountHost, and getAccountDomain.
2362 * Example:
2364 * include_once('AkAccountLocation.php');
2366 * class ApplicationController extends AkActionController
2368 * var $before_filter = '_findAccount';
2370 * function _findAccount()
2372 * $this->account = Account::find(array('conditions'=>array('username = ?', $this->account_domain)));
2375 * class AccountController extends ApplicationController
2377 * function new_account()
2379 * $this->new_account = Account::create($this->params['new_account']);
2380 * $this->redirectTo(array('host' => $this->accountHost($this->new_account->username), 'controller' => 'weblog'));
2383 * function authenticate()
2385 * $this->session[$this->account_domain] = 'authenticated';
2386 * $this->redirectTo(array('controller => 'weblog'));
2389 * function _isAuthenticated()
2391 * return !empty($this->session['account_domain']) ? $this->session['account_domain'] == 'authenticated' : false;
2395 * // The view:
2396 * Your domain: {account_url?}
2398 * By default, all the methods will query for $this->account->username as the account key, but you can
2399 * specialize that by overwriting defaultAccountSubdomain. You can of course also pass it in
2400 * as the first argument to all the methods.
2402 function defaultAccountSubdomain()
2404 if(!empty($this->account)){
2405 return $this->account->respondsTo('username');
2409 function accountUrl($account_subdomain = null, $use_ssl = null)
2411 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain;
2412 $use_ssl = empty($use_ssl) ? $use_ssl : $this->Request->isSsl();
2413 return ($use_ssl ? 'https://' : 'http://') . $this->accountHost($account_subdomain);
2416 function accountHost($account_subdomain = null)
2418 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain;
2419 $account_host = '';
2420 $account_host .= $account_subdomain . '.';
2421 $account_host .= $this->accountDomain();
2422 return $account_host;
2425 function accountDomain()
2427 $account_domain = '';
2428 if(count($this->Request->getSubdomains()) > 1){
2429 $account_domain .= join('.',$this->Request->getSubdomains()) . '.';
2431 $account_domain .= $this->Request->getDomain() . $this->Request->getPortString();
2432 return $account_domain;
2435 function getAccountSubdomain()
2437 return array_shift($this->Request->getSubdomains());
2442 Data streaming
2443 ====================================================================
2444 Methods for sending files and streams to the browser instead of rendering.
2447 var $default_send_file_options = array(
2448 'type' => 'application/octet-stream',
2449 'disposition' => 'attachment',
2450 'stream' => true,
2451 'buffer_size' => 4096
2455 * Sends the file by streaming it 4096 bytes at a time. This way the
2456 * whole file doesn't need to be read into memory at once. This makes
2457 * it feasible to send even large files.
2459 * Be careful to sanitize the path parameter if it coming from a web
2460 * page. sendFile($params['path']) allows a malicious user to
2461 * download any file on your server.
2463 * Options:
2464 * * <tt>filename</tt> - suggests a filename for the browser to use.
2465 * Defaults to realpath($path).
2466 * * <tt>type</tt> - specifies an HTTP content type.
2467 * Defaults to 'application/octet-stream'.
2468 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded.
2469 * Valid values are 'inline' and 'attachment' (default).
2470 * * <tt>stream</tt> - whether to send the file to the user agent as it is read (true)
2471 * or to read the entire file before sending (false). Defaults to true.
2472 * * <tt>buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
2473 * Defaults to 4096.
2475 * The default Content-Type and Content-Disposition headers are
2476 * set to download arbitrary binary files in as many browsers as
2477 * possible. IE versions 4, 5, 5.5, and 6 are all known to have
2478 * a variety of quirks (especially when downloading over SSL).
2480 * Simple download:
2481 * sendFile('/path/to.zip');
2483 * Show a JPEG in browser:
2484 * sendFile('/path/to.jpeg', array('type' => 'image/jpeg', 'disposition' => 'inline'));
2486 * Read about the other Content-* HTTP headers if you'd like to
2487 * provide the user with more information (such as Content-Description).
2488 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
2490 * Also be aware that the document may be cached by proxies and browsers.
2491 * The Pragma and Cache-Control headers declare how the file may be cached
2492 * by intermediaries. They default to require clients to validate with
2493 * the server before releasing cached responses. See
2494 * http://www.mnot.net/cache_docs/ for an overview of web caching and
2495 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
2496 * for the Cache-Control header spec.
2498 function sendFile($path, $options = array())
2500 $path = realpath($path);
2501 if(!file_exists($path)){
2502 trigger_error(Ak::t('Cannot read file %path',array('%path'=>$path)), E_USER_NOTICE);
2503 return false;
2505 $options['length'] = empty($options['length']) ? filesize($path) : $options['length'];
2506 $options['filename'] = empty($options['filename']) ? basename($path) : $options['filename'];
2507 $options['type'] = empty($options['type']) ? Ak::mime_content_type($path) : $options['type'];
2509 $this->performed_render = false;
2510 $this->_sendFileHeaders($options);
2512 if(!empty($options['stream'])){
2513 require_once(AK_LIB_DIR.DS.'AkStream.php');
2514 $this->render(array('text'=> new AkStream($path,$options['buffer_size'])));
2515 }else{
2516 $this->render(array('text'=> Ak::file_get_contents($path)));
2521 * Send binary data to the user as a file download. May set content type, apparent file name,
2522 * and specify whether to show data inline or download as an attachment.
2524 * Options:
2525 * * <tt>filename</tt> - Suggests a filename for the browser to use.
2526 * * <tt>type</tt> - specifies an HTTP content type.
2527 * Defaults to 'application/octet-stream'.
2528 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded.
2529 * Valid values are 'inline' and 'attachment' (default).
2531 * Generic data download:
2532 * sendData($buffer)
2534 * Download a dynamically-generated tarball:
2535 * sendData(Ak::compress('dir','tgz'), array('filename' => 'dir.tgz'));
2537 * Display an image Active Record in the browser:
2538 * sendData($image_data, array('type' =>Ak::mime_content_type('image_name.png'), 'disposition' => 'inline'));
2540 * See +sendFile+ for more information on HTTP Content-* headers and caching.
2542 function sendData($data, $options = array())
2544 $options['length'] = empty($options['length']) ? Ak::size($data) : $options['length'];
2545 $this->_sendFileHeaders($options);
2546 $this->performed_render = false;
2547 $this->renderText($data);
2551 * Creates a file for streaming from a file.
2552 * This way you might free memory usage is file is too large
2554 function sendDataAsStream($data, $options)
2556 $temp_file_name = tempnam(AK_TMP_DIR, Ak::randomString());
2557 $fp = fopen($temp_file_name, 'w');
2558 fwrite($fp, $data);
2559 fclose($fp);
2560 $this->sendFile($temp_file_name, $options);
2564 function _sendFileHeaders(&$options)
2566 $options = array_merge($this->default_send_file_options,$options);
2567 foreach (array('length', 'type', 'disposition') as $arg){
2568 empty($options[$arg]) ? trigger_error(Ak::t('%arg option required', array('%arg'=>$arg)), E_USER_ERROR) : null;
2570 $disposition = empty($options['disposition']) ? 'attachment' : $options['disposition'];
2571 $disposition .= !empty($options['filename']) ? '; filename="'.$options['filename'].'"' : '';
2572 $this->Response->addHeader(array(
2573 'Content-Length' => $options['length'],
2574 'Content-Type' => trim($options['type']), // fixes a problem with extra '\r' with some browsers
2575 'Content-Disposition' => $disposition,
2576 'Content-Transfer-Encoding' => 'binary'
2582 function redirectToLocale($locale)
2584 if($this->Request->__internationalization_support_enabled){
2585 $lang = isset($this->params['lang']) ? $this->params['lang'] : $locale;
2587 if($locale != $lang){
2588 $this->redirectTo(array_merge($this->Request->getParams(),array('lang'=>$locale)));
2589 return true;
2592 return false;
2596 function api($protocol = 'xml_rpc')
2598 $web_services = array_merge(Ak::toArray($this->web_services), Ak::toArray($this->web_service));
2599 if(!empty($web_services)){
2600 $web_services = array_unique($web_services);
2601 require_once(AK_LIB_DIR.DS.'AkActionWebService.php');
2602 require_once(AK_LIB_DIR.DS.'AkActionWebService'.DS.'AkActionWebServiceServer.php');
2603 $Server =& new AkActionWebServiceServer($protocol);
2604 foreach ($web_services as $web_service){
2605 $Server->addService($web_service);
2607 $Server->init();
2608 $Server->serve();
2609 exit;
2610 }else{
2611 die(Ak::t('There is not any webservice configured at this endpoint'));
2618 HTTP Authentication
2619 ====================================================================
2621 * Simple Basic example:
2623 * class PostsController extends ApplicationController
2625 * var $_authorized_users = array('bermi' => 'secret');
2627 * function __construct(){
2628 * $this->beforeFilter(array('authenticate' => array('except' => array('index'))));
2631 * function index() {
2632 * $this->renderText("Everyone can see me!");
2635 * function edit(){
2636 * $this->renderText("I'm only accessible if you know the password");
2639 * function authenticate(){
2640 * return $this->_authenticateOrRequestWithHttpBasic('App name', $this->_authorized_users);
2644 * Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
2645 * the regular HTML interface is protected by a session approach:
2647 * class ApplicationController extends AkActionController
2649 * var $models = 'account';
2651 * function __construct() {
2652 * $this->beforeFilter(array('_setAccount', 'authenticate'));
2655 * function _setAccount() {
2656 * $this->Account = $this->account->findFirstBy('url_name', array_pop($this->Request->getSubdomains()));
2659 * function authenticate() {
2660 * if($this->Request->isFormat('XML', 'ATOM')){
2661 * if($User = $this->_authenticateWithHttpBasic($Account)){
2662 * $this->CurrentUser = $User;
2663 * }else{
2664 * $this->_requestHttpBasicAuthentication();
2666 * }else{
2667 * if($this->isSessionAuthenticated()){
2668 * $this->CurrentUser = $Account->user->find($_SESSION['authenticated']['user_id']);
2669 * }else{
2670 * $this->redirectTo(array('controller'=>'login'));
2671 * return false;
2677 * On shared hosts, Apache sometimes doesn't pass authentication headers to
2678 * FCGI instances. If your environment matches this description and you cannot
2679 * authenticate, try this rule in public/.htaccess (replace the plain one):
2681 * RewriteRule ^(.*)$ index.php [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
2684 function _authenticateOrRequestWithHttpBasic($realm = AK_APP_NAME, $login_procedure)
2686 if($Result = $this->_authenticateWithHttpBasic($login_procedure)){
2687 return $Result;
2689 return $this->_requestHttpBasicAuthentication($realm);
2692 function _authenticateWithHttpBasic($login_procedure)
2694 return $this->_authenticate($login_procedure);
2697 function _requestHttpBasicAuthentication($realm = AK_APP_NAME)
2699 return $this->_authenticationRequest($realm);
2703 * This is method takes a $login_procedure for performing access authentication.
2705 * If an array is given, it will check the key for a user and the value will be verified to match given password.
2707 * You can pass and array like array('handler' => $Account, 'method' => 'verifyCredentials'), which will call
2709 * $Account->verifyCredentials($user_name, $password, $Controller)
2711 * You can also pass an object which implements an "authenticate" method. when calling
2713 * $this->_authenticate(new User());
2715 * It will call the $User->authenticate($user_name, $password, $Controller)
2717 * In both cases the authentication method should return true for valid credentials or false is invalid.
2719 * @return bool
2721 function _authenticate($login_procedure)
2723 if(!$this->_authorization()){
2724 return false;
2725 }else{
2726 list($user_name, $password) = $this->_getUserNameAndPassword();
2727 if(is_array($login_procedure)){
2728 if(!isset($login_procedure['handler'])){
2729 return isset($login_procedure[$user_name]) && $login_procedure[$user_name] == $password;
2730 }elseif(is_a($login_procedure['handler']) && method_exists($login_procedure['handler'], $login_procedure['method'])){
2731 return $login_procedure['handler']->$login_procedure['method']($user_name, $password, $this);
2733 }elseif(method_exists($login_procedure, 'authenticate')){
2734 return $login_procedure->authenticate($user_name, $password, $this);
2737 return false;
2740 function _getUserNameAndPassword()
2742 $credentials = $this->_decodeCredentials();
2743 return !is_array($credentials) ? split('/:/', $credentials , 2) : $credentials;
2746 function _authorization()
2748 return
2749 empty($this->Request->env['PHP_AUTH_USER']) ? (
2750 empty($this->Request->env['HTTP_AUTHORIZATION']) ? (
2751 empty($this->Request->env['X-HTTP_AUTHORIZATION']) ? (
2752 empty($this->Request->env['X_HTTP_AUTHORIZATION']) ? (
2753 isset($this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION']) ?
2754 $this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION'] : null
2755 ) : $this->Request->env['X_HTTP_AUTHORIZATION']
2756 ) : $this->Request->env['X-HTTP_AUTHORIZATION']
2757 ) : $this->Request->env['HTTP_AUTHORIZATION']
2758 ) : array($this->Request->env['PHP_AUTH_USER'], $this->Request->env['PHP_AUTH_PW']);
2761 function _decodeCredentials()
2763 $authorization = $this->_authorization();
2764 if(is_array($authorization)){
2765 return $authorization;
2767 $credentials = (array)split(' ', $authorization);
2768 return base64_decode(array_pop($credentials));
2771 function _encodeCredentials($user_name, $password)
2773 return 'Basic '.base64_encode("$user_name:$password");
2776 function _authenticationRequest($realm)
2778 header('WWW-Authenticate: Basic realm="' . str_replace('"','',$realm) . '"');
2780 if(method_exists($this, 'access_denied')){
2781 $this->access_denied();
2782 }else{
2783 header('HTTP/1.0 401 Unauthorized');
2784 echo "HTTP Basic: Access denied.\n";
2785 exit;
2792 * Function for getting the singleton controller;
2794 * @return AkActionController instance
2796 function &AkActionController()
2798 $params = func_num_args() == 0 ? null : func_get_args();
2799 $AkActionController =& Ak::singleton('AkActionController', $params);
2800 return $AkActionController;