returning false or invoking a render in a before-filter halts the execution chain...
[akelos.git] / lib / AkActionController.php
blob6d0741af0ca0e3bc0319fe668187ca16380793e2
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 require_once(AK_LIB_DIR.DS.'AkObject.php');
13 defined('AK_HIGH_LOAD_MODE') ? null : define('AK_HIGH_LOAD_MODE', false);
14 defined('AK_APP_NAME') ? null : define('AK_APP_NAME', 'Application');
16 /**
17 * @package ActionController
18 * @subpackage Base
19 * @author Bermi Ferrer <bermi a.t akelos c.om>
20 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
21 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
24 class AkActionController extends AkObject
26 var $_high_load_mode = AK_HIGH_LOAD_MODE;
27 var $_enable_plugins = true;
28 var $_auto_instantiate_models = true;
29 var $validate_output = false;
31 var $_ssl_requirement = false;
32 var $_ssl_allowed_actions = array();
33 var $ssl_for_all_actions = true;
35 /**
36 * Determines whether the view has access to controller internals $this->Request, $this->Response, $this->session, and $this->Template.
37 * By default, it does.
39 var $_view_controller_internals = true;
41 /**
42 * Protected instance variable cache
44 var $_protected_variables_cache = array();
46 /**
47 * Prepends all the URL-generating helpers from AssetHelper.
48 * This makes it possible to easily move javascripts, stylesheets,
49 * and images to a dedicated asset server away from the main web server.
50 * Example:
51 * $this->_asset_host = 'http://assets.example.com';
53 var $asset_host = AK_ASSET_HOST;
56 var $_Logger;
58 /**
59 * Determines which template class should be used by AkActionController.
61 var $TemplateClass;
63 /**
64 * Turn on +_ignore_missing_templates+ if you want to unit test actions without
65 * making the associated templates.
67 var $_ignore_missing_templates;
69 /**
70 * Holds the Request object that's primarily used to get environment variables
72 var $Request;
74 /**
75 * Holds an array of all the GET, POST, and Url parameters passed to the action.
76 * Accessed like <tt>$this->params['post_id'];</tt>
77 * to get the post_id.
79 var $params = array();
81 /**
82 * Holds the Response object that's primarily used to set additional HTTP _headers
83 * through access like <tt>$this->Response->_headers['Cache-Control'] = 'no-cache';</tt>.
84 * Can also be used to access the final body HTML after a template
85 * has been rendered through $this->Response->body -- useful for <tt>after_filter</tt>s
86 * that wants to manipulate the output, such as a OutputCompressionFilter.
88 var $Response;
90 /**
91 * Holds an array of objects in the session. Accessed like <tt>$this->session['person']</tt>
92 * to get the object tied to the 'person' key. The session will hold any type of object
93 * as values, but the key should be a string.
95 var $session;
97 /**
98 * Holds an array of header names and values. Accessed like <tt>$this->_headers['Cache-Control']</tt>
99 * to get the value of the Cache-Control directive. Values should always be specified as strings.
101 var $_headers = array();
104 * Holds the array of variables that are passed on to the template class to be
105 * made available to the view. This array is generated by taking a snapshot of
106 * all the instance variables in the current scope just before a template is rendered.
108 var $_assigns = array();
111 * Holds the name of the action this controller is processing.
113 var $_action_name;
115 var $cookies;
117 var $helpers = 'default';
119 var $app_helpers;
120 var $plugin_helpers = 'all';
122 var $web_service;
123 var $web_services = array();
125 var $web_service_api;
126 var $web_service_apis = array();
128 var $module_name;
129 var $_module_path;
132 * Old fashioned way of dispatching requests. Please use AkDispatcher or roll your own.
134 * @deprecated
136 function handleRequest()
138 AK_LOG_EVENTS && empty($this->_Logger) ? ($this->_Logger =& Ak::getLogger()) : null;
139 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->warning('Using deprecated request dispatcher AkActionController::handleRequest. Use to AkDispatcher + AkDispatcher::dispatch instead.') : null;
140 require_once(AK_LIB_DIR.DS.'AkDispatcher.php');
141 $Dispatcher =& new AkDispatcher();
142 $Dispatcher->dispatch();
145 function process(&$Request, &$Response)
147 AK_LOG_EVENTS && empty($this->_Logger) ? ($this->_Logger =& Ak::getLogger()) : null;
149 $this->Request =& $Request;
150 $this->Response =& $Response;
151 $this->params = $this->Request->getParams();
152 $this->_action_name = $this->Request->getAction();
154 $this->_ensureActionExists();
156 Ak::t('Akelos'); // We need to get locales ready
158 if($this->_high_load_mode !== true){
159 if(!empty($this->_auto_instantiate_models)){
160 $this->instantiateIncludedModelClasses();
162 if(!empty($this->_enable_plugins)){
163 $this->loadPlugins();
165 if(!empty($this->helpers)){
166 $this->instantiateHelpers();
168 }else{
169 $this->_enableLayoutOnRender = false;
172 $this->_ensureProperProtocol();
174 // After filters
175 $this->afterFilter('_handleFlashAttribute');
177 $this->_loadActionView();
179 if(isset($this->api)){
180 require_once(AK_LIB_DIR.DS.'AkActionWebService.php');
181 $this->aroundFilter(new AkActionWebService($this));
184 $this->performActionWithFilters($this->_action_name);
186 if (!$this->_hasPerformed()){
187 $this->_enableLayoutOnRender ? $this->renderWithLayout() : $this->renderWithoutLayout();
190 if(!empty($this->validate_output)){
191 $this->_validateGeneratedXhtml();
194 $this->Response->outputResults();
197 function _loadActionView()
199 empty($this->_assigns) ? ($this->_assigns = array()) : null;
200 empty($this->_default_render_status_code) ? ($this->_default_render_status_code = 200) : null;
201 $this->_enableLayoutOnRender = !isset($this->_enableLayoutOnRender) ? true : $this->_enableLayoutOnRender;
202 $this->passed_args = !isset($this->Request->pass)? array() : $this->Request->pass;
203 empty($this->cookies) && isset($_COOKIE) ? ($this->cookies =& $_COOKIE) : null;
205 if(empty($this->Template)){
206 require_once(AK_LIB_DIR.DS.'AkActionView.php');
207 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'AkPhpTemplateHandler.php');
208 $this->Template =& new AkActionView($this->_getTemplateBasePath(),
209 $this->Request->getParameters(),$this->Request->getController());
211 $this->Template->_controllerInstance =& $this;
212 $this->Template->_registerTemplateHandler('tpl','AkPhpTemplateHandler');
216 function loadPlugins()
218 Ak::loadPlugins();
222 * Creates an instance of each available helper and links it into into current controller.
224 * Per example, if a helper TextHelper is located into the file text_helper.php.
225 * An instance is created on current controller
226 * at $this->text_helper. This instance is also available on the view by calling $text_helper.
228 * Helpers can be found at lib/AkActionView/helpers (this might change in a future)
230 function instantiateHelpers()
232 $helpers = $this->getDefaultHelpers();
233 $helpers = array_merge($helpers, $this->getApplicationHelpers());
234 $helpers = array_merge($helpers, $this->getPluginHelpers());
235 $helpers = array_merge($helpers, $this->getModuleHelper());
236 $helpers = array_merge($helpers, $this->getCurrentControllerHelper());
238 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'AkActionViewHelper.php');
240 $available_helpers = array();
241 foreach ($helpers as $file=>$helper){
242 $helper_class_name = strstr($helper, 'Helper') ? $helper : $helper.'Helper';
243 $full_path = preg_match('/[\\\\\/]+/',$file);
244 $file_path = $full_path ? $file : AK_LIB_DIR.DS.'AkActionView'.DS.'helpers'.DS.$file;
245 include_once($file_path);
246 if(class_exists($helper_class_name)){
247 $attribute_name = $full_path ? AkInflector::underscore($helper_class_name) : substr($file,0,-4);
248 $available_helpers[] = $attribute_name;
249 $this->$attribute_name =& new $helper_class_name(&$this);
250 if(method_exists($this->$attribute_name,'setController')){
251 $this->$attribute_name->setController(&$this);
253 if(method_exists($this->$attribute_name,'init')){
254 $this->$attribute_name->init();
258 defined('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS') ? null : define('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS', join(',',$available_helpers));
261 function getCurrentControllerHelper()
263 $helper = $this->getControllerName();
264 $helper = AkInflector::is_plural($helper)?AkInflector::singularize($helper):$helper;
265 $helper_file_name = AK_HELPERS_DIR.DS.$this->_module_path.AkInflector::underscore($helper).'_helper.php';
267 if(file_exists($helper_file_name)){
268 return array($helper_file_name => $helper);
270 return array();
273 function getModuleHelper()
275 $this->getControllerName(); // module name is set when we first retrieve the controller name
276 if(!empty($this->module_name)){
277 $helper_file_name = AK_HELPERS_DIR.DS.AkInflector::underscore($this->module_name).'_helper.php';
278 if(file_exists($helper_file_name)){
279 return array($helper_file_name => $this->module_name);
282 return array();
285 function getDefaultHelpers()
287 if($this->helpers == 'default'){
288 $available_helpers = Ak::dir(AK_LIB_DIR.DS.'AkActionView'.DS.'helpers',array('dirs'=>false));
289 $helper_names = array();
290 foreach ($available_helpers as $available_helper){
291 $helper_names[$available_helper] = AkInflector::classify(substr($available_helper,0,-10));
293 return $helper_names;
294 }elseif (is_string($this->helpers)){
295 return Ak::stringToArray($this->helpers);
297 return $this->helpers;
300 function getApplicationHelpers()
302 $helper_names = array();
303 if ($this->app_helpers == 'all'){
304 $available_helpers = Ak::dir(AK_HELPERS_DIR,array('dirs'=>false));
305 $helper_names = array();
306 foreach ($available_helpers as $available_helper){
307 $helper_names[AK_HELPERS_DIR.DS.$available_helper] = AkInflector::classify(substr($available_helper,0,-10));
310 } elseif (!empty($this->app_helpers)){
311 foreach (Ak::toArray($this->app_helpers) as $helper_name){
312 $helper_names[AK_HELPERS_DIR.DS.AkInflector::underscore($helper_name).'_helper.php'] = AkInflector::camelize($helper_name);
315 return $helper_names;
318 function getPluginHelpers()
320 $helper_names = AkActionController::addPluginHelper(false); // Trick for getting helper names set by AkPlugin::addHelper
321 if(empty($helper_names)){
322 return array();
323 }elseif ($this->plugin_helpers == 'all'){
324 return $helper_names;
325 }else {
326 $selected_helper_names = array();
327 foreach (Ak::toArray($this->plugin_helpers) as $helper_name){
328 $helper_name = AkInflector::camelize($helper_name);
329 if($path = array_shift(array_keys($helper_names, AkInflector::camelize($helper_name)))){
330 $selected_helper_names[$path] = $helper_names[$path];
333 return $selected_helper_names;
338 * Used for adding helpers to the base class like those added by the plugins engine.
340 * @param string $helper_name Helper class name like CalendarHelper
341 * @param array $options - path: Path to the helper class, defaults to AK_PLUGINS_DIR/helper_name/lib/helper_name.php
343 function addPluginHelper($helper_name, $options = array())
345 static $helpers = array();
346 if($helper_name === false){
347 return $helpers;
349 $underscored_helper_name = AkInflector::underscore($helper_name);
350 $default_options = array(
351 'path' => AK_PLUGINS_DIR.DS.$underscored_helper_name.DS.'lib'.DS.$underscored_helper_name.'.php'
353 $options = array_merge($default_options, $options);
354 $helpers[$options['path']] = $helper_name;
357 function _validateGeneratedXhtml()
359 require_once(AK_LIB_DIR.DS.'AkXhtmlValidator.php');
360 $XhtmlValidator = new AkXhtmlValidator();
361 if($XhtmlValidator->validate($this->Response->body) === false){
362 $this->Response->sendHeaders();
363 echo '<h1>'.Ak::t('Ooops! There are some errors on current XHTML page').'</h1>';
364 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";
365 $XhtmlValidator->showErrors();
366 echo "<hr /><h2>".Ak::t('Showing XHTML code')."</h2><hr /><div style='border:5px solid red;margin:5px;padding:15px;'>".$this->Response->body."</pre>";
367 die();
373 * Methods for loading desired models into this controller
375 function setModel($model)
377 $this->instantiateIncludedModelClasses(array($model));
380 function setModels($models)
382 $this->instantiateIncludedModelClasses($models);
385 function instantiateIncludedModelClasses($models = array())
387 require_once(AK_LIB_DIR.DS.'AkActiveRecord.php');
388 require_once(AK_APP_DIR.DS.'shared_model.php');
390 empty($this->model) ? ($this->model = $this->params['controller']) : null;
391 empty($this->models) ? ($this->models = array()) : null;
393 $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))));
395 foreach ($models as $model){
396 $this->instantiateModelClass($model, (empty($this->finder_options[$model])?array():$this->finder_options[$model]));
400 function instantiateModelClass($model_class_name, $finder_options = array())
402 $underscored_model_class_name = AkInflector::underscore($model_class_name);
403 $controller_name = $this->getControllerName();
404 $id = empty($this->params[$underscored_model_class_name]['id']) ?
405 (empty($this->params['id']) ? false :
406 (($model_class_name == $controller_name || $model_class_name == AkInflector::singularize($controller_name)) ? $this->params['id'] : false)) :
407 $this->params[$underscored_model_class_name]['id'];
409 if(class_exists($model_class_name)){
410 $underscored_model_class_name = AkInflector::underscore($model_class_name);
412 if(!isset($this->$model_class_name) || !isset($this->$underscored_model_class_name)){
413 if($finder_options !== false && is_numeric($id)){
414 $model =& new $model_class_name();
415 if(empty($finder_options)){
416 $model =& $model->find($id);
417 }else{
418 $model =& $model->find($id, $finder_options);
420 }else{
421 $model =& new $model_class_name();
423 if(!isset($this->$model_class_name)){
424 $this->$model_class_name =& $model;
426 if(!isset($this->$underscored_model_class_name)){
427 $this->$underscored_model_class_name =& $model;
436 Rendering content
437 ====================================================================
441 * Renders the content that will be returned to the browser as the Response body.
443 * === Rendering an action
445 * Action rendering is the most common form and the type used automatically by
446 * Action Controller when nothing else is specified. By default, actions are
447 * rendered within the current layout (if one exists).
449 * * Renders the template for the action "goal" within the current controller
451 * $this->render(array('action'=>'goal'));
453 * * Renders the template for the action "short_goal" within the current controller,
454 * but without the current active layout
456 * $this->render(array('action'=>'short_goal','layout'=>false));
458 * * Renders the template for the action "long_goal" within the current controller,
459 * but with a custom layout
461 * $this->render(array('action'=>'long_goal','layout'=>'spectacular'));
463 * === Rendering partials
465 * Partial rendering is most commonly used together with Ajax calls that only update
466 * one or a few elements on a page without reloading. Rendering of partials from
467 * the controller makes it possible to use the same partial template in
468 * both the full-page rendering (by calling it from within the template) and when
469 * sub-page updates happen (from the controller action responding to Ajax calls).
470 * By default, the current layout is not used.
472 * * Renders the partial located at app/views/controller/_win.tpl
474 * $this->render(array('partial'=>'win'));
476 * * Renders the partial with a status code of 500 (internal error)
478 * $this->render(array('partial'=>'broken','status'=>500));
480 * * Renders the same partial but also makes a local variable available to it
482 * $this->render(array('partial' => 'win', 'locals' => array('name'=>'david')));
484 * * Renders a collection of the same partial by making each element of $wins available through
485 * the local variable "win" as it builds the complete Response
487 * $this->render(array('partial'=>'win','collection'=>$wins));
489 * * Renders the same collection of partials, but also renders the win_divider partial in between
490 * each win partial.
492 * $this->render(array('partial'=>'win','collection'=>$wins,'spacer_template'=>'win_divider'));
494 * === Rendering a template
496 * Template rendering works just like action rendering except that it takes a
497 * path relative to the template root.
498 * The current layout is automatically applied.
500 * * Renders the template located in app/views/weblog/show.tpl
501 * $this->render(array('template'=>'weblog/show'));
503 * === Rendering a file
505 * File rendering works just like action rendering except that it takes a
506 * filesystem path. By default, the path is assumed to be absolute, and the
507 * current layout is not applied.
509 * * Renders the template located at the absolute filesystem path
510 * $this->render(array('file'=>'/path/to/some/template.tpl'));
511 * $this->render(array('file'=>'c:/path/to/some/template.tpl'));
513 * * Renders a template within the current layout, and with a 404 status code
514 * $this->render(array('file' => '/path/to/some/template.tpl', 'layout' => true, 'status' => 404));
515 * $this->render(array('file' => 'c:/path/to/some/template.tpl', 'layout' => true, 'status' => 404));
517 * * Renders a template relative to the template root and chooses the proper file extension
518 * $this->render(array('file' => 'some/template', 'use_full_path' => true));
521 * === Rendering text
523 * Rendering of text is usually used for tests or for rendering prepared content,
524 * such as a cache. By default, text
525 * rendering is not done within the active layout.
527 * * Renders the clear text "hello world" with status code 200
528 * $this->render(array('text' => 'hello world!'));
530 * * Renders the clear text "Explosion!" with status code 500
531 * $this->render(array('text' => "Explosion!", 'status' => 500 ));
533 * * Renders the clear text "Hi there!" within the current active layout (if one exists)
534 * $this->render(array('text' => "Explosion!", 'layout' => true));
536 * * Renders the clear text "Hi there!" within the layout
537 * * placed in "app/views/layouts/special.tpl"
538 * $this->render(array('text' => "Explosion!", 'layout => "special"));
541 * === Rendering an inline template
543 * Rendering of an inline template works as a cross between text and action
544 * rendering where the source for the template
545 * is supplied inline, like text, but its evaled by PHP, like action. By default,
546 * PHP is used for rendering and the current layout is not used.
548 * * Renders "hello, hello, hello, again"
549 * $this->render(array('inline' => "<?php echo str_repeat('hello, ', 3).'again'?>" ));
551 * * Renders "hello david"
552 * $this->render(array('inline' => "<?php echo 'hello ' . $name ?>", 'locals' => array('name' => 'david')));
555 * === Rendering nothing
557 * Rendering nothing is often convenient in combination with Ajax calls that
558 * perform their effect client-side or
559 * when you just want to communicate a status code. Due to a bug in Safari, nothing
560 * actually means a single space.
562 * * Renders an empty Response with status code 200
563 * $this->render(array('nothing' => true));
565 * * Renders an empty Response with status code 401 (access denied)
566 * $this->render(array('nothing' => true, 'status' => 401));
568 function render($options = null, $status = 200)
570 if(empty($options['partial']) && $this->_hasPerformed()){
571 $this->_doubleRenderError(Ak::t("Can only render or redirect once per action"));
572 return false;
575 $this->_flash_handled ? null : $this->_handleFlashAttribute();
577 if(!is_array($options)){
578 return $this->renderFile(empty($options) ? $this->getDefaultTemplateName() : $options, $status, true);
581 if(!empty($options['text'])){
582 return $this->renderText($options['text'], @$options['status']);
583 }else{
585 if(!empty($options['file'])){
586 return $this->renderFile($options['file'], @$options['status'], @$options['use_full_path'], @(array)$options['locals']);
587 }elseif(!empty($options['template'])){
588 return $this->renderFile($options['template'], @$options['status'], true);
589 }elseif(!empty($options['inline'])){
590 return $this->renderTemplate($options['inline'], @$options['status'], @$options['type'], @(array)$options['locals']);
591 }elseif(!empty($options['action'])){
592 return $this->renderAction($options['action'], @$options['status'], @$options['layout']);
593 }elseif(!empty($options['partial'])){
594 if($options['partial'] === true){
595 $options['partial'] = !empty($options['template']) ? $options['template'] : $this->getDefaultTemplateName();
597 if(!empty($options['collection'])){
598 return $this->renderPartialCollection($options['partial'], $options['collection'], @$options['spacer_template'], @$options['locals'], @$options['status']);
599 }else{
600 return $this->renderPartial($options['partial'], @$options['object'], @$options['locals'], @$options['status']);
602 }elseif(!empty($options['nothing'])){
603 // Safari doesn't pass the _headers of the return if the Response is zero length
604 return $this->renderText(' ', @$options['status']);
605 }else{
606 return $this->renderFile($this->getDefaultTemplateName(), @$options['status'], true);
608 return true;
613 * Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead
614 * of sending it as the Response body to the browser.
616 function renderToString($options = null)
618 $result = $this->render($options);
619 $this->eraseRenderResults();
620 $this->variables_added = null;
621 $this->Template->_assigns_added = null;
622 return $result;
625 function renderAction($_action_name, $status = null, $with_layout = true)
627 $this->$_action_name();
628 $template = $this->getDefaultTemplateName($_action_name);
629 if(!empty($with_layout) && !$this->_isTemplateExemptFromLayout($template)){
630 return $this->renderWithLayout($template, $status, $with_layout);
631 }else{
632 return $this->renderWithoutLayout($template, $status);
636 function renderFile($template_path, $status = null, $use_full_path = false, $locals = array())
638 $this->_addVariablesToAssigns();
639 $locals = array_merge($locals,$this->_assigns);
641 if($use_full_path){
642 $this->_assertExistanceOfTemplateFile($template_path);
645 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->message("Rendering $this->full_template_path" . (!empty($status) ? " ($status)":'')) : null;
646 return $this->renderText($this->Template->renderFile($template_path, $use_full_path, $locals), $status);
649 function renderTemplate($template, $status = null, $type = 'tpl', $local_assigns = array())
651 $this->_addVariablesToAssigns();
652 $local_assigns = array_merge($local_assigns,$this->_assigns);
653 return $this->renderText($this->Template->renderTemplate($type, $template, null, $local_assigns), $status);
656 function renderText($text = null, $status = null)
658 $this->performed_render = true;
659 $this->Response->_headers['Status'] = !empty($status) ? $status : $this->_default_render_status_code;
660 $this->Response->body = $text;
661 return $text;
664 function renderNothing($status = null)
666 return $this->renderText(' ', $status);
669 function renderPartial($partial_path = null, $object = null, $local_assigns = null, $status = null)
671 $partial_path = empty($partial_path) ? $this->getDefaultTemplateName() : $partial_path;
672 $this->variables_added = false;
673 $this->performed_render = false;
674 $this->_addVariablesToAssigns();
675 $this->Template->controller =& $this;
676 $this->$partial_path = $this->renderText($this->Template->renderPartial($partial_path, $object, array_merge($this->_assigns, (array)$local_assigns)), $status);
677 return $this->$partial_path;
680 function renderPartialCollection($partial_name, $collection, $partial_spacer_template = null, $local_assigns = null, $status = null)
682 $this->_addVariablesToAssigns();
683 $collection_name = AkInflector::pluralize($partial_name).'_collection';
684 $result = $this->Template->renderPartialCollection($partial_name, $collection, $partial_spacer_template, $local_assigns);
685 if(empty($this->$collection_name)){
686 $this->$collection_name = $result;
688 $this->variables_added = false;
689 $this->performed_render = false;
691 return $result;
694 function renderWithLayout($template_name = null, $status = null, $layout = null)
696 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
697 return $this->renderWithALayout($template_name, $status, $layout);
700 function renderWithoutLayout($template_name = null, $status = null)
702 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
703 return $this->render($template_name, $status);
707 * Clears the rendered results, allowing for another render to be performed.
709 function eraseRenderResults()
711 $this->Response->body = '';
712 $this->performed_render = false;
713 $this->variables_added = false;
716 function _addVariablesToAssigns()
718 if(empty($this->variables_added)){
719 $this->_addInstanceVariablesToAssigns();
720 $this->variables_added = true;
724 function _addInstanceVariablesToAssigns()
726 $this->_protected_variables_cache = array_merge($this->_protected_variables_cache, $this->_getProtectedInstanceVariables());
728 foreach (array_diff(array_keys(get_object_vars($this)), $this->_protected_variables_cache) as $attribute){
729 if($attribute[0] != '_'){
730 $this->_assigns[$attribute] =& $this->$attribute;
735 function _getProtectedInstanceVariables()
737 return !empty($this->_view_controller_internals) ?
738 array('_assigns', 'performed_redirect', 'performed_render','db') :
739 array('_assigns', 'performed_redirect', 'performed_render', 'session', 'cookies',
740 'Template','db','helpers','models','layout','Response','Request',
741 'params','passed_args');
746 * Use this to translate strings in the scope of your controller
748 * @see Ak::t
750 function t($string, $array = null)
752 return Ak::t($string, $array, AkInflector::underscore($this->getControllerName()));
758 Redirects
759 ====================================================================
763 * Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
765 * * <tt>Array</tt>: The URL will be generated by calling $this->UrlFor with the +options+.
766 * * <tt>String starting with protocol:// (like http://)</tt>: Is passed straight through
767 * as the target for redirection.
768 * * <tt>String not containing a protocol</tt>: The current protocol and host is prepended to the string.
769 * * <tt>back</tt>: Back to the page that issued the Request-> Useful for forms that are
770 * triggered from multiple places.
771 * Short-hand for redirectTo(Request->env["HTTP_REFERER"])
773 * Examples:
774 * redirectTo(array('action' => 'show', 'id' => 5));
775 * redirectTo('http://www.akelos.com');
776 * redirectTo('/images/screenshot.jpg');
777 * redirectTo('back');
779 * The redirection happens as a "302 Moved" header.
781 function redirectTo($options = array(), $parameters_for_method_reference = null)
783 if(is_string($options)) {
784 if(preg_match('/^\w+:\/\/.*/',$options)){
785 if($this->_hasPerformed()){
786 $this->_doubleRenderError();
788 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->message('Redirected to '.$options) : null;
789 $this->_handleFlashAttribute();
790 $this->Response->redirect($options);
791 $this->Response->redirected_to = $options;
792 $this->performed_redirect = true;
793 }elseif ($options == 'back'){
794 $this->redirectTo($this->Request->env['HTTP_REFERER']);
795 }else{
796 $this->redirectTo($this->Request->getProtocol(). $this->Request->getHostWithPort(). $options);
798 }else{
799 if(empty($parameters_for_method_reference)){
800 $this->redirectTo($this->UrlFor($options));
801 $this->Response->redirected_to = $options;
802 }else{
803 $this->redirectTo($this->UrlFor($options, $parameters_for_method_reference));
804 $this->Response->redirected_to = $options;
805 $this->Response->redirected_to_method_params = $parameters_for_method_reference;
810 function redirectToAction($action, $options = array())
812 $this->redirectTo(array_merge(array('action'=>$action), $options));
817 * This methods are required for retrieving available controllers for URL Routing
819 function rewriteOptions($options)
821 $defaults = $this->defaultUrlOptions($options);
822 if(!empty($this->module_name)){
823 $defaults['module'] = $this->getModuleName();
825 if(!empty($options['controller']) && strstr($options['controller'], '/')){
826 $defaults['module'] = substr($options['controller'], 0, strrpos($options['controller'], '/'));
827 $options['controller'] = substr($options['controller'], strrpos($options['controller'], '/') + 1);
829 $options = !empty($defaults) ? array_merge($defaults, $options) : $options;
830 $options['controller'] = empty($options['controller']) ? AkInflector::underscore($this->getControllerName()) : $options['controller'];
831 return $options;
834 function getControllerName()
836 if(!isset($this->controller_name)){
837 $current_class_name = str_replace('_', '::', get_class($this));
838 if (!AK_PHP5){
839 $current_class_name = $this->__getControllerName_PHP4_fix($current_class_name);
841 $controller_name = substr($current_class_name,0,-10);
842 $this->controller_name = $this->_removeModuleNameFromControllerName($controller_name);
844 return $this->controller_name;
847 function __getControllerName_PHP4_fix($class_name)
849 $included_controllers = $this->_getIncludedControllerNames();
850 $lowercase_included_controllers = array_map('strtolower', $included_controllers);
851 $key = array_search(strtolower($class_name), $lowercase_included_controllers, true);
852 return $included_controllers[$key];
855 function getModuleName()
857 return $this->module_name;
860 function setModuleName($module_name)
862 return $this->module_name = $module_name;
866 * Removes the modules name from the controller if exists and sets it.
868 * @return $controller_name
870 function _removeModuleNameFromControllerName($controller_name)
872 if(strstr($controller_name, '::')){
873 $module_parts = explode ('::',$controller_name);
874 $controller_name = array_pop($module_parts);
875 $this->setModuleName(join('/', array_map(array('AkInflector','underscore'), $module_parts)));
877 return $controller_name;
880 function _getTemplateBasePath()
882 return AK_APP_DIR.DS.'views'.DS.(empty($this->_module_path)?'':$this->_module_path).$this->Request->getController();
885 function _getIncludedControllerNames()
887 $controllers = array();
888 foreach (get_included_files() as $file_name){
889 if(strstr($file_name,AK_CONTROLLERS_DIR)){
890 $controllers[] = AkInflector::classify(str_replace(array(AK_CONTROLLERS_DIR.DS,'.php', DS, '//'),array('','','/', '/'),$file_name));
893 return $controllers;
900 URL generation/rewriting
901 ====================================================================
906 * Overwrite to implement a number of default options that all urlFor-based methods will use.
907 * The default options should come in
908 * the form of a an array, just like the one you would use for $this->UrlFor directly. Example:
910 * function defaultUrlOptions($options)
912 * return array('project' => ($this->Project->isActive() ? $this->Project->url_name : 'unknown'));
915 * As you can infer from the example, this is mostly useful for situations where you want to
916 * centralize dynamic decisions about the urls as they stem from the business domain.
917 * Please note that any individual $this->UrlFor call can always override the defaults set
918 * by this method.
920 function defaultUrlOptions($options)
926 * Returns a URL that has been rewritten according to the options array and the defined Routes.
927 * (For doing a complete redirect, use redirectTo).
929 * <tt>$this->UrlFor</tt> is used to:
931 * All keys given to $this->UrlFor are forwarded to the Route module, save for the following:
932 * * <tt>anchor</tt> -- specifies the anchor name to be appended to the path. For example,
933 * <tt>$this->UrlFor(array('controller' => 'posts', 'action' => 'show', 'id' => 10, 'anchor' => 'comments'</tt>
934 * will produce "/posts/show/10#comments".
935 * * <tt>only_path</tt> -- if true, returns the absolute URL (omitting the protocol, host name, and port)
936 * * <tt>trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
937 * is currently not recommended since it breaks caching.
938 * * <tt>host</tt> -- overrides the default (current) host if provided
939 * * <tt>protocol</tt> -- overrides the default (current) protocol if provided
941 * The URL is generated from the remaining keys in the array. A URL contains two key parts: the <base> and a query string.
942 * Routes composes a query string as the key/value pairs not included in the <base>.
944 * The default Routes setup supports a typical Akelos Framework path of "controller/action/id"
945 * where action and id are optional, with
946 * action defaulting to 'index' when not given. Here are some typical $this->UrlFor statements
947 * and their corresponding URLs:
949 * $this->UrlFor(array('controller'=>'posts','action'=>'recent')); // 'proto://host.com/posts/recent'
950 * $this->UrlFor(array('controller'=>'posts','action'=>'index')); // 'proto://host.com/posts'
951 * $this->UrlFor(array('controller'=>'posts','action'=>'show','id'=>10)); // 'proto://host.com/posts/show/10'
953 * When generating a new URL, missing values may be filled in from the current
954 * Request's parameters. For example,
955 * <tt>$this->UrlFor(array('action'=>'some_action'));</tt> will retain the current controller,
956 * as expected. This behavior extends to other parameters, including <tt>controller</tt>,
957 * <tt>id</tt>, and any other parameters that are placed into a Route's path.
959 * The URL helpers such as <tt>$this->UrlFor</tt> have a limited form of memory:
960 * when generating a new URL, they can look for missing values in the current Request's parameters.
961 * Routes attempts to guess when a value should and should not be
962 * taken from the defaults. There are a few simple rules on how this is performed:
964 * * If the controller name begins with a slash, no defaults are used: <tt>$this->UrlFor(array('controller'=>'/home'));</tt>
965 * * If the controller changes, the action will default to index unless provided
967 * The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
968 * route given by <tt>map->connect('people/:last/:first/:action', array('action' => 'bio', 'controller' => 'people'))</tt>.
970 * Suppose that the current URL is "people/hh/david/contacts". Let's consider a few
971 * different cases of URLs which are generated from this page.
973 * * <tt>$this->UrlFor(array('action'=>'bio'));</tt> -- During the generation of this URL,
974 * default values will be used for the first and
975 * last components, and the action shall change. The generated URL will be, "people/hh/david/bio".
976 * * <tt>$this->UrlFor(array('first'=>'davids-little-brother'));</tt> This
977 * generates the URL 'people/hh/davids-little-brother' -- note
978 * that this URL leaves out the assumed action of 'bio'.
980 * However, you might ask why the action from the current Request, 'contacts', isn't
981 * carried over into the new URL. The answer has to do with the order in which
982 * the parameters appear in the generated path. In a nutshell, since the
983 * value that appears in the slot for <tt>first</tt> is not equal to default value
984 * for <tt>first</tt> we stop using defaults. On it's own, this rule can account
985 * for much of the typical Akelos Framework URL behavior.
987 * Although a convienence, defaults can occasionaly get in your way. In some cases
988 * a default persists longer than desired.
989 * The default may be cleared by adding <tt>'name' => null</tt> to <tt>$this->UrlFor</tt>'s options.
990 * This is often required when writing form helpers, since the defaults in play
991 * may vary greatly depending upon where the helper is used from. The following line
992 * will redirect to PostController's default action, regardless of the page it is
993 * displayed on:
995 * $this->UrlFor(array('controller' => 'posts', 'action' => null));
997 * If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the
998 * overwrite_params options. Say for your posts you have different views for showing and printing them.
999 * Then, in the show view, you get the URL for the print view like this
1001 * $this->UrlFor(array('overwrite_params' => array('action' => 'print')));
1003 * This takes the current URL as is and only exchanges the action. In contrast,
1004 * <tt>$this->UrlFor(array('action'=>'print'));</tt>
1005 * would have slashed-off the path components after the changed action.
1007 function urlFor($options = array(), $parameters_for_method_reference = null)
1009 return $this->rewrite($this->rewriteOptions($options));
1012 function addToUrl($options = array(), $options_to_exclude = array())
1014 $options_to_exclude = array_merge(array('ak','lang',AK_SESSION_NAME,'AK_SESSID','PHPSESSID'), $options_to_exclude);
1015 $options = array_merge(array_merge(array('action'=>$this->Request->getAction()),$this->params),$options);
1016 foreach ($options_to_exclude as $option_to_exclude){
1017 unset($options[$option_to_exclude]);
1019 return $this->urlFor($options);
1022 function getActionName()
1024 return $this->Request->getAction();
1028 function _doubleRenderError($message = null)
1030 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);
1033 function _hasPerformed()
1035 return !empty($this->performed_render) || !empty($this->performed_redirect);
1038 function _getRequestOrigin()
1040 return $this->Request->remote_ip.' at '.Ak::getDate();
1043 function _getCompleteRequestUri()
1045 return $this->Request->protocol . $this->Request->host . $this->Request->request_uri;
1048 function _closeSession()
1050 !empty($this->session) ? session_write_close() : null;
1054 function _hasTemplate($template_name = null)
1056 return file_exists(empty($template_name) ? $this->getDefaultTemplateName() : $template_name);
1059 function _templateIsPublic($template_name = null)
1061 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
1062 return $this->Template->fileIsPublic($template_name);
1065 function _isTemplateExemptFromLayout($template_name = null)
1067 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name;
1068 return $this->Template->_javascriptTemplateExists($template_name);
1071 function _assertExistanceOfTemplateFile($template_name)
1073 $extension = $this->Template->delegateTemplateExists($template_name);
1074 $this->full_template_path = $this->Template->getFullTemplatePath($template_name, $extension ? $extension : 'tpl');
1075 if(!$this->_hasTemplate($this->full_template_path)){
1076 if(!empty($this->_ignore_missing_templates) && $this->_ignore_missing_templates === true){
1077 return;
1079 $template_type = strstr($template_name,'layouts') ? 'layout' : 'template';
1080 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);
1084 function getDefaultTemplateName($default_action_name = null)
1086 return empty($default_action_name) ? (empty($this->_default_template_name) ? $this->_action_name : $this->_default_template_name) : $default_action_name;
1089 function setDefaultTemplateName($template_name)
1091 $this->_default_template_name = $template_name;
1096 function rewrite($options = array())
1098 return $this->_rewriteUrl($this->_rewritePath($options), $options);
1102 function toString()
1104 return $this->Request->getProtocol().$this->Request->getHostWithPort().
1105 $this->Request->getPath().@$this->parameters['controller'].
1106 @$this->parameters['action'].@$this->parameters['inspect'];
1110 * Given a path and options, returns a rewritten URL string
1112 function _rewriteUrl($path, $options)
1114 $rewritten_url = '';
1115 if(empty($options['only_path'])){
1116 $rewritten_url .= !empty($options['protocol']) ? $options['protocol'] : $this->Request->getProtocol();
1117 $rewritten_url .= empty($rewritten_url) || strpos($rewritten_url,'://') ? '' : '://';
1118 $rewritten_url .= $this->_rewriteAuthentication($options);
1119 $rewritten_url .= !empty($options['host']) ? $options['host'] : $this->Request->getHostWithPort();
1120 $options = Ak::delete($options, array('user','password','host','protocol'));
1123 $rewritten_url .= empty($options['skip_relative_url_root']) ? $this->Request->getRelativeUrlRoot() : '';
1125 if(empty($options['skip_url_locale'])){
1126 $locale = $this->Request->getLocaleFromUrl();
1127 if(empty($options['lang'])){
1128 $rewritten_url .= (empty($locale) ? '' : '/').$locale;
1133 $rewritten_url .= (substr($rewritten_url,-1) == '/' ? '' : (AK_URL_REWRITE_ENABLED ? '' : (!empty($path[0]) && $path[0] != '/' ? '/' : '')));
1134 $rewritten_url .= $path;
1135 $rewritten_url .= empty($options['trailing_slash']) ? '' : '/';
1136 $rewritten_url .= empty($options['anchor']) ? '' : '#'.$options['anchor'];
1138 return $rewritten_url;
1141 function _rewriteAuthentication($options)
1143 if(!isset($options['user']) && isset($options['password'])){
1144 return urlencode($options['user']).':'.urlencode($options['password']).'@';
1145 }else{
1146 return '';
1150 function _rewritePath($options)
1152 if(!empty($options['params'])){
1153 foreach ($options['params'] as $k=>$v){
1154 $options[$k] = $v;
1156 unset($options['params']);
1158 if(!empty($options['overwrite_params'])){
1159 foreach ($options['overwrite_params'] as $k=>$v){
1160 $options[$k] = $v;
1162 unset($options['overwrite_params']);
1164 foreach (array('anchor', 'params', 'only_path', 'host', 'protocol', 'trailing_slash', 'skip_relative_url_root') as $k){
1165 unset($options[$k]);
1167 $path = Ak::toUrl($options);
1168 return $path;
1172 * Returns a query string with escaped keys and values from the passed array. If the passed
1173 * array contains an 'id' it'll
1174 * be added as a path element instead of a regular parameter pair.
1176 function buildQueryString($array, $only_keys = null)
1178 $array = !empty($only_keys) ? array_keys($array) : $array;
1179 return Ak::toUrl($array);
1186 Layouts
1187 ====================================================================
1189 * Layouts reverse the common pattern of including shared headers and footers in many templates
1190 * to isolate changes in repeated setups. The inclusion pattern has pages that look like this:
1192 * <?php echo $controller->render('shared/header') ?>
1193 * Hello World
1194 * <?php echo $controller->render('shared/footer') ?>
1196 * This approach is a decent way of keeping common structures isolated from the
1197 * changing content, but it's verbose and if( you ever want to change the structure
1198 * of these two includes, you'll have to change all the templates.
1200 * With layouts, you can flip it around and have the common structure know where
1201 * to insert changing content. This means that the header and footer are only
1202 * mentioned in one place, like this:
1204 * <!-- The header part of this layout -->
1205 * <?php echo $content_for_layout ?>
1206 * <!-- The footer part of this layout -->
1208 * And then you have content pages that look like this:
1210 * hello world
1212 * Not a word about common structures. At rendering time, the content page is
1213 * computed and then inserted in the layout,
1214 * like this:
1216 * <!-- The header part of this layout -->
1217 * hello world
1218 * <!-- The footer part of this layout -->
1220 * == Accessing shared variables
1222 * Layouts have access to variables specified in the content pages and vice versa.
1223 * This allows you to have layouts with references that won't materialize before
1224 * rendering time:
1226 * <h1><?php echo $page_title ?></h1>
1227 * <?php echo $content_for_layout ?>
1229 * ...and content pages that fulfill these references _at_ rendering time:
1231 * <?php $page_title = 'Welcome'; ?>
1232 * Off-world colonies offers you a chance to start a new life
1234 * The result after rendering is:
1236 * <h1>Welcome</h1>
1237 * Off-world colonies offers you a chance to start a new life
1239 * == Automatic layout assignment
1241 * If there is a template in <tt>app/views/layouts/</tt> with the same name as
1242 * the current controller then it will be automatically
1243 * set as that controller's layout unless explicitly told otherwise. Say you have
1244 * a WeblogController, for example. If a template named <tt>app/views/layouts/weblog.tpl</tt>
1245 * exists then it will be automatically set as the layout for your WeblogController.
1246 * You can create a layout with the name <tt>application.tpl</tt>
1247 * and this will be set as the default controller if there is no layout with
1248 * the same name as the current controller and there is no layout explicitly
1249 * assigned on the +layout+ attribute. Setting a layout explicitly will always
1250 * override the automatic behaviour
1251 * for the controller where the layout is set. Explicitly setting the layout
1252 * in a parent class, though, will not override the
1253 * child class's layout assignement if the child class has a layout with the same name.
1255 * == Inheritance for layouts
1257 * Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
1259 * class BankController extends AkActionController
1261 * var $layout = 'bank_standard';
1264 * class InformationController extends BankController
1268 * class VaultController extends BankController
1270 * var $layout = 'access_level_layout';
1273 * class EmployeeController extends BankController
1275 * var $layout = null;
1278 * The InformationController uses 'bank_standard' inherited from the BankController, the VaultController
1279 * and picks the layout 'access_level_layout', and the EmployeeController doesn't want to use a layout at all.
1281 * == Types of layouts
1283 * Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
1284 * you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
1285 * be done either by an inline method.
1287 * The method reference is the preferred approach to variable layouts and is used like this:
1289 * class WeblogController extends AkActionController
1291 * function __construct()
1293 * $this->setLayout(array(&$this, '_writersAndReaders'));
1296 * function index()
1298 * // fetching posts
1301 * function _writersAndReaders()
1303 * return is_logged_in() ? 'writer_layout' : 'reader_layout';
1307 * Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
1308 * is logged in or not.
1310 * The most common way of specifying a layout is still just as a plain template name:
1312 * class WeblogController extends AkActionController
1314 * var $layout = 'weblog_standard';
1317 * If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
1319 * == Conditional layouts
1321 * If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
1322 * 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
1323 * <tt>only</tt> and <tt>except</tt> options can be passed to the layout call. For example:
1325 * class WeblogController extends AkActionController
1327 * function __construct()
1329 * $this->setLayout('weblog_standard', array('except' => 'rss'));
1332 * // ...
1336 * This will assign 'weblog_standard' as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
1337 * around the rendered view.
1339 * Both the <tt>only</tt> and <tt>except</tt> condition can accept an arbitrary number of method names, so
1340 * <tt>'except' => array('rss', 'text_only')</tt> is valid, as is <tt>'except' => 'rss'</tt>.
1342 * == Using a different layout in the action render call
1344 * If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
1345 * Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
1346 * This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
1347 * qualified template and layout names as this example shows:
1349 * class WeblogController extends AkActionController
1351 * function help()
1353 * $this->render(array('action'=>'help/index','layout'=>'help'));
1359 * If a layout is specified, all actions rendered through render and render_action will have their result assigned
1360 * to <tt>$this->content_for_layout</tt>, which can then be used by the layout to insert their contents with
1361 * <tt><?php echo $$this->content_for_layout ?></tt>. This layout can itself depend on instance variables assigned during action
1362 * performance and have access to them as any normal template would.
1364 function setLayout($template_name, $conditions = array())
1366 $this->_addLayoutConditions($conditions);
1367 $this->layout = $template_name;
1370 function getLayoutConditions()
1372 return empty($this->_layout_conditions) ? array() : $this->_layout_conditions;
1375 function _addLayoutConditions($conditions)
1377 $this->_layout_conditions = $conditions;
1383 * Returns the name of the active layout. If the layout was specified as a method reference, this method
1384 * is called and the return value is used. Likewise if( the layout was specified as an inline method (through a method
1385 * object). If the layout was defined without a directory, layouts is assumed. So <tt>setLayout('weblog/standard')</tt> will return
1386 * weblog/standard, but <tt>setLayout('standard')</tt> will return layouts/standard.
1388 function getActiveLayout($passed_layout = null)
1390 if(empty($passed_layout)){
1391 $layout = !isset($this->layout) ? AkInflector::underscore($this->getControllerName()) : $this->layout;
1392 }else{
1393 $layout =& $passed_layout;
1395 if(is_array($layout) && is_object($layout[0]) && method_exists($layout[0], $layout[1])){
1396 $this->active_layout = $layout[0]->{$layout[1]}();
1397 }elseif(method_exists($this,$layout) && strtolower(get_class($this)) !== strtolower($layout)){
1398 $this->active_layout = $this->$layout();
1399 }else{
1400 $this->active_layout = $layout;
1403 if(!empty($this->active_layout)){
1404 return strstr($this->active_layout,DS) ? $this->active_layout : 'layouts'.DS.$this->active_layout;
1409 function renderWithALayout($options = null, $status = null, $layout = null)
1411 $template_with_options = !empty($options) && is_array($options);
1413 if($this->_canApplyLayout($template_with_options, $options) && ($layout = $this->_pickLayout($template_with_options, $options, $layout))){
1415 $options = $template_with_options? array_merge((array)$options,array('layout'=>false)) : $options;
1417 $this->content_for_layout = $this->render($options, $status);
1419 if($template_with_options){
1420 $status = empty($options['status']) ? $status : $options['status'];
1423 $this->eraseRenderResults();
1424 $this->_addVariablesToAssigns();
1426 return $this->renderText($this->Template->renderFile($layout, true, &$this->_assigns), $status);
1427 }else{
1428 return $this->render($options, $status, &$this->_assigns);
1432 function _canApplyLayout($template_with_options, $options)
1434 return !empty($template_with_options) ? $this->_isCandidateForLayout($options) : !$this->_isTemplateExemptFromLayout();
1437 function _isCandidateForLayout($options)
1439 return !empty($options['layout']) ||
1440 (empty($options['text']) && empty($options['file']) && empty($options['inline']) && empty($options['partial']) && empty($options['nothing'])) &&
1441 !$this->_isTemplateExemptFromLayout($this->_getDefaultTemplateName(empty($options['action']) ? $options['template'] : $options['action']));
1444 function _pickLayout($template_with_options, $options, $layout = null)
1446 if(!empty($template_with_options)){
1447 $layout = empty($options['layout']) ? ($this->_doesActionHasLayout() ? $this->getActiveLayout(): false) : $this->getActiveLayout($options['layout']);
1448 }elseif(empty($layout) || $layout === true){
1449 $layout = $this->_doesActionHasLayout() ? $this->getActiveLayout() : false;
1451 if(!empty($layout)){
1452 $layout = strstr($layout,'/') || strstr($layout,DS) ? $layout : 'layouts'.DS.$layout;
1453 $layout = substr($layout,0,7) === 'layouts' ?
1454 (empty($this->_module_path) ? AK_VIEWS_DIR.DS.$layout.'.tpl' : AK_VIEWS_DIR.DS.'layouts'.DS.trim($this->_module_path, DS).'.tpl') :
1455 $layout.'.tpl';
1456 if (file_exists($layout)) {
1457 return $layout;
1459 $layout = null;
1461 if(empty($layout) && $layout !== false && defined('AK_DEFAULT_LAYOUT')){
1462 $layout = AK_VIEWS_DIR.DS.'layouts'.DS.AK_DEFAULT_LAYOUT.'.tpl';
1464 return file_exists($layout) ? $layout : false;
1467 function _doesActionHasLayout()
1469 $conditions = $this->getLayoutConditions();
1471 $action_name = $this->Request->getAction();
1472 if(!empty($conditions['only']) && ((is_array($conditions['only']) && in_array($action_name,$conditions['only'])) ||
1473 (is_string($conditions['only']) && $action_name == $conditions['only']))){
1474 return true;
1475 }elseif (!empty($conditions['only'])){
1476 return false;
1478 if(!empty($conditions['except']) && ((is_array($conditions['except']) && in_array($action_name,$conditions['except'])) ||
1479 (is_string($conditions['except']) && $action_name == $conditions['except']))){
1480 return false;
1483 return true;
1490 Filters
1491 ====================================================================
1493 * Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
1494 * authentication, caching, or auditing before the intended action is performed. Or to do localization or output
1495 * compression after the action has been performed.
1497 * Filters have access to the request, response, and all the instance variables set by other filters in the chain
1498 * or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>beforeFilter</tt>
1499 * to halt the processing before the intended action is processed by returning false or performing a redirect or render.
1500 * This is especially useful for filters like authentication where you're not interested in allowing the action to be
1501 * performed if the proper credentials are not in order.
1503 * == Filter inheritance
1505 * Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
1506 * affecting the superclass. For example:
1508 * class BankController extends AkActionController
1510 * function __construct()
1512 * $this->beforeFilter('_audit');
1515 * function _audit(&$controller)
1517 * // record the action and parameters in an audit log
1521 * class VaultController extends BankController
1523 * function __construct()
1525 * $this->beforeFilter('_verifyCredentials');
1528 * function _verifyCredentials(&$controller)
1530 * // make sure the user is allowed into the vault
1534 * Now any actions performed on the BankController will have the audit method called before. On the VaultController,
1535 * first the audit method is called, then the _verifyCredentials method. If the _audit method returns false, then
1536 * _verifyCredentials and the intended action are never called.
1538 * == Filter types
1540 * A filter can take one of three forms: method reference, external class, or inline method. The first
1541 * is the most common and works by referencing a method somewhere in the inheritance hierarchy of
1542 * the controller by use of a method name. In the bank example above, both BankController and VaultController use this form.
1544 * Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
1545 * are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
1547 * class OutputCompressionFilter
1549 * function filter(&$controller)
1551 * $controller->response->body = compress($controller->response->body);
1555 * class NewspaperController extends AkActionController
1557 * function __construct()
1559 * $this->afterFilter(new OutputCompressionFilter());
1563 * The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
1564 * manipulate them as it sees fit.
1567 * == Filter chain ordering
1569 * Using <tt>beforeFilter</tt> and <tt>afterFilter</tt> appends the specified filters to the existing chain. That's usually
1570 * just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
1571 * can use <tt>prependBeforeFilter</tt> and <tt>prependAfterFilter</tt>. Filters added by these methods will be put at the
1572 * beginning of their respective chain and executed before the rest. For example:
1574 * class ShoppingController extends AkActionController
1576 * function __construct()
1578 * $this->beforeFilter('verifyOpenShop');
1583 * class CheckoutController extends AkActionController
1585 * function __construct()
1587 * $this->prependBeforeFilter('ensureItemsInCart', 'ensureItemsInStock');
1591 * The filter chain for the CheckoutController is now <tt>ensureItemsInCart, ensureItemsInStock,</tt>
1592 * <tt>verifyOpenShop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
1593 * is open or not.
1595 * You may pass multiple filter arguments of each type.
1597 * == Around filters
1599 * In addition to the individual before and after filters, it's also possible to specify that a single object should handle
1600 * both the before and after call. That's especially useful when you need to keep state active between the before and after,
1601 * such as the example of a benchmark filter below:
1603 * class WeblogController extends AkActionController
1605 * function __construct()
1607 * $this->aroundFilter(new BenchmarkingFilter());
1610 * // Before this action is performed, BenchmarkingFilter->before($controller) is executed
1611 * function index()
1614 * // After this action has been performed, BenchmarkingFilter->after($controller) is executed
1617 * class BenchmarkingFilter
1619 * function before(&$controller)
1621 * start_timer();
1624 * function after(&$controller)
1626 * stop_timer();
1627 * report_result();
1631 * == Filter chain skipping
1633 * Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
1634 * subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
1635 * they would like to be relieved of. Examples
1637 * class ApplicationController extends AkActionController
1639 * function __construct()
1641 * $this->beforeFilter('authenticate');
1645 * class WeblogController extends ApplicationController
1647 * // will run the authenticate filter
1650 * class SignupController extends AkActionController
1652 * function __construct()
1654 * $this->skipBeforeFilter('authenticate');
1656 * // will not run the authenticate filter
1659 * == Filter conditions
1661 * Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
1662 * exclude or the actions to include when executing the filter. Available conditions are +only+ or +except+, both
1663 * of which accept an arbitrary number of method references. For example:
1665 * class Journal extends AkActionController
1667 * function __construct()
1668 * { // only require authentication if the current action is edit or delete
1669 * $this->beforeFilter(array('_authorize'=>array('only'=>array('edit','delete')));
1672 * function _authorize(&$controller)
1674 * // redirect to login unless authenticated
1679 var $_includedActions = array(), $_beforeFilters = array(), $_afterFilters = array(), $_excludedActions = array();
1681 * The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
1682 * on this controller are performed.
1684 function appendBeforeFilter()
1686 $filters = array_reverse(func_get_args());
1687 foreach (array_keys($filters) as $k){
1688 $conditions = $this->_extractConditions(&$filters[$k]);
1689 $this->_addActionConditions($filters[$k], $conditions);
1690 $this->_appendFilterToChain('before', $filters[$k]);
1695 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
1696 * on this controller are performed.
1698 function prependBeforeFilter()
1700 $filters = array_reverse(func_get_args());
1701 foreach (array_keys($filters) as $k){
1702 $conditions = $this->_extractConditions(&$filters[$k]);
1703 $this->_addActionConditions($filters[$k], $conditions);
1704 $this->_prependFilterToChain('before', $filters[$k]);
1709 * Short-hand for appendBeforeFilter since that's the most common of the two.
1711 function beforeFilter()
1713 $filters = func_get_args();
1714 foreach (array_keys($filters) as $k){
1715 $this->appendBeforeFilter($filters[$k]);
1720 * The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
1721 * on this controller are performed.
1723 function appendAfterFilter()
1725 $filters = array_reverse(func_get_args());
1726 foreach (array_keys($filters) as $k){
1727 $conditions = $this->_extractConditions(&$filters[$k]);
1728 $this->_addActionConditions(&$filters[$k], $conditions);
1729 $this->_appendFilterToChain('after', &$filters[$k]);
1735 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
1736 * on this controller are performed.
1738 function prependAfterFilter()
1740 $filters = array_reverse(func_get_args());
1741 foreach (array_keys($filters) as $k){
1742 $conditions = $this->_extractConditions(&$filters[$k]);
1743 $this->_addActionConditions(&$filters[$k], $conditions);
1744 $this->_prependFilterToChain('after', &$filters[$k]);
1749 * Short-hand for appendAfterFilter since that's the most common of the two.
1750 * */
1751 function afterFilter()
1753 $filters = func_get_args();
1754 foreach (array_keys($filters) as $k){
1755 $this->appendAfterFilter($filters[$k]);
1760 * The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
1761 * on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
1762 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like:
1764 * B::before()
1765 * A::before()
1766 * A::after()
1767 * B::after()
1769 function appendAroundFilter()
1771 $filters = func_get_args();
1772 foreach (array_keys($filters) as $k){
1773 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]);
1774 $this->appendBeforeFilter(array(&$filters[$k],'before'));
1776 $filters = array_reverse($filters);
1777 foreach (array_keys($filters) as $k){
1778 $this->prependAfterFilter(array(&$filters[$k],'after'));
1783 * The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
1784 * on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
1785 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like:
1787 * A::before()
1788 * B::before()
1789 * B::after()
1790 * A::after()
1792 function prependAroundFilter()
1794 $filters = func_get_args();
1795 foreach (array_keys($filters) as $k){
1796 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]);
1797 $this->prependBeforeFilter(array(&$filters[$k],'before'));
1799 $filters = array_reverse($filters);
1800 foreach (array_keys($filters) as $k){
1801 $this->appendAfterFilter(array(&$filters[$k],'after'));
1806 * Short-hand for appendAroundFilter since that's the most common of the two.
1808 function aroundFilter()
1810 $filters = func_get_args();
1811 call_user_func_array(array(&$this,'appendAroundFilter'), $filters);
1815 * Removes the specified filters from the +before+ filter chain.
1816 * This is especially useful for managing the chain in inheritance hierarchies where only one out
1817 * of many sub-controllers need a different hierarchy.
1819 function skipBeforeFilter($filters)
1821 $filters = func_get_args();
1822 $this->_skipFilter($filters, 'before');
1826 * Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
1827 * filters, not instances. This is especially useful for managing the chain in inheritance hierarchies where only one out
1828 * of many sub-controllers need a different hierarchy.
1830 function skipAfterFilter($filters)
1832 $filters = func_get_args();
1833 $this->_skipFilter($filters, 'after');
1836 function _skipFilter(&$filters, $type)
1838 $_filters =& $this->{'_'.$type.'Filters'};
1839 // array_diff doesn't play nice with some PHP5 releases when it comes to
1840 // Objects as it only diff equal references, not object types
1841 foreach (array_keys($filters) as $k){
1842 if(AK_PHP5){
1843 if(is_object($filters[$k])){
1844 foreach (array_keys($_filters) as $k2){
1845 if(is_object($_filters[$k2]) && get_class($_filters[$k2]) == get_class($filters[$k])){
1846 $pos = $k2;
1847 break;
1850 }else{
1851 $pos = array_search($filters[$k], $_filters);
1854 array_splice($_filters, $pos, 1, null);
1855 return ;
1857 $_filters = array_diff($_filters,array($filters[$k]));
1862 * Returns all the before filters for this class.
1864 function beforeFilters()
1866 return $this->_beforeFilters;
1870 * Returns all the after filters for this class and all its ancestors.
1872 function afterFilters()
1874 return $this->_afterFilters;
1878 * Returns a mapping between filters and the actions that may run them.
1880 function includedActions()
1882 return $this->_includedActions;
1886 * Returns a mapping between filters and actions that may not run them.
1888 function excludedActions()
1890 return $this->_excludedActions;
1894 function _appendFilterToChain($condition, $filters)
1896 $this->{"_{$condition}Filters"}[] =& $filters;
1899 function _prependFilterToChain($condition, $filters)
1901 array_unshift($this->{"_{$condition}Filters"}, $filters);
1904 function _ensureFilterRespondsToBeforeAndAfter(&$filter_object)
1906 if(!method_exists(&$filter_object,'before') && !method_exists(&$filter_object,'after')){
1907 trigger_error(Ak::t('Filter object must respond to both before and after'), E_USER_ERROR);
1911 function _extractConditions(&$filters)
1913 if(is_array($filters) && !isset($filters[0])){
1914 $keys = array_keys($filters);
1915 $conditions = $filters[$keys[0]];
1916 $filters = $keys[0];
1917 return $conditions;
1921 function _addActionConditions($filters, $conditions)
1923 if(!empty($conditions['only'])){
1924 $this->_includedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_includedActions, $conditions['only']);
1926 if(!empty($conditions['except'])){
1927 $this->_excludedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_excludedActions, $conditions['except']);
1931 function _conditionArray($actions, $filter_actions)
1933 $filter_actions = is_array($filter_actions) ? $filter_actions : array($filter_actions);
1934 $filter_actions = array_map(array(&$this,'_filterId'),$filter_actions);
1935 return array_unique(array_merge($actions, $filter_actions));
1939 function _filterId($filters)
1941 return is_string($filters) ? $filters : md5(serialize($filters));
1944 function performActionWithoutFilters($action)
1946 if(method_exists(&$this, $action)){
1947 call_user_func_array(array(&$this, $action), @(array)$this->passed_args);
1951 function performActionWithFilters($method = '')
1953 if ($this->beforeAction($method) !== false && !$this->_hasPerformed()){
1954 $this->performActionWithoutFilters($method);
1955 $this->afterAction($method);
1956 return true;
1958 return false;
1961 function performAction($method = '')
1963 $this->performActionWithFilters($method);
1968 * Calls all the defined before-filter filters, which are added by using "beforeFilter($method)".
1969 * If any of the filters return false, no more filters will be executed and the action is aborted.
1971 function beforeAction($method = '')
1973 Ak::profile('Running before controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__);
1974 return $this->_callFilters($this->_beforeFilters, $method);
1978 * Calls all the defined after-filter filters, which are added by using "afterFilter($method)".
1979 * If any of the filters return false, no more filters will be executed.
1981 function afterAction($method = '')
1983 Ak::profile('Running after controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__);
1984 return $this->_callFilters(&$this->_afterFilters, $method);
1988 function _callFilters(&$filters, $method = '')
1990 $filter_result = null;
1991 foreach (array_keys($filters) as $k){
1992 $filter =& $filters[$k];
1993 if(!$this->_actionIsExempted($filter, $method)){
1994 if(is_array($filter) && is_object($filter[0]) && method_exists($filter[0], $filter[1])){
1995 $filter_result = $filter[0]->$filter[1]($this);
1996 }elseif(!is_object($filter) && method_exists($this, $filter)){
1997 $filter_result = $this->$filter($this);
1998 }elseif(is_object($filter) && method_exists($filter, 'filter')){
1999 $filter_result = $filter->filter($this);
2000 }else{
2001 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);
2005 if($filter_result === false){
2006 !empty($this->_Logger) ? $this->_Logger->info(Ak::t('Filter chain halted as '.$filter.' returned false')) : null;
2007 return false;
2010 return $filter_result;
2014 function _actionIsExempted($filter, $method = '')
2016 $method_id = is_string($method) ? $method : $this->_filterId($method);
2017 $filter_id = $this->_filterId($filter);
2019 if((!empty($this->_includedActions[$filter_id]) && !in_array($method_id, $this->_includedActions[$filter_id])) ||
2020 (!empty($this->_excludedActions[$filter_id]) && in_array($method_id, $this->_excludedActions[$filter_id]))){
2021 return true;
2024 return false;
2030 Flash communication between actions
2031 ====================================================================
2033 * The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
2034 * to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
2035 * that sets <tt>flash['notice] = 'Successfully created'</tt> before redirecting to a display action that can then expose
2036 * the flash to its template. Actually, that exposure is automatically done. Example:
2038 * class WeblogController extends ActionController
2040 * function create()
2042 * // save post
2043 * $this->flash['notice] = 'Successfully created post';
2044 * $this->redirectTo(array('action'=>'display','params' => array('id' =>$Post->id)));
2047 * function display()
2049 * // doesn't need to assign the flash notice to the template, that's done automatically
2053 * display.tpl
2054 * <?php if($flash['notice']) : ?><div class='notice'><?php echo $flash['notice'] ?></div><?php endif; ?>
2056 * 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
2057 * as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
2059 * ==flash_now
2061 * Sets a flash that will not be available to the next action, only to the current.
2063 * $this->flash_now['message] = 'Hello current action';
2065 * This method enables you to use the flash as a central messaging system in your app.
2066 * When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
2067 * When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
2068 * vanish when the current action is done.
2070 * Entries set via <tt>flash_now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
2072 var $flash = array();
2073 var $flash_now = array();
2074 var $flash_options = array();
2075 var $_flash_handled = false;
2077 function _handleFlashAttribute()
2079 $this->_flash_handled = true;
2081 $next_flash = empty($this->flash) ? false : $this->flash;
2082 $this->flash = array();
2083 if(isset($_SESSION['__flash'])){
2084 $this->flash = $_SESSION['__flash'];
2086 $_SESSION['__flash'] = $next_flash;
2087 if(!empty($this->flash_now)){
2088 $this->flash = array_merge((array)$this->flash,(array)$this->flash_now);
2090 $this->_handleFlashOptions();
2093 function _handleFlashOptions()
2095 $next_flash_options = empty($this->flash_options) ? false : $this->flash_options;
2096 $this->flash_options = array();
2097 if(isset($_SESSION['__flash_options'])){
2098 $this->flash_options = $_SESSION['__flash_options'];
2100 $_SESSION['__flash_options'] = $next_flash_options;
2101 if(!empty($this->flash_now_options)){
2102 $this->flash_options = array_merge((array)$this->flash_options,(array)$this->flash_now_options);
2107 function _mergeFlashOnFlashNow()
2109 $this->flash_now = array_merge($this->flash_now,$this->flash);
2114 Pagination for Active Record collections
2115 ====================================================================
2117 * The Pagination module aids in the process of paging large collections of
2118 * Active Record objects. It offers macro-style automatic fetching of your
2119 * model for multiple views, or explicit fetching for single actions. And if
2120 * the magic isn't flexible enough for your needs, you can create your own
2121 * paginators with a minimal amount of code.
2123 * The Pagination module can handle as much or as little as you wish. In the
2124 * controller, have it automatically query your model for pagination; or,
2125 * if you prefer, create Paginator objects yourself
2127 * Pagination is included automatically for all controllers.
2129 * For help rendering pagination links, see
2130 * Helpers/PaginationHelper.
2132 * ==== Automatic pagination for every action in a controller
2134 * class PersonController extends ApplicationController
2136 * var $model = 'person';
2137 * var $paginate = array('people'=>array('order' => 'last_name, first_name',
2138 * 'per_page' => 20));
2141 * Each action in this controller now has access to a <tt>$this->people</tt>
2142 * instance variable, which is an ordered collection of model objects for the
2143 * current page (at most 20, sorted by last name and first name), and a
2144 * <tt>$this->person_pages</tt> Paginator instance. The current page is determined
2145 * by the <tt>$params['page']</tt> variable.
2147 * ==== Pagination for a single action
2149 * function show_all()
2151 * list($this->person_pages, $this->people) =
2152 * $this->paginate('people', array('order' => 'last_name, first_name'));
2155 * Like the previous example, but explicitly creates <tt>$this->person_pages</tt>
2156 * and <tt>$this->people</tt> for a single action, and uses the default of 10 items
2157 * per page.
2159 * ==== Custom/"classic" pagination
2161 * function list()
2163 * $this->person_pages = new AkPaginator(&$this, $Person->count(), 10, $params['page']);
2164 * $this->people = $this->Person->find('all', array(
2165 * 'order'=> 'last_name, first_name',
2166 * 'limit' => $this->person_pages->items_per_page,
2167 * 'offset' => $this->person_pages->getOffset()));
2170 * Explicitly creates the paginator from the previous example and uses
2171 * AkPaginator::toSql to retrieve <tt>$this->people</tt> from the model.
2173 // An array holding options for controllers using macro-style pagination
2174 var $_pagination_options = array(
2175 'class_name' => null,
2176 'singular_name' => null,
2177 'per_page' => 10,
2178 'conditions' => null,
2179 'order_by' => null,
2180 'order' => null,
2181 'join' => null,
2182 'joins' => null,
2183 'include' => null,
2184 'select' => null,
2185 'parameter' => 'page'
2188 // The default options for pagination
2189 var $_pagination_default_options = array(
2190 'class_name' => null,
2191 'singular_name' => null,
2192 'per_page' => 10,
2193 'conditions' => null,
2194 'order_by' => null,
2195 'order' => null,
2196 'join' => null,
2197 'joins' => null,
2198 'include' => null,
2199 'select' => null,
2200 'parameter' => 'page'
2203 var $_pagination_actions = array();
2205 function _paginationValidateOptions($collection_id, $options = array(), $in_action)
2207 $this->_pagination_options = array_merge($this->_pagination_default_options, $this->_pagination_options);
2208 $valid_options = array_keys($this->_pagination_default_options);
2210 $valid_options = !in_array($in_action, $valid_options) ? array_merge($valid_options, $this->_pagination_actions) : $valid_options;
2212 $unknown_option_keys = array_diff(array_keys($this->_pagination_options) , $valid_options);
2214 if(!empty($unknown_option_keys)){
2215 trigger_error(Ak::t('Unknown options for pagination: %unknown_option',array('%unknown_option'=>join(', ',$unknown_option_keys))), E_USER_WARNING);
2218 $this->_pagination_options['singular_name'] = !empty($this->_pagination_options['singular_name']) ? $this->_pagination_options['singular_name'] : AkInflector::singularize($collection_id);
2219 $this->_pagination_options['class_name'] = !empty($this->_pagination_options['class_name']) ? $this->_pagination_options['class_name'] : AkInflector::camelize($this->_pagination_options['singular_name']);
2223 * Returns a paginator and a collection of Active Record model instances
2224 * for the paginator's current page. This is designed to be used in a
2225 * single action.
2227 * +options+ are:
2228 * <tt>singular_name</tt>:: the singular name to use, if it can't be inferred by
2229 * singularizing the collection name
2230 * <tt>class_name</tt>:: the class name to use, if it can't be inferred by
2231 * camelizing the singular name
2232 * <tt>per_page</tt>:: the maximum number of items to include in a
2233 * single page. Defaults to 10
2234 * <tt>conditions</tt>:: optional conditions passed to Model::find('all', $this->params); and
2235 * Model::count()
2236 * <tt>order</tt>:: optional order parameter passed to Model::find('all', $this->params);
2237 * <tt>order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model::find('all', $this->params)
2238 * <tt>joins</tt>:: optional joins parameter passed to Model::find('all', $this->params)
2239 * and Model::count()
2240 * <tt>join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model::find('all', $this->params)
2241 * and Model::count()
2242 * <tt>include</tt>:: optional eager loading parameter passed to Model::find('all', $this->params)
2243 * and Model::count()
2245 * Creates a +before_filter+ which automatically paginates an Active
2246 * Record model for all actions in a controller (or certain actions if
2247 * specified with the <tt>actions</tt> option).
2249 * +options+ are the same as PaginationHelper::paginate, with the addition
2250 * of:
2251 * <tt>actions</tt>:: an array of actions for which the pagination is
2252 * active. Defaults to +null+ (i.e., every action)
2254 function paginate($collection_id, $options = array())
2256 $this->_paginationValidateOptions($collection_id, $options, true);
2257 $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options);
2258 $this->beforeFilter('_paginationCreateAndRetrieveCollections');
2262 function _paginationCreateAndRetrieveCollections()
2264 foreach($this->_pagination_options[$this->class] as $collection_id=>$options){
2265 if(!empty($options['actions']) && in_array($options['actions'], $action_name)){
2266 continue;
2269 list($paginator, $collection) = $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options);
2271 $this->{$options['singular_name'].'_pages'} =& $paginator;
2272 $this->$collection_name =& $collection;
2277 * Returns the total number of items in the collection to be paginated for
2278 * the +model+ and given +conditions+. Override this method to implement a
2279 * custom counter.
2281 function _paginationCountCollection(&$model, $conditions, $joins)
2283 return $model->count($conditions, $joins);
2287 * Returns a collection of items for the given +$model+ and +$options['conditions']+,
2288 * ordered by +$options['order']+, for the current page in the given +$paginator+.
2289 * Override this method to implement a custom finder.
2291 function _paginationFindCollection(&$model, $options, &$paginator)
2293 return $model->find('all', array(
2294 'conditions' => $this->_pagination_options['conditions'],
2295 'order' => !empty($options['order_by']) ? $options['order_by'] : $options['order'],
2296 'joins' => !empty($options['join']) ? $options['join'] : $options['joins'],
2297 'include' => $this->_pagination_options['include'],
2298 'select' => $this->_pagination_options['select'],
2299 'limit' => $this->_pagination_options['per_page'],
2300 'offset' => $paginator->getOffset()));
2304 * @todo Fix this function
2306 function _paginationLoadPaginatorAndCollection($collection_id, $options)
2308 $page = $this->params[$options['parameter']];
2309 $count = $this->_paginationCountCollection($klass, $options['conditions'],
2310 empty($options['join']) ? $options['join'] : $options['joins']);
2312 require_once(AK_LIB_DIR.DS.'AkActionController'.DS.'AkPaginator.php');
2313 $paginator =& new AkPaginator($this, $count, $options['per_page'], $page);
2314 $collection =& $this->_paginationFindCollection($options['class_name'], $options, $paginator);
2316 return array(&$paginator, &$collection);
2320 Protocol conformance
2321 ====================================================================
2325 * Specifies that the named actions requires an SSL connection to be performed (which is enforced by ensure_proper_protocol).
2327 function setSslRequiredActions($actions)
2329 $this->_ssl_required_actions = empty($this->_ssl_required_actions) ?
2330 (is_string($actions) ? Ak::stringToArray($actions) : $actions) :
2331 array_merge($this->_ssl_required_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions));
2334 function setSslAllowedActions($actions)
2336 $this->_ssl_allowed_actions = empty($this->_ssl_allowed_actions) ?
2337 (is_string($actions) ? Ak::stringToArray($actions) : $actions) :
2338 array_merge($this->_ssl_allowed_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions));
2342 * Returns true if the current action is supposed to run as SSL
2344 function _isSslRequired()
2346 return !empty($this->_ssl_required_actions) && is_array($this->_ssl_required_actions) && isset($this->_action_name) ?
2347 in_array($this->_action_name, $this->_ssl_required_actions) : false;
2350 function _isSslAllowed()
2352 return (!empty($this->ssl_for_all_actions) && empty($this->_ssl_allowed_actions)) ||
2353 (!empty($this->_ssl_allowed_actions) && is_array($this->_ssl_allowed_actions) && isset($this->_action_name) ?
2354 in_array($this->_action_name, $this->_ssl_allowed_actions) : false);
2357 function _ensureProperProtocol()
2359 if($this->_isSslAllowed()){
2360 return true;
2362 if ($this->_isSslRequired() && !$this->Request->isSsl()){
2363 $this->redirectTo(substr_replace(AK_CURRENT_URL,'s:',4,1));
2364 return false;
2365 }elseif($this->Request->isSsl() && !$this->_isSslRequired()){
2366 $this->redirectTo(substr_replace(AK_CURRENT_URL,'',4,1));
2367 return false;
2372 Account Location
2373 ====================================================================
2375 * Account location is a set of methods that supports the account-key-as-subdomain
2376 * way of identifying the current scope. These methods allow you to easily produce URLs that
2377 * match this style and to get the current account key from the subdomain.
2379 * The methods are: getAccountUrl, getAccountHost, and getAccountDomain.
2381 * Example:
2383 * include_once('AkAccountLocation.php');
2385 * class ApplicationController extends AkActionController
2387 * var $before_filter = '_findAccount';
2389 * function _findAccount()
2391 * $this->account = Account::find(array('conditions'=>array('username = ?', $this->account_domain)));
2394 * class AccountController extends ApplicationController
2396 * function new_account()
2398 * $this->new_account = Account::create($this->params['new_account']);
2399 * $this->redirectTo(array('host' => $this->accountHost($this->new_account->username), 'controller' => 'weblog'));
2402 * function authenticate()
2404 * $this->session[$this->account_domain] = 'authenticated';
2405 * $this->redirectTo(array('controller => 'weblog'));
2408 * function _isAuthenticated()
2410 * return !empty($this->session['account_domain']) ? $this->session['account_domain'] == 'authenticated' : false;
2414 * // The view:
2415 * Your domain: {account_url?}
2417 * By default, all the methods will query for $this->account->username as the account key, but you can
2418 * specialize that by overwriting defaultAccountSubdomain. You can of course also pass it in
2419 * as the first argument to all the methods.
2421 function defaultAccountSubdomain()
2423 if(!empty($this->account)){
2424 return $this->account->respondsTo('username');
2428 function accountUrl($account_subdomain = null, $use_ssl = null)
2430 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain;
2431 $use_ssl = empty($use_ssl) ? $use_ssl : $this->Request->isSsl();
2432 return ($use_ssl ? 'https://' : 'http://') . $this->accountHost($account_subdomain);
2435 function accountHost($account_subdomain = null)
2437 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain;
2438 $account_host = '';
2439 $account_host .= $account_subdomain . '.';
2440 $account_host .= $this->accountDomain();
2441 return $account_host;
2444 function accountDomain()
2446 $account_domain = '';
2447 if(count($this->Request->getSubdomains()) > 1){
2448 $account_domain .= join('.',$this->Request->getSubdomains()) . '.';
2450 $account_domain .= $this->Request->getDomain() . $this->Request->getPortString();
2451 return $account_domain;
2454 function getAccountSubdomain()
2456 return array_shift($this->Request->getSubdomains());
2461 Data streaming
2462 ====================================================================
2463 Methods for sending files and streams to the browser instead of rendering.
2466 var $default_send_file_options = array(
2467 'type' => 'application/octet-stream',
2468 'disposition' => 'attachment',
2469 'stream' => true,
2470 'buffer_size' => 4096
2474 * Sends the file by streaming it 4096 bytes at a time. This way the
2475 * whole file doesn't need to be read into memory at once. This makes
2476 * it feasible to send even large files.
2478 * Be careful to sanitize the path parameter if it coming from a web
2479 * page. sendFile($params['path']) allows a malicious user to
2480 * download any file on your server.
2482 * Options:
2483 * * <tt>filename</tt> - suggests a filename for the browser to use.
2484 * Defaults to realpath($path).
2485 * * <tt>type</tt> - specifies an HTTP content type.
2486 * Defaults to 'application/octet-stream'.
2487 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded.
2488 * Valid values are 'inline' and 'attachment' (default).
2489 * * <tt>stream</tt> - whether to send the file to the user agent as it is read (true)
2490 * or to read the entire file before sending (false). Defaults to true.
2491 * * <tt>buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
2492 * Defaults to 4096.
2494 * The default Content-Type and Content-Disposition headers are
2495 * set to download arbitrary binary files in as many browsers as
2496 * possible. IE versions 4, 5, 5.5, and 6 are all known to have
2497 * a variety of quirks (especially when downloading over SSL).
2499 * Simple download:
2500 * sendFile('/path/to.zip');
2502 * Show a JPEG in browser:
2503 * sendFile('/path/to.jpeg', array('type' => 'image/jpeg', 'disposition' => 'inline'));
2505 * Read about the other Content-* HTTP headers if you'd like to
2506 * provide the user with more information (such as Content-Description).
2507 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
2509 * Also be aware that the document may be cached by proxies and browsers.
2510 * The Pragma and Cache-Control headers declare how the file may be cached
2511 * by intermediaries. They default to require clients to validate with
2512 * the server before releasing cached responses. See
2513 * http://www.mnot.net/cache_docs/ for an overview of web caching and
2514 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
2515 * for the Cache-Control header spec.
2517 function sendFile($path, $options = array())
2519 $path = realpath($path);
2520 if(!file_exists($path)){
2521 trigger_error(Ak::t('Cannot read file %path',array('%path'=>$path)), E_USER_NOTICE);
2522 return false;
2524 $options['length'] = empty($options['length']) ? filesize($path) : $options['length'];
2525 $options['filename'] = empty($options['filename']) ? basename($path) : $options['filename'];
2526 $options['type'] = empty($options['type']) ? Ak::mime_content_type($path) : $options['type'];
2528 $this->performed_render = false;
2529 $this->_sendFileHeaders($options);
2531 if(!empty($options['stream'])){
2532 require_once(AK_LIB_DIR.DS.'AkStream.php');
2533 $this->render(array('text'=> new AkStream($path,$options['buffer_size'])));
2534 }else{
2535 $this->render(array('text'=> Ak::file_get_contents($path)));
2540 * Send binary data to the user as a file download. May set content type, apparent file name,
2541 * and specify whether to show data inline or download as an attachment.
2543 * Options:
2544 * * <tt>filename</tt> - Suggests a filename for the browser to use.
2545 * * <tt>type</tt> - specifies an HTTP content type.
2546 * Defaults to 'application/octet-stream'.
2547 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded.
2548 * Valid values are 'inline' and 'attachment' (default).
2550 * Generic data download:
2551 * sendData($buffer)
2553 * Download a dynamically-generated tarball:
2554 * sendData(Ak::compress('dir','tgz'), array('filename' => 'dir.tgz'));
2556 * Display an image Active Record in the browser:
2557 * sendData($image_data, array('type' =>Ak::mime_content_type('image_name.png'), 'disposition' => 'inline'));
2559 * See +sendFile+ for more information on HTTP Content-* headers and caching.
2561 function sendData($data, $options = array())
2563 $options['length'] = empty($options['length']) ? Ak::size($data) : $options['length'];
2564 $this->_sendFileHeaders($options);
2565 $this->performed_render = false;
2566 $this->renderText($data);
2570 * Creates a file for streaming from a file.
2571 * This way you might free memory usage is file is too large
2573 function sendDataAsStream($data, $options)
2575 $temp_file_name = tempnam(AK_TMP_DIR, Ak::randomString());
2576 $fp = fopen($temp_file_name, 'w');
2577 fwrite($fp, $data);
2578 fclose($fp);
2579 $this->sendFile($temp_file_name, $options);
2583 function _sendFileHeaders(&$options)
2585 $options = array_merge($this->default_send_file_options,$options);
2586 foreach (array('length', 'type', 'disposition') as $arg){
2587 empty($options[$arg]) ? trigger_error(Ak::t('%arg option required', array('%arg'=>$arg)), E_USER_ERROR) : null;
2589 $disposition = empty($options['disposition']) ? 'attachment' : $options['disposition'];
2590 $disposition .= !empty($options['filename']) ? '; filename="'.$options['filename'].'"' : '';
2591 $this->Response->addHeader(array(
2592 'Content-Length' => $options['length'],
2593 'Content-Type' => trim($options['type']), // fixes a problem with extra '\r' with some browsers
2594 'Content-Disposition' => $disposition,
2595 'Content-Transfer-Encoding' => 'binary'
2601 function redirectToLocale($locale)
2603 if($this->Request->__internationalization_support_enabled){
2604 $lang = isset($this->params['lang']) ? $this->params['lang'] : $locale;
2606 if($locale != $lang){
2607 $this->redirectTo(array_merge($this->Request->getParams(),array('lang'=>$locale)));
2608 return true;
2611 return false;
2615 function api($protocol = 'xml_rpc')
2617 $web_services = array_merge(Ak::toArray($this->web_services), Ak::toArray($this->web_service));
2618 if(!empty($web_services)){
2619 $web_services = array_unique($web_services);
2620 require_once(AK_LIB_DIR.DS.'AkActionWebService.php');
2621 require_once(AK_LIB_DIR.DS.'AkActionWebService'.DS.'AkActionWebServiceServer.php');
2622 $Server =& new AkActionWebServiceServer($protocol);
2623 foreach ($web_services as $web_service){
2624 $Server->addService($web_service);
2626 $Server->init();
2627 $Server->serve();
2628 exit;
2629 }else{
2630 die(Ak::t('There is not any webservice configured at this endpoint'));
2637 HTTP Authentication
2638 ====================================================================
2640 * Simple Basic example:
2642 * class PostsController extends ApplicationController
2644 * var $_authorized_users = array('bermi' => 'secret');
2646 * function __construct(){
2647 * $this->beforeFilter(array('authenticate' => array('except' => array('index'))));
2650 * function index() {
2651 * $this->renderText("Everyone can see me!");
2654 * function edit(){
2655 * $this->renderText("I'm only accessible if you know the password");
2658 * function authenticate(){
2659 * return $this->_authenticateOrRequestWithHttpBasic('App name', $this->_authorized_users);
2663 * Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
2664 * the regular HTML interface is protected by a session approach:
2666 * class ApplicationController extends AkActionController
2668 * var $models = 'account';
2670 * function __construct() {
2671 * $this->beforeFilter(array('_setAccount', 'authenticate'));
2674 * function _setAccount() {
2675 * $this->Account = $this->account->findFirstBy('url_name', array_pop($this->Request->getSubdomains()));
2678 * function authenticate() {
2679 * if($this->Request->isFormat('XML', 'ATOM')){
2680 * if($User = $this->_authenticateWithHttpBasic($Account)){
2681 * $this->CurrentUser = $User;
2682 * }else{
2683 * $this->_requestHttpBasicAuthentication();
2685 * }else{
2686 * if($this->isSessionAuthenticated()){
2687 * $this->CurrentUser = $Account->user->find($_SESSION['authenticated']['user_id']);
2688 * }else{
2689 * $this->redirectTo(array('controller'=>'login'));
2690 * return false;
2696 * On shared hosts, Apache sometimes doesn't pass authentication headers to
2697 * FCGI instances. If your environment matches this description and you cannot
2698 * authenticate, try this rule in public/.htaccess (replace the plain one):
2700 * RewriteRule ^(.*)$ index.php [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
2703 function _authenticateOrRequestWithHttpBasic($realm = AK_APP_NAME, $login_procedure)
2705 if($Result = $this->_authenticateWithHttpBasic($login_procedure)){
2706 return $Result;
2708 return $this->_requestHttpBasicAuthentication($realm);
2711 function _authenticateWithHttpBasic($login_procedure)
2713 return $this->_authenticate($login_procedure);
2716 function _requestHttpBasicAuthentication($realm = AK_APP_NAME)
2718 return $this->_authenticationRequest($realm);
2722 * This is method takes a $login_procedure for performing access authentication.
2724 * If an array is given, it will check the key for a user and the value will be verified to match given password.
2726 * You can pass and array like array('handler' => $Account, 'method' => 'verifyCredentials'), which will call
2728 * $Account->verifyCredentials($user_name, $password, $Controller)
2730 * You can also pass an object which implements an "authenticate" method. when calling
2732 * $this->_authenticate(new User());
2734 * It will call the $User->authenticate($user_name, $password, $Controller)
2736 * In both cases the authentication method should return true for valid credentials or false is invalid.
2738 * @return bool
2740 function _authenticate($login_procedure)
2742 if(!$this->_authorization()){
2743 return false;
2744 }else{
2745 list($user_name, $password) = $this->_getUserNameAndPassword();
2746 if(is_array($login_procedure)){
2747 if(!isset($login_procedure['handler'])){
2748 return isset($login_procedure[$user_name]) && $login_procedure[$user_name] == $password;
2749 }elseif(is_a($login_procedure['handler']) && method_exists($login_procedure['handler'], $login_procedure['method'])){
2750 return $login_procedure['handler']->$login_procedure['method']($user_name, $password, $this);
2752 }elseif(method_exists($login_procedure, 'authenticate')){
2753 return $login_procedure->authenticate($user_name, $password, $this);
2756 return false;
2759 function _getUserNameAndPassword()
2761 $credentials = $this->_decodeCredentials();
2762 return !is_array($credentials) ? split('/:/', $credentials , 2) : $credentials;
2765 function _authorization()
2767 return
2768 empty($this->Request->env['PHP_AUTH_USER']) ? (
2769 empty($this->Request->env['HTTP_AUTHORIZATION']) ? (
2770 empty($this->Request->env['X-HTTP_AUTHORIZATION']) ? (
2771 empty($this->Request->env['X_HTTP_AUTHORIZATION']) ? (
2772 isset($this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION']) ?
2773 $this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION'] : null
2774 ) : $this->Request->env['X_HTTP_AUTHORIZATION']
2775 ) : $this->Request->env['X-HTTP_AUTHORIZATION']
2776 ) : $this->Request->env['HTTP_AUTHORIZATION']
2777 ) : array($this->Request->env['PHP_AUTH_USER'], $this->Request->env['PHP_AUTH_PW']);
2780 function _decodeCredentials()
2782 $authorization = $this->_authorization();
2783 if(is_array($authorization)){
2784 return $authorization;
2786 $credentials = (array)split(' ', $authorization);
2787 return base64_decode(array_pop($credentials));
2790 function _encodeCredentials($user_name, $password)
2792 return 'Basic '.base64_encode("$user_name:$password");
2795 function _authenticationRequest($realm)
2797 header('WWW-Authenticate: Basic realm="' . str_replace('"','',$realm) . '"');
2799 if(method_exists($this, 'access_denied')){
2800 $this->access_denied();
2801 }else{
2802 header('HTTP/1.0 401 Unauthorized');
2803 echo "HTTP Basic: Access denied.\n";
2804 exit;
2808 function _ensureActionExists()
2810 if(!method_exists($this, $this->_action_name)){
2811 if(AK_ENVIRONMENT == 'development'){
2812 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->error('Action '.$this->_action_name.' not found on '.$this->getControllerName()) : null;
2813 trigger_error(Ak::t('Controller <i>%controller_name</i> can\'t handle action %action_name',
2814 array(
2815 '%controller_name' => $this->getControllerName(),
2816 '%action_name' => $this->_action_name,
2817 )), E_USER_ERROR);
2818 }elseif(@include(AK_PUBLIC_DIR.DS.'405.php')){
2819 exit;
2820 }else{
2821 header("HTTP/1.1 405 Method Not Allowed");
2822 die('405 Method Not Allowed');
2830 * Function for getting the singleton controller;
2832 * @return AkActionController instance
2834 function &AkActionController()
2836 $params = func_num_args() == 0 ? null : func_get_args();
2837 $AkActionController =& Ak::singleton('AkActionController', $params);
2838 return $AkActionController;