/lib/AkActionController.php
PHP | 2846 lines | 1355 code | 290 blank | 1201 comment | 177 complexity | d9d05d2b37031605d95621cb9c8f3024 MD5 | raw file
1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */ 3 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// +----------------------------------------------------------------------+ 10 11require_once(AK_LIB_DIR.DS.'AkObject.php'); 12 13defined('AK_HIGH_LOAD_MODE') ? null : define('AK_HIGH_LOAD_MODE', false); 14defined('AK_APP_NAME') ? null : define('AK_APP_NAME', 'Application'); 15 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> 22 */ 23 24class AkActionController extends AkObject 25{ 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; 30 31 var $_ssl_requirement = false; 32 var $_ssl_allowed_actions = array(); 33 var $ssl_for_all_actions = true; 34 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. 38 */ 39 var $_view_controller_internals = true; 40 41 /** 42 * Protected instance variable cache 43 */ 44 var $_protected_variables_cache = array(); 45 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'; 52 */ 53 var $asset_host = AK_ASSET_HOST; 54 55 56 var $_Logger; 57 58 /** 59 * Determines which template class should be used by AkActionController. 60 */ 61 var $TemplateClass; 62 63 /** 64 * Turn on +_ignore_missing_templates+ if you want to unit test actions without 65 * making the associated templates. 66 */ 67 var $_ignore_missing_templates; 68 69 /** 70 * Holds the Request object that's primarily used to get environment variables 71 */ 72 var $Request; 73 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. 78 */ 79 var $params = array(); 80 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. 87 */ 88 var $Response; 89 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. 94 */ 95 var $session; 96 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. 100 */ 101 var $_headers = array(); 102 103 /** 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. 107 */ 108 var $_assigns = array(); 109 110 /** 111 * Holds the name of the action this controller is processing. 112 */ 113 var $_action_name; 114 115 var $cookies; 116 117 var $helpers = 'default'; 118 119 var $app_helpers; 120 var $plugin_helpers = 'all'; 121 122 var $web_service; 123 var $web_services = array(); 124 125 var $web_service_api; 126 var $web_service_apis = array(); 127 128 var $module_name; 129 var $_module_path; 130 131 /** 132 * Old fashioned way of dispatching requests. Please use AkDispatcher or roll your own. 133 * 134 * @deprecated 135 */ 136 function handleRequest() 137 { 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(); 143 } 144 145 function process(&$Request, &$Response) 146 { 147 AK_LOG_EVENTS && empty($this->_Logger) ? ($this->_Logger =& Ak::getLogger()) : null; 148 149 $this->Request =& $Request; 150 $this->Response =& $Response; 151 $this->params = $this->Request->getParams(); 152 $this->_action_name = $this->Request->getAction(); 153 154 $this->_ensureActionExists(); 155 156 Ak::t('Akelos'); // We need to get locales ready 157 158 if($this->_high_load_mode !== true){ 159 if(!empty($this->_auto_instantiate_models)){ 160 $this->instantiateIncludedModelClasses(); 161 } 162 if(!empty($this->_enable_plugins)){ 163 $this->loadPlugins(); 164 } 165 if(!empty($this->helpers)){ 166 $this->instantiateHelpers(); 167 } 168 }else{ 169 $this->_enableLayoutOnRender = false; 170 } 171 172 $this->_ensureProperProtocol(); 173 174 // After filters 175 $this->afterFilter('_handleFlashAttribute'); 176 177 $this->_loadActionView(); 178 179 if(isset($this->api)){ 180 require_once(AK_LIB_DIR.DS.'AkActionWebService.php'); 181 $this->aroundFilter(new AkActionWebService($this)); 182 } 183 184 $this->performActionWithFilters($this->_action_name); 185 186 if (!$this->_hasPerformed()){ 187 $this->_enableLayoutOnRender ? $this->renderWithLayout() : $this->renderWithoutLayout(); 188 } 189 190 if(!empty($this->validate_output)){ 191 $this->_validateGeneratedXhtml(); 192 } 193 194 $this->Response->outputResults(); 195 } 196 197 function _loadActionView() 198 { 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; 204 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()); 210 211 $this->Template->_controllerInstance =& $this; 212 $this->Template->_registerTemplateHandler('tpl','AkPhpTemplateHandler'); 213 } 214 } 215 216 function loadPlugins() 217 { 218 Ak::loadPlugins(); 219 } 220 221 /** 222 * Creates an instance of each available helper and links it into into current controller. 223 * 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. 227 * 228 * Helpers can be found at lib/AkActionView/helpers (this might change in a future) 229 */ 230 function instantiateHelpers() 231 { 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()); 237 238 require_once(AK_LIB_DIR.DS.'AkActionView'.DS.'AkActionViewHelper.php'); 239 240 $available_helpers = array(); 241 foreach ($helpers as $file=>$helper){ 242 $helper_class_name = AkInflector::camelize(AkInflector::demodulize(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 247 if(class_exists($helper_class_name)){ 248 $attribute_name = $full_path ? AkInflector::underscore($helper_class_name) : substr($file,0,-4); 249 $available_helpers[] = $attribute_name; 250 $this->$attribute_name =& new $helper_class_name(&$this); 251 if(method_exists($this->$attribute_name,'setController')){ 252 $this->$attribute_name->setController(&$this); 253 } 254 if(method_exists($this->$attribute_name,'init')){ 255 $this->$attribute_name->init(); 256 } 257 } 258 } 259 defined('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS') ? null : define('AK_ACTION_CONTROLLER_AVAILABLE_HELPERS', join(',',$available_helpers)); 260 } 261 262 function getCurrentControllerHelper() 263 { 264 $helper = $this->getControllerName(); 265 $helper = AkInflector::is_plural($helper)?AkInflector::singularize($helper):$helper; 266 $helper_file_name = AK_HELPERS_DIR.DS.$this->_module_path.AkInflector::underscore($helper).'_helper.php'; 267 268 if(file_exists($helper_file_name)){ 269 return array($helper_file_name => $helper); 270 } 271 return array(); 272 } 273 274 function getModuleHelper() 275 { 276 $this->getControllerName(); // module name is set when we first retrieve the controller name 277 if(!empty($this->module_name)){ 278 $helper_file_name = AK_HELPERS_DIR.DS.AkInflector::underscore($this->module_name).'_helper.php'; 279 if(file_exists($helper_file_name)){ 280 return array($helper_file_name => $this->module_name); 281 } 282 } 283 return array(); 284 } 285 286 function getDefaultHelpers() 287 { 288 if($this->helpers == 'default'){ 289 $available_helpers = Ak::dir(AK_LIB_DIR.DS.'AkActionView'.DS.'helpers',array('dirs'=>false)); 290 $helper_names = array(); 291 foreach ($available_helpers as $available_helper){ 292 $helper_names[$available_helper] = AkInflector::classify(substr($available_helper,0,-10)); 293 } 294 return $helper_names; 295 }else{ 296 $this->helpers = Ak::toArray($this->helpers); 297 } 298 return $this->helpers; 299 } 300 301 function getApplicationHelpers() 302 { 303 $helper_names = array(); 304 if ($this->app_helpers == 'all'){ 305 $available_helpers = Ak::dir(AK_HELPERS_DIR,array('dirs'=>false)); 306 $helper_names = array(); 307 foreach ($available_helpers as $available_helper){ 308 $helper_names[AK_HELPERS_DIR.DS.$available_helper] = AkInflector::classify(substr($available_helper,0,-10)); 309 } 310 311 } elseif (!empty($this->app_helpers)){ 312 foreach (Ak::toArray($this->app_helpers) as $helper_name){ 313 $helper_names[AK_HELPERS_DIR.DS.AkInflector::underscore($helper_name).'_helper.php'] = AkInflector::camelize($helper_name); 314 } 315 } 316 return $helper_names; 317 } 318 319 function getPluginHelpers() 320 { 321 $helper_names = AkActionController::addPluginHelper(false); // Trick for getting helper names set by AkPlugin::addHelper 322 if(empty($helper_names)){ 323 return array(); 324 }elseif ($this->plugin_helpers == 'all'){ 325 return $helper_names; 326 }else { 327 $selected_helper_names = array(); 328 foreach (Ak::toArray($this->plugin_helpers) as $helper_name){ 329 $helper_name = AkInflector::camelize($helper_name); 330 if($path = array_shift(array_keys($helper_names, AkInflector::camelize($helper_name)))){ 331 $selected_helper_names[$path] = $helper_names[$path]; 332 } 333 } 334 return $selected_helper_names; 335 } 336 } 337 338 /** 339 * Used for adding helpers to the base class like those added by the plugins engine. 340 * 341 * @param string $helper_name Helper class name like CalendarHelper 342 * @param array $options - path: Path to the helper class, defaults to AK_PLUGINS_DIR/helper_name/lib/helper_name.php 343 */ 344 function addPluginHelper($helper_name, $options = array()) 345 { 346 static $helpers = array(); 347 if($helper_name === false){ 348 return $helpers; 349 } 350 $underscored_helper_name = AkInflector::underscore($helper_name); 351 $default_options = array( 352 'path' => AK_PLUGINS_DIR.DS.$underscored_helper_name.DS.'lib'.DS.$underscored_helper_name.'.php' 353 ); 354 $options = array_merge($default_options, $options); 355 $helpers[$options['path']] = $helper_name; 356 } 357 358 function _validateGeneratedXhtml() 359 { 360 require_once(AK_LIB_DIR.DS.'AkXhtmlValidator.php'); 361 $XhtmlValidator = new AkXhtmlValidator(); 362 if($XhtmlValidator->validate($this->Response->body) === false){ 363 $this->Response->sendHeaders(); 364 echo '<h1>'.Ak::t('Ooops! There are some errors on current XHTML page').'</h1>'; 365 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"; 366 $XhtmlValidator->showErrors(); 367 echo "<hr /><h2>".Ak::t('Showing XHTML code')."</h2><hr /><div style='border:5px solid red;margin:5px;padding:15px;'>".$this->Response->body."</pre>"; 368 die(); 369 } 370 } 371 372 373 /** 374 * Methods for loading desired models into this controller 375 */ 376 function setModel($model) 377 { 378 $this->instantiateIncludedModelClasses(array($model)); 379 } 380 381 function setModels($models) 382 { 383 $this->instantiateIncludedModelClasses($models); 384 } 385 386 function instantiateIncludedModelClasses($models = array()) 387 { 388 require_once(AK_LIB_DIR.DS.'AkActiveRecord.php'); 389 require_once(AK_APP_DIR.DS.'shared_model.php'); 390 391 empty($this->model) ? ($this->model = $this->params['controller']) : null; 392 empty($this->models) ? ($this->models = array()) : null; 393 394 $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 396 foreach ($models as $model){ 397 $this->instantiateModelClass($model, (empty($this->finder_options[$model])?array():$this->finder_options[$model])); 398 } 399 } 400 401 function instantiateModelClass($model_class_name, $finder_options = array()) 402 { 403 $underscored_model_class_name = AkInflector::underscore($model_class_name); 404 $controller_name = $this->getControllerName(); 405 $id = empty($this->params[$underscored_model_class_name]['id']) ? 406 (empty($this->params['id']) ? false : 407 (($model_class_name == $controller_name || $model_class_name == AkInflector::singularize($controller_name)) ? $this->params['id'] : false)) : 408 $this->params[$underscored_model_class_name]['id']; 409 410 if(class_exists($model_class_name)){ 411 $underscored_model_class_name = AkInflector::underscore($model_class_name); 412 413 if(!isset($this->$model_class_name) || !isset($this->$underscored_model_class_name)){ 414 if($finder_options !== false && is_numeric($id)){ 415 $model =& new $model_class_name(); 416 if(empty($finder_options)){ 417 $model =& $model->find($id); 418 }else{ 419 $model =& $model->find($id, $finder_options); 420 } 421 }else{ 422 $model =& new $model_class_name(); 423 } 424 if(!isset($this->$model_class_name)){ 425 $this->$model_class_name =& $model; 426 } 427 if(!isset($this->$underscored_model_class_name)){ 428 $this->$underscored_model_class_name =& $model; 429 } 430 } 431 } 432 } 433 434 435 436 /** 437 Rendering content 438 ==================================================================== 439 */ 440 441 /** 442 * Renders the content that will be returned to the browser as the Response body. 443 * 444 * === Rendering an action 445 * 446 * Action rendering is the most common form and the type used automatically by 447 * Action Controller when nothing else is specified. By default, actions are 448 * rendered within the current layout (if one exists). 449 * 450 * * Renders the template for the action "goal" within the current controller 451 * 452 * $this->render(array('action'=>'goal')); 453 * 454 * * Renders the template for the action "short_goal" within the current controller, 455 * but without the current active layout 456 * 457 * $this->render(array('action'=>'short_goal','layout'=>false)); 458 * 459 * * Renders the template for the action "long_goal" within the current controller, 460 * but with a custom layout 461 * 462 * $this->render(array('action'=>'long_goal','layout'=>'spectacular')); 463 * 464 * === Rendering partials 465 * 466 * Partial rendering is most commonly used together with Ajax calls that only update 467 * one or a few elements on a page without reloading. Rendering of partials from 468 * the controller makes it possible to use the same partial template in 469 * both the full-page rendering (by calling it from within the template) and when 470 * sub-page updates happen (from the controller action responding to Ajax calls). 471 * By default, the current layout is not used. 472 * 473 * * Renders the partial located at app/views/controller/_win.tpl 474 * 475 * $this->render(array('partial'=>'win')); 476 * 477 * * Renders the partial with a status code of 500 (internal error) 478 * 479 * $this->render(array('partial'=>'broken','status'=>500)); 480 * 481 * * Renders the same partial but also makes a local variable available to it 482 * 483 * $this->render(array('partial' => 'win', 'locals' => array('name'=>'david'))); 484 * 485 * * Renders a collection of the same partial by making each element of $wins available through 486 * the local variable "win" as it builds the complete Response 487 * 488 * $this->render(array('partial'=>'win','collection'=>$wins)); 489 * 490 * * Renders the same collection of partials, but also renders the win_divider partial in between 491 * each win partial. 492 * 493 * $this->render(array('partial'=>'win','collection'=>$wins,'spacer_template'=>'win_divider')); 494 * 495 * === Rendering a template 496 * 497 * Template rendering works just like action rendering except that it takes a 498 * path relative to the template root. 499 * The current layout is automatically applied. 500 * 501 * * Renders the template located in app/views/weblog/show.tpl 502 * $this->render(array('template'=>'weblog/show')); 503 * 504 * === Rendering a file 505 * 506 * File rendering works just like action rendering except that it takes a 507 * filesystem path. By default, the path is assumed to be absolute, and the 508 * current layout is not applied. 509 * 510 * * Renders the template located at the absolute filesystem path 511 * $this->render(array('file'=>'/path/to/some/template.tpl')); 512 * $this->render(array('file'=>'c:/path/to/some/template.tpl')); 513 * 514 * * Renders a template within the current layout, and with a 404 status code 515 * $this->render(array('file' => '/path/to/some/template.tpl', 'layout' => true, 'status' => 404)); 516 * $this->render(array('file' => 'c:/path/to/some/template.tpl', 'layout' => true, 'status' => 404)); 517 * 518 * * Renders a template relative to the template root and chooses the proper file extension 519 * $this->render(array('file' => 'some/template', 'use_full_path' => true)); 520 * 521 * 522 * === Rendering text 523 * 524 * Rendering of text is usually used for tests or for rendering prepared content, 525 * such as a cache. By default, text 526 * rendering is not done within the active layout. 527 * 528 * * Renders the clear text "hello world" with status code 200 529 * $this->render(array('text' => 'hello world!')); 530 * 531 * * Renders the clear text "Explosion!" with status code 500 532 * $this->render(array('text' => "Explosion!", 'status' => 500 )); 533 * 534 * * Renders the clear text "Hi there!" within the current active layout (if one exists) 535 * $this->render(array('text' => "Explosion!", 'layout' => true)); 536 * 537 * * Renders the clear text "Hi there!" within the layout 538 * * placed in "app/views/layouts/special.tpl" 539 * $this->render(array('text' => "Explosion!", 'layout => "special")); 540 * 541 * 542 * === Rendering an inline template 543 * 544 * Rendering of an inline template works as a cross between text and action 545 * rendering where the source for the template 546 * is supplied inline, like text, but its evaled by PHP, like action. By default, 547 * PHP is used for rendering and the current layout is not used. 548 * 549 * * Renders "hello, hello, hello, again" 550 * $this->render(array('inline' => "<?php echo str_repeat('hello, ', 3).'again'?>" )); 551 * 552 * * Renders "hello david" 553 * $this->render(array('inline' => "<?php echo 'hello ' . $name ?>", 'locals' => array('name' => 'david'))); 554 * 555 * 556 * === Rendering nothing 557 * 558 * Rendering nothing is often convenient in combination with Ajax calls that 559 * perform their effect client-side or 560 * when you just want to communicate a status code. Due to a bug in Safari, nothing 561 * actually means a single space. 562 * 563 * * Renders an empty Response with status code 200 564 * $this->render(array('nothing' => true)); 565 * 566 * * Renders an empty Response with status code 401 (access denied) 567 * $this->render(array('nothing' => true, 'status' => 401)); 568 */ 569 function render($options = null, $status = 200) 570 { 571 if(empty($options['partial']) && $this->_hasPerformed()){ 572 $this->_doubleRenderError(Ak::t("Can only render or redirect once per action")); 573 return false; 574 } 575 576 $this->_flash_handled ? null : $this->_handleFlashAttribute(); 577 578 if(!is_array($options)){ 579 return $this->renderFile(empty($options) ? $this->getDefaultTemplateName() : $options, $status, true); 580 } 581 582 if(!empty($options['text'])){ 583 return $this->renderText($options['text'], @$options['status']); 584 }else{ 585 586 if(!empty($options['file'])){ 587 return $this->renderFile($options['file'], @$options['status'], @$options['use_full_path'], @(array)$options['locals']); 588 }elseif(!empty($options['template'])){ 589 return $this->renderFile($options['template'], @$options['status'], true); 590 }elseif(!empty($options['inline'])){ 591 return $this->renderTemplate($options['inline'], @$options['status'], @$options['type'], @(array)$options['locals']); 592 }elseif(!empty($options['action'])){ 593 return $this->renderAction($options['action'], @$options['status'], @$options['layout']); 594 }elseif(!empty($options['partial'])){ 595 if($options['partial'] === true){ 596 $options['partial'] = !empty($options['template']) ? $options['template'] : $this->getDefaultTemplateName(); 597 } 598 if(!empty($options['collection'])){ 599 return $this->renderPartialCollection($options['partial'], $options['collection'], @$options['spacer_template'], @$options['locals'], @$options['status']); 600 }else{ 601 return $this->renderPartial($options['partial'], @$options['object'], @$options['locals'], @$options['status']); 602 } 603 }elseif(!empty($options['nothing'])){ 604 // Safari doesn't pass the _headers of the return if the Response is zero length 605 return $this->renderText(' ', @$options['status']); 606 }else{ 607 return $this->renderFile($this->getDefaultTemplateName(), @$options['status'], true); 608 } 609 return true; 610 } 611 } 612 613 /** 614 * Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead 615 * of sending it as the Response body to the browser. 616 */ 617 function renderToString($options = null) 618 { 619 $result = $this->render($options); 620 $this->eraseRenderResults(); 621 $this->variables_added = null; 622 $this->Template->_assigns_added = null; 623 return $result; 624 } 625 626 function renderAction($_action_name, $status = null, $with_layout = true) 627 { 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); 633 } 634 } 635 636 function renderFile($template_path, $status = null, $use_full_path = false, $locals = array()) 637 { 638 $this->_addVariablesToAssigns(); 639 $locals = array_merge($locals,$this->_assigns); 640 641 if($use_full_path){ 642 $this->_assertExistanceOfTemplateFile($template_path); 643 } 644 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); 647 } 648 649 function renderTemplate($template, $status = null, $type = 'tpl', $local_assigns = array()) 650 { 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); 654 } 655 656 function renderText($text = null, $status = null) 657 { 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; 662 } 663 664 function renderNothing($status = null) 665 { 666 return $this->renderText(' ', $status); 667 } 668 669 function renderPartial($partial_path = null, $object = null, $local_assigns = null, $status = null) 670 { 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; 678 } 679 680 function renderPartialCollection($partial_name, $collection, $partial_spacer_template = null, $local_assigns = null, $status = null) 681 { 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; 687 } 688 $this->variables_added = false; 689 $this->performed_render = false; 690 691 return $result; 692 } 693 694 function renderWithLayout($template_name = null, $status = null, $layout = null) 695 { 696 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name; 697 return $this->renderWithALayout($template_name, $status, $layout); 698 } 699 700 function renderWithoutLayout($template_name = null, $status = null) 701 { 702 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name; 703 return $this->render($template_name, $status); 704 } 705 706 /** 707 * Clears the rendered results, allowing for another render to be performed. 708 */ 709 function eraseRenderResults() 710 { 711 $this->Response->body = ''; 712 $this->performed_render = false; 713 $this->variables_added = false; 714 } 715 716 function _addVariablesToAssigns() 717 { 718 if(empty($this->variables_added)){ 719 $this->_addInstanceVariablesToAssigns(); 720 $this->variables_added = true; 721 } 722 } 723 724 function _addInstanceVariablesToAssigns() 725 { 726 $this->_protected_variables_cache = array_merge($this->_protected_variables_cache, $this->_getProtectedInstanceVariables()); 727 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; 731 } 732 } 733 } 734 735 function _getProtectedInstanceVariables() 736 { 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'); 742 } 743 744 745 /** 746 * Use this to translate strings in the scope of your controller 747 * 748 * @see Ak::t 749 */ 750 function t($string, $array = null) 751 { 752 return Ak::t($string, $array, AkInflector::underscore($this->getControllerName())); 753 } 754 755 756 757 /** 758 Redirects 759 ==================================================================== 760 */ 761 762 /** 763 * Redirects the browser to the target specified in +options+. This parameter can take one of three forms: 764 * 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"]) 772 * 773 * Examples: 774 * redirectTo(array('action' => 'show', 'id' => 5)); 775 * redirectTo('http://www.akelos.com'); 776 * redirectTo('/images/screenshot.jpg'); 777 * redirectTo('back'); 778 * 779 * The redirection happens as a "302 Moved" header. 780 */ 781 function redirectTo($options = array(), $parameters_for_method_reference = null) 782 { 783 if(is_string($options)) { 784 if(preg_match('/^\w+:\/\/.*/',$options)){ 785 if($this->_hasPerformed()){ 786 $this->_doubleRenderError(); 787 } 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); 797 } 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; 806 } 807 } 808 } 809 810 function redirectToAction($action, $options = array()) 811 { 812 $this->redirectTo(array_merge(array('action'=>$action), $options)); 813 } 814 815 816 /** 817 * This methods are required for retrieving available controllers for URL Routing 818 */ 819 function rewriteOptions($options) 820 { 821 $defaults = $this->defaultUrlOptions($options); 822 if(!empty($this->module_name)){ 823 $defaults['module'] = $this->getModuleName(); 824 } 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); 828 } 829 $options = !empty($defaults) ? array_merge($defaults, $options) : $options; 830 $options['controller'] = empty($options['controller']) ? AkInflector::underscore($this->getControllerName()) : $options['controller']; 831 return $options; 832 } 833 834 function getControllerName() 835 { 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); 840 } 841 $controller_name = substr($current_class_name,0,-10); 842 $this->controller_name = $this->_removeModuleNameFromControllerName($controller_name); 843 } 844 return $this->controller_name; 845 } 846 847 function __getControllerName_PHP4_fix($class_name) 848 { 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]; 853 } 854 855 function getModuleName() 856 { 857 return $this->module_name; 858 } 859 860 function setModuleName($module_name) 861 { 862 return $this->module_name = $module_name; 863 } 864 865 /** 866 * Removes the modules name from the controller if exists and sets it. 867 * 868 * @return $controller_name 869 */ 870 function _removeModuleNameFromControllerName($controller_name) 871 { 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))); 876 } 877 return $controller_name; 878 } 879 880 function _getTemplateBasePath() 881 { 882 return AK_APP_DIR.DS.'views'.DS.(empty($this->_module_path)?'':$this->_module_path).$this->Request->getController(); 883 } 884 885 function _getIncludedControllerNames() 886 { 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)); 891 } 892 } 893 return $controllers; 894 } 895 896 897 898 899 /** 900 URL generation/rewriting 901 ==================================================================== 902 */ 903 904 905 /** 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: 909 * 910 * function defaultUrlOptions($options) 911 * { 912 * return array('project' => ($this->Project->isActive() ? $this->Project->url_name : 'unknown')); 913 * } 914 * 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. 919 */ 920 function defaultUrlOptions($options) 921 { 922 } 923 924 925 /** 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). 928 * 929 * <tt>$this->UrlFor</tt> is used to: 930 * 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 940 * 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>. 943 * 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: 948 * 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' 952 * 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. 958 * 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: 963 * 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 966 * 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>. 969 * 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. 972 * 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'. 979 * 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. 986 * 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: 994 * 995 * $this->UrlFor(array('controller' => 'posts', 'action' => null)); 996 * 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 1000 * 1001 * $this->UrlFor(array('overwrite_params' => array('action' => 'print'))); 1002 * 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. 1006 */ 1007 function urlFor($options = array(), $parameters_for_method_reference = null) 1008 { 1009 return $this->rewrite($this->rewriteOptions($options)); 1010 } 1011 1012 function addToUrl($options = array(), $options_to_exclude = array()) 1013 { 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]); 1018 } 1019 return $this->urlFor($options); 1020 } 1021 1022 function getActionName() 1023 { 1024 return $this->Request->getAction(); 1025 } 1026 1027 1028 function _doubleRenderError($message = null) 1029 { 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); 1031 } 1032 1033 function _hasPerformed() 1034 { 1035 return !empty($this->performed_render) || !empty($this->performed_redirect); 1036 } 1037 1038 function _getRequestOrigin() 1039 { 1040 return $this->Request->remote_ip.' at '.Ak::getDate(); 1041 } 1042 1043 function _getCompleteRequestUri() 1044 { 1045 return $this->Request->protocol . $this->Request->host . $this->Request->request_uri; 1046 } 1047 1048 function _closeSession() 1049 { 1050 !empty($this->session) ? session_write_close() : null; 1051 } 1052 1053 1054 function _hasTemplate($template_name = null) 1055 { 1056 return file_exists(empty($template_name) ? $this->getDefaultTemplateName() : $template_name); 1057 } 1058 1059 function _templateIsPublic($template_name = null) 1060 { 1061 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name; 1062 return $this->Template->fileIsPublic($template_name); 1063 } 1064 1065 function _isTemplateExemptFromLayout($template_name = null) 1066 { 1067 $template_name = empty($template_name) ? $this->getDefaultTemplateName() : $template_name; 1068 return $this->Template->_javascriptTemplateExists($template_name); 1069 } 1070 1071 function _assertExistanceOfTemplateFile($template_name) 1072 { 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; 1078 } 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); 1081 } 1082 } 1083 1084 function getDefaultTemplateName($default_action_name = null) 1085 { 1086 return empty($default_action_name) ? (empty($this->_default_template_name) ? $this->_action_name : $this->_default_template_name) : $default_action_name; 1087 } 1088 1089 function setDefaultTemplateName($template_name) 1090 { 1091 $this->_default_template_name = $template_name; 1092 } 1093 1094 1095 1096 function rewrite($options = array()) 1097 { 1098 return $this->_rewriteUrl($this->_rewritePath($options), $options); 1099 } 1100 1101 1102 function toString() 1103 { 1104 return $this->Request->getProtocol().$this->Request->getHostWithPort(). 1105 $this->Request->getPath().@$this->parameters['controller']. 1106 @$this->parameters['action'].@$this->parameters['inspect']; 1107 } 1108 1109 /** 1110 * Given a path and options, returns a rewritten URL string 1111 */ 1112 function _rewriteUrl($path, $options) 1113 { 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')); 1121 } 1122 1123 $rewritten_url .= empty($options['skip_relative_url_root']) ? $this->Request->getRelativeUrlRoot() : ''; 1124 1125 if(empty($options['skip_url_locale'])){ 1126 $locale = $this->Request->getLocaleFromUrl(); 1127 if(empty($options['lang'])){ 1128 $rewritten_url .= (empty($locale) ? '' : '/').$locale; 1129 } 1130 1131 } 1132 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']; 1137 1138 return $rewritten_url; 1139 } 1140 1141 function _rewriteAuthentication($options) 1142 { 1143 if(!isset($options['user']) && isset($options['password'])){ 1144 return urlencode($options['user']).':'.urlencode($options['password']).'@'; 1145 }else{ 1146 return ''; 1147 } 1148 } 1149 1150 function _rewritePath($options) 1151 { 1152 if(!empty($options['params'])){ 1153 foreach ($options['params'] as $k=>$v){ 1154 $options[$k] = $v; 1155 } 1156 unset($options['params']); 1157 } 1158 if(!empty($options['overwrite_params'])){ 1159 foreach ($options['overwrite_params'] as $k=>$v){ 1160 $options[$k] = $v; 1161 } 1162 unset($options['overwrite_params']); 1163 } 1164 foreach (array('anchor', 'params', 'only_path', 'host', 'protocol', 'trailing_slash', 'skip_relative_url_root') as $k){ 1165 unset($options[$k]); 1166 } 1167 $path = Ak::toUrl($options); 1168 return $path; 1169 } 1170 1171 /** 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. 1175 */ 1176 function buildQueryString($array, $only_keys = null) 1177 { 1178 $array = !empty($only_keys) ? array_keys($array) : $array; 1179 return Ak::toUrl($array); 1180 } 1181 1182 1183 1184 1185 /** 1186 Layouts 1187 ==================================================================== 1188 * 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: 1191 * 1192 * <?php echo $controller->render('shared/header') ?> 1193 * Hello World 1194 * <?php echo $controller->render('shared/footer') ?> 1195 * 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. 1199 * 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: 1203 * 1204 * <!-- The header part of this layout --> 1205 * <?php echo $content_for_layout ?> 1206 * <!-- The footer part of this layout --> 1207 * 1208 * And then you have content pages that look like this: 1209 * 1210 * hello world 1211 * 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: 1215 * 1216 * <!-- The header part of this layout --> 1217 * hello world 1218 * <!-- The footer part of this layout --> 1219 * 1220 * == Accessing shared variables 1221 * 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: 1225 * 1226 * <h1><?php echo $page_title ?></h1> 1227 * <?php echo $content_for_layout ?> 1228 * 1229 * ...and content pages that fulfill these references _at_ rendering time: 1230 * 1231 * <?php $page_title = 'Welcome'; ?> 1232 * Off-world colonies offers you a chance to start a new life 1233 * 1234 * The result after rendering is: 1235 * 1236 * <h1>Welcome</h1> 1237 * Off-world colonies offers you a chance to start a new life 1238 * 1239 * == Automatic layout assignment 1240 * 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. 1254 * 1255 * == Inheritance for layouts 1256 * 1257 * Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples: 1258 * 1259 * class BankController extends AkActionController 1260 * { 1261 * var $layout = 'bank_standard'; 1262 * } 1263 * 1264 * class InformationController extends BankController 1265 * { 1266 * } 1267 * 1268 * class VaultController extends BankController 1269 * { 1270 * var $layout = 'access_level_layout'; 1271 * } 1272 * 1273 * class EmployeeController extends BankController 1274 * { 1275 * var $layout = null; 1276 * } 1277 * 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. 1280 * 1281 * == Types of layouts 1282 * 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. 1286 * 1287 * The method reference is the preferred approach to variable layouts and is used like this: 1288 * 1289 * class WeblogController extends AkActionController 1290 * { 1291 * function __construct() 1292 * { 1293 * $this->setLayout(array(&$this, '_writersAndReaders')); 1294 * } 1295 * 1296 * function index() 1297 * { 1298 * // fetching posts 1299 * } 1300 * 1301 * function _writersAndReaders() 1302 * { 1303 * return is_logged_in() ? 'writer_layout' : 'reader_layout'; 1304 * } 1305 * } 1306 * 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. 1309 * 1310 * The most common way of specifying a layout is still just as a plain template name: 1311 * 1312 * class WeblogController extends AkActionController 1313 * { 1314 * var $layout = 'weblog_standard'; 1315 * } 1316 * 1317 * If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+. 1318 * 1319 * == Conditional layouts 1320 * 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: 1324 * 1325 * class WeblogController extends AkActionController 1326 * { 1327 * function __construct() 1328 * { 1329 * $this->setLayout('weblog_standard', array('except' => 'rss')); 1330 * } 1331 * 1332 * // ... 1333 * 1334 * } 1335 * 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. 1338 * 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>. 1341 * 1342 * == Using a different layout in the action render call 1343 * 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: 1348 * 1349 * class WeblogController extends AkActionController 1350 * { 1351 * function help() 1352 * { 1353 * $this->render(array('action'=>'help/index','layout'=>'help')); 1354 * } 1355 * } 1356 */ 1357 1358 /** 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. 1363 */ 1364 function setLayout($template_name, $conditions = array()) 1365 { 1366 $this->_addLayoutConditions($conditions); 1367 $this->layout = $template_name; 1368 } 1369 1370 function getLayoutConditions() 1371 { 1372 return empty($this->_layout_conditions) ? array() : $this->_layout_conditions; 1373 } 1374 1375 function _addLayoutConditions($conditions) 1376 { 1377 $this->_layout_conditions = $conditions; 1378 } 1379 1380 1381 1382 /** 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. 1387 */ 1388 function getActiveLayout($passed_layout = null) 1389 { 1390 if(empty($passed_layout)){ 1391 $layout = !isset($this->layout) ? AkInflector::underscore($this->getControllerName()) : $this->layout; 1392 }else{ 1393 $layout =& $passed_layout; 1394 } 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; 1401 } 1402 1403 if(!empty($this->active_layout)){ 1404 return strstr($this->active_layout,DS) ? $this->active_layout : 'layouts'.DS.$this->active_layout; 1405 } 1406 } 1407 1408 1409 function renderWithALayout($options = null, $status = null, $layout = null) 1410 { 1411 $template_with_options = !empty($options) && is_array($options); 1412 1413 if($this->_canApplyLayout($template_with_options, $options) && ($layout = $this->_pickLayout($template_with_options, $options, $layout))){ 1414 1415 $options = $template_with_options? array_merge((array)$options,array('layout'=>false)) : $options; 1416 1417 $this->content_for_layout = $this->render($options, $status); 1418 1419 if($template_with_options){ 1420 $status = empty($options['status']) ? $status : $options['status']; 1421 } 1422 1423 $this->eraseRenderResults(); 1424 $this->_addVariablesToAssigns(); 1425 1426 return $this->renderText($this->Template->renderFile($layout, true, &$this->_assigns), $status); 1427 }else{ 1428 return $this->render($options, $status, &$this->_assigns); 1429 } 1430 } 1431 1432 function _canApplyLayout($template_with_options, $options) 1433 { 1434 return !empty($template_with_options) ? $this->_isCandidateForLayout($options) : !$this->_isTemplateExemptFromLayout(); 1435 } 1436 1437 function _isCandidateForLayout($options) 1438 { 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'])); 1442 } 1443 1444 function _pickLayout($template_with_options, $options, $layout = null) 1445 { 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; 1450 } 1451 if(!empty($layout)){ 1452 1453 $layout = strstr($layout,'/') || strstr($layout,DS) ? $layout : 'layouts'.DS.$layout; 1454 $layout = preg_replace('/\.tpl$/', '', $layout); 1455 1456 $layout = substr($layout,0,7) === 'layouts' ? 1457 (empty($this->_module_path) || !empty($this->layout) ? AK_VIEWS_DIR.DS.$layout.'.tpl' : AK_VIEWS_DIR.DS.'layouts'.DS.trim($this->_module_path, DS).'.tpl') : 1458 $layout.'.tpl'; 1459 1460 if (file_exists($layout)) { 1461 return $layout; 1462 } 1463 $layout = null; 1464 } 1465 if(empty($layout) && $layout !== false && defined('AK_DEFAULT_LAYOUT')){ 1466 $layout = AK_VIEWS_DIR.DS.'layouts'.DS.AK_DEFAULT_LAYOUT.'.tpl'; 1467 } 1468 return file_exists($layout) ? $layout : false; 1469 } 1470 1471 function _doesActionHasLayout() 1472 { 1473 $conditions = $this->getLayoutConditions(); 1474 1475 $action_name = $this->Request->getAction(); 1476 if(!empty($conditions['only']) && ((is_array($conditions['only']) && in_array($action_name,$conditions['only'])) || 1477 (is_string($conditions['only']) && $action_name == $conditions['only']))){ 1478 return true; 1479 }elseif (!empty($conditions['only'])){ 1480 return false; 1481 } 1482 if(!empty($conditions['except']) && ((is_array($conditions['except']) && in_array($action_name,$conditions['except'])) || 1483 (is_string($conditions['except']) && $action_name == $conditions['except']))){ 1484 return false; 1485 } 1486 1487 return true; 1488 } 1489 1490 1491 1492 1493 /** 1494 Filters 1495 ==================================================================== 1496 * 1497 * Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do 1498 * authentication, caching, or auditing before the intended action is performed. Or to do localization or output 1499 * compression after the action has been performed. 1500 * 1501 * Filters have access to the request, response, and all the instance variables set by other filters in the chain 1502 * or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>beforeFilter</tt> 1503 * to halt the processing before the intended action is processed by returning false or performing a redirect or render. 1504 * This is especially useful for filters like authentication where you're not interested in allowing the action to be 1505 * performed if the proper credentials are not in order. 1506 * 1507 * == Filter inheritance 1508 * 1509 * Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without 1510 * affecting the superclass. For example: 1511 * 1512 * class BankController extends AkActionController 1513 * { 1514 * function __construct() 1515 * { 1516 * $this->beforeFilter('_audit'); 1517 * } 1518 * 1519 * function _audit(&$controller) 1520 * { 1521 * // record the action and parameters in an audit log 1522 * } 1523 * } 1524 * 1525 * class VaultController extends BankController 1526 * { 1527 * function __construct() 1528 * { 1529 * $this->beforeFilter('_verifyCredentials'); 1530 * } 1531 * 1532 * function _verifyCredentials(&$controller) 1533 * { 1534 * // make sure the user is allowed into the vault 1535 * } 1536 * } 1537 * 1538 * Now any actions performed on the BankController will have the audit method called before. On the VaultController, 1539 * first the audit method is called, then the _verifyCredentials method. If the _audit method returns false, then 1540 * _verifyCredentials and the intended action are never called. 1541 * 1542 * == Filter types 1543 * 1544 * A filter can take one of three forms: method reference, external class, or inline method. The first 1545 * is the most common and works by referencing a method somewhere in the inheritance hierarchy of 1546 * the controller by use of a method name. In the bank example above, both BankController and VaultController use this form. 1547 * 1548 * Using an external class makes for more easily reused generic filters, such as output compression. External filter classes 1549 * are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example: 1550 * 1551 * class OutputCompressionFilter 1552 * { 1553 * function filter(&$controller) 1554 * { 1555 * $controller->response->body = compress($controller->response->body); 1556 * } 1557 * } 1558 * 1559 * class NewspaperController extends AkActionController 1560 * { 1561 * function __construct() 1562 * { 1563 * $this->afterFilter(new OutputCompressionFilter()); 1564 * } 1565 * } 1566 * 1567 * The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can 1568 * manipulate them as it sees fit. 1569 * 1570 * 1571 * == Filter chain ordering 1572 * 1573 * Using <tt>beforeFilter</tt> and <tt>afterFilter</tt> appends the specified filters to the existing chain. That's usually 1574 * just fine, but some times you care more about the order in which the filters are executed. When that's the case, you 1575 * can use <tt>prependBeforeFilter</tt> and <tt>prependAfterFilter</tt>. Filters added by these methods will be put at the 1576 * beginning of their respective chain and executed before the rest. For example: 1577 * 1578 * class ShoppingController extends AkActionController 1579 * { 1580 * function __construct() 1581 * { 1582 * $this->beforeFilter('verifyOpenShop'); 1583 * } 1584 * } 1585 * 1586 * 1587 * class CheckoutController extends AkActionController 1588 * { 1589 * function __construct() 1590 * { 1591 * $this->prependBeforeFilter('ensureItemsInCart', 'ensureItemsInStock'); 1592 * } 1593 * } 1594 * 1595 * The filter chain for the CheckoutController is now <tt>ensureItemsInCart, ensureItemsInStock,</tt> 1596 * <tt>verifyOpenShop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop 1597 * is open or not. 1598 * 1599 * You may pass multiple filter arguments of each type. 1600 * 1601 * == Around filters 1602 * 1603 * In addition to the individual before and after filters, it's also possible to specify that a single object should handle 1604 * both the before and after call. That's especially useful when you need to keep state active between the before and after, 1605 * such as the example of a benchmark filter below: 1606 * 1607 * class WeblogController extends AkActionController 1608 * { 1609 * function __construct() 1610 * { 1611 * $this->aroundFilter(new BenchmarkingFilter()); 1612 * } 1613 * 1614 * // Before this action is performed, BenchmarkingFilter->before($controller) is executed 1615 * function index() 1616 * { 1617 * } 1618 * // After this action has been performed, BenchmarkingFilter->after($controller) is executed 1619 * } 1620 * 1621 * class BenchmarkingFilter 1622 * { 1623 * function before(&$controller) 1624 * { 1625 * start_timer(); 1626 * } 1627 * 1628 * function after(&$controller) 1629 * { 1630 * stop_timer(); 1631 * report_result(); 1632 * } 1633 * } 1634 * 1635 * == Filter chain skipping 1636 * 1637 * Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the 1638 * subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters 1639 * they would like to be relieved of. Examples 1640 * 1641 * class ApplicationController extends AkActionController 1642 * { 1643 * function __construct() 1644 * { 1645 * $this->beforeFilter('authenticate'); 1646 * } 1647 * } 1648 * 1649 * class WeblogController extends ApplicationController 1650 * { 1651 * // will run the authenticate filter 1652 * } 1653 * 1654 * class SignupController extends AkActionController 1655 * { 1656 * function __construct() 1657 * { 1658 * $this->skipBeforeFilter('authenticate'); 1659 * } 1660 * // will not run the authenticate filter 1661 * } 1662 * 1663 * == Filter conditions 1664 * 1665 * Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to 1666 * exclude or the actions to include when executing the filter. Available conditions are +only+ or +except+, both 1667 * of which accept an arbitrary number of method references. For example: 1668 * 1669 * class Journal extends AkActionController 1670 * { 1671 * function __construct() 1672 * { // only require authentication if the current action is edit or delete 1673 * $this->beforeFilter(array('_authorize'=>array('only'=>array('edit','delete'))); 1674 * } 1675 * 1676 * function _authorize(&$controller) 1677 * { 1678 * // redirect to login unless authenticated 1679 * } 1680 * } 1681 */ 1682 1683 var $_includedActions = array(), $_beforeFilters = array(), $_afterFilters = array(), $_excludedActions = array(); 1684 /** 1685 * The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions 1686 * on this controller are performed. 1687 */ 1688 function appendBeforeFilter() 1689 { 1690 $filters = array_reverse(func_get_args()); 1691 foreach (array_keys($filters) as $k){ 1692 $conditions = $this->_extractConditions(&$filters[$k]); 1693 $this->_addActionConditions($filters[$k], $conditions); 1694 $this->_appendFilterToChain('before', $filters[$k]); 1695 } 1696 } 1697 1698 /** 1699 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions 1700 * on this controller are performed. 1701 */ 1702 function prependBeforeFilter() 1703 { 1704 $filters = array_reverse(func_get_args()); 1705 foreach (array_keys($filters) as $k){ 1706 $conditions = $this->_extractConditions(&$filters[$k]); 1707 $this->_addActionConditions($filters[$k], $conditions); 1708 $this->_prependFilterToChain('before', $filters[$k]); 1709 } 1710 } 1711 1712 /** 1713 * Short-hand for appendBeforeFilter since that's the most common of the two. 1714 */ 1715 function beforeFilter() 1716 { 1717 $filters = func_get_args(); 1718 foreach (array_keys($filters) as $k){ 1719 $this->appendBeforeFilter($filters[$k]); 1720 } 1721 } 1722 1723 /** 1724 * The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions 1725 * on this controller are performed. 1726 */ 1727 function appendAfterFilter() 1728 { 1729 $filters = array_reverse(func_get_args()); 1730 foreach (array_keys($filters) as $k){ 1731 $conditions = $this->_extractConditions(&$filters[$k]); 1732 $this->_addActionConditions(&$filters[$k], $conditions); 1733 $this->_appendFilterToChain('after', &$filters[$k]); 1734 } 1735 1736 } 1737 1738 /** 1739 * The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions 1740 * on this controller are performed. 1741 */ 1742 function prependAfterFilter() 1743 { 1744 $filters = array_reverse(func_get_args()); 1745 foreach (array_keys($filters) as $k){ 1746 $conditions = $this->_extractConditions(&$filters[$k]); 1747 $this->_addActionConditions(&$filters[$k], $conditions); 1748 $this->_prependFilterToChain('after', &$filters[$k]); 1749 } 1750 } 1751 1752 /** 1753 * Short-hand for appendAfterFilter since that's the most common of the two. 1754 * */ 1755 function afterFilter() 1756 { 1757 $filters = func_get_args(); 1758 foreach (array_keys($filters) as $k){ 1759 $this->appendAfterFilter($filters[$k]); 1760 } 1761 } 1762 1763 /** 1764 * The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions 1765 * on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all 1766 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like: 1767 * 1768 * B::before() 1769 * A::before() 1770 * A::after() 1771 * B::after() 1772 */ 1773 function appendAroundFilter() 1774 { 1775 $filters = func_get_args(); 1776 foreach (array_keys($filters) as $k){ 1777 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]); 1778 $this->appendBeforeFilter(array(&$filters[$k],'before')); 1779 } 1780 $filters = array_reverse($filters); 1781 foreach (array_keys($filters) as $k){ 1782 $this->prependAfterFilter(array(&$filters[$k],'after')); 1783 } 1784 } 1785 1786 /** 1787 * The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions 1788 * on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all 1789 * respond to both +before+ and +after+. So if you do appendAroundFilter(new A(), new B()), the callstack will look like: 1790 * 1791 * A::before() 1792 * B::before() 1793 * B::after() 1794 * A::after() 1795 */ 1796 function prependAroundFilter() 1797 { 1798 $filters = func_get_args(); 1799 foreach (array_keys($filters) as $k){ 1800 $this->_ensureFilterRespondsToBeforeAndAfter(&$filters[$k]); 1801 $this->prependBeforeFilter(array(&$filters[$k],'before')); 1802 } 1803 $filters = array_reverse($filters); 1804 foreach (array_keys($filters) as $k){ 1805 $this->appendAfterFilter(array(&$filters[$k],'after')); 1806 } 1807 } 1808 1809 /** 1810 * Short-hand for appendAroundFilter since that's the most common of the two. 1811 */ 1812 function aroundFilter() 1813 { 1814 $filters = func_get_args(); 1815 call_user_func_array(array(&$this,'appendAroundFilter'), $filters); 1816 } 1817 1818 /** 1819 * Removes the specified filters from the +before+ filter chain. 1820 * This is especially useful for managing the chain in inheritance hierarchies where only one out 1821 * of many sub-controllers need a different hierarchy. 1822 */ 1823 function skipBeforeFilter($filters) 1824 { 1825 $filters = func_get_args(); 1826 $this->_skipFilter($filters, 'before'); 1827 } 1828 1829 /** 1830 * Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference 1831 * filters, not instances. This is especially useful for managing the chain in inheritance hierarchies where only one out 1832 * of many sub-controllers need a different hierarchy. 1833 */ 1834 function skipAfterFilter($filters) 1835 { 1836 $filters = func_get_args(); 1837 $this->_skipFilter($filters, 'after'); 1838 } 1839 1840 function _skipFilter(&$filters, $type) 1841 { 1842 $_filters =& $this->{'_'.$type.'Filters'}; 1843 // array_diff doesn't play nice with some PHP5 releases when it comes to 1844 // Objects as it only diff equal references, not object types 1845 foreach (array_keys($filters) as $k){ 1846 if(AK_PHP5){ 1847 if(is_object($filters[$k])){ 1848 foreach (array_keys($_filters) as $k2){ 1849 if(is_object($_filters[$k2]) && get_class($_filters[$k2]) == get_class($filters[$k])){ 1850 $pos = $k2; 1851 break; 1852 } 1853 } 1854 }else{ 1855 $pos = array_search($filters[$k], $_filters); 1856 } 1857 1858 array_splice($_filters, $pos, 1, null); 1859 return ; 1860 } 1861 $_filters = array_diff($_filters,array($filters[$k])); 1862 } 1863 } 1864 1865 /** 1866 * Returns all the before filters for this class. 1867 */ 1868 function beforeFilters() 1869 { 1870 return $this->_beforeFilters; 1871 } 1872 1873 /** 1874 * Returns all the after filters for this class and all its ancestors. 1875 */ 1876 function afterFilters() 1877 { 1878 return $this->_afterFilters; 1879 } 1880 1881 /** 1882 * Returns a mapping between filters and the actions that may run them. 1883 */ 1884 function includedActions() 1885 { 1886 return $this->_includedActions; 1887 } 1888 1889 /** 1890 * Returns a mapping between filters and actions that may not run them. 1891 */ 1892 function excludedActions() 1893 { 1894 return $this->_excludedActions; 1895 } 1896 1897 1898 function _appendFilterToChain($condition, $filters) 1899 { 1900 $this->{"_{$condition}Filters"}[] =& $filters; 1901 } 1902 1903 function _prependFilterToChain($condition, $filters) 1904 { 1905 array_unshift($this->{"_{$condition}Filters"}, $filters); 1906 } 1907 1908 function _ensureFilterRespondsToBeforeAndAfter(&$filter_object) 1909 { 1910 if(!method_exists(&$filter_object,'before') && !method_exists(&$filter_object,'after')){ 1911 trigger_error(Ak::t('Filter object must respond to both before and after'), E_USER_ERROR); 1912 } 1913 } 1914 1915 function _extractConditions(&$filters) 1916 { 1917 if(is_array($filters) && !isset($filters[0])){ 1918 $keys = array_keys($filters); 1919 $conditions = $filters[$keys[0]]; 1920 $filters = $keys[0]; 1921 return $conditions; 1922 } 1923 } 1924 1925 function _addActionConditions($filters, $conditions) 1926 { 1927 if(!empty($conditions['only'])){ 1928 $this->_includedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_includedActions, $conditions['only']); 1929 } 1930 if(!empty($conditions['except'])){ 1931 $this->_excludedActions[$this->_filterId($filters)] = $this->_conditionArray($this->_excludedActions, $conditions['except']); 1932 } 1933 } 1934 1935 function _conditionArray($actions, $filter_actions) 1936 { 1937 $filter_actions = is_array($filter_actions) ? $filter_actions : array($filter_actions); 1938 $filter_actions = array_map(array(&$this,'_filterId'),$filter_actions); 1939 return array_unique(array_merge($actions, $filter_actions)); 1940 } 1941 1942 1943 function _filterId($filters) 1944 { 1945 return is_string($filters) ? $filters : md5(serialize($filters)); 1946 } 1947 1948 function performActionWithoutFilters($action) 1949 { 1950 if(method_exists(&$this, $action)){ 1951 call_user_func_array(array(&$this, $action), @(array)$this->passed_args); 1952 } 1953 } 1954 1955 function performActionWithFilters($method = '') 1956 { 1957 if ($this->beforeAction($method) !== false && !$this->_hasPerformed()){ 1958 $this->performActionWithoutFilters($method); 1959 $this->afterAction($method); 1960 return true; 1961 } 1962 return false; 1963 } 1964 1965 function performAction($method = '') 1966 { 1967 $this->performActionWithFilters($method); 1968 } 1969 1970 1971 /** 1972 * Calls all the defined before-filter filters, which are added by using "beforeFilter($method)". 1973 * If any of the filters return false, no more filters will be executed and the action is aborted. 1974 */ 1975 function beforeAction($method = '') 1976 { 1977 Ak::profile('Running before controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__); 1978 return $this->_callFilters($this->_beforeFilters, $method); 1979 } 1980 1981 /** 1982 * Calls all the defined after-filter filters, which are added by using "afterFilter($method)". 1983 * If any of the filters return false, no more filters will be executed. 1984 */ 1985 function afterAction($method = '') 1986 { 1987 Ak::profile('Running after controller action filters '.__CLASS__.'::'.__FUNCTION__.' '.__LINE__); 1988 return $this->_callFilters(&$this->_afterFilters, $method); 1989 } 1990 1991 1992 function _callFilters(&$filters, $method = '') 1993 { 1994 $filter_result = null; 1995 foreach (array_keys($filters) as $k){ 1996 $filter =& $filters[$k]; 1997 if(!$this->_actionIsExempted($filter, $method)){ 1998 if(is_array($filter) && is_object($filter[0]) && method_exists($filter[0], $filter[1])){ 1999 $filter_result = $filter[0]->$filter[1]($this); 2000 }elseif(!is_object($filter) && method_exists($this, $filter)){ 2001 $filter_result = $this->$filter($this); 2002 }elseif(is_object($filter) && method_exists($filter, 'filter')){ 2003 $filter_result = $filter->filter($this); 2004 }else{ 2005 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); 2006 } 2007 2008 } 2009 if($filter_result === false){ 2010 !empty($this->_Logger) ? $this->_Logger->info(Ak::t('Filter chain halted as '.$filter.' returned false')) : null; 2011 return false; 2012 } 2013 } 2014 return $filter_result; 2015 } 2016 2017 2018 function _actionIsExempted($filter, $method = '') 2019 { 2020 $method_id = is_string($method) ? $method : $this->_filterId($method); 2021 $filter_id = $this->_filterId($filter); 2022 2023 if((!empty($this->_includedActions[$filter_id]) && !in_array($method_id, $this->_includedActions[$filter_id])) || 2024 (!empty($this->_excludedActions[$filter_id]) && in_array($method_id, $this->_excludedActions[$filter_id]))){ 2025 return true; 2026 } 2027 2028 return false; 2029 } 2030 2031 2032 2033 /** 2034 Flash communication between actions 2035 ==================================================================== 2036 * 2037 * The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed 2038 * to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action 2039 * that sets <tt>flash['notice] = 'Successfully created'</tt> before redirecting to a display action that can then expose 2040 * the flash to its template. Actually, that exposure is automatically done. Example: 2041 * 2042 * class WeblogController extends ActionController 2043 * { 2044 * function create() 2045 * { 2046 * // save post 2047 * $this->flash['notice] = 'Successfully created post'; 2048 * $this->redirectTo(array('action'=>'display','params' => array('id' =>$Post->id))); 2049 * } 2050 * 2051 * function display() 2052 * { 2053 * // doesn't need to assign the flash notice to the template, that's done automatically 2054 * } 2055 * } 2056 * 2057 * display.tpl 2058 * <?php if($flash['notice']) : ?><div class='notice'><?php echo $flash['notice'] ?></div><?php endif; ?> 2059 * 2060 * 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 2061 * as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. 2062 * 2063 * ==flash_now 2064 * 2065 * Sets a flash that will not be available to the next action, only to the current. 2066 * 2067 * $this->flash_now['message] = 'Hello current action'; 2068 * 2069 * This method enables you to use the flash as a central messaging system in your app. 2070 * When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>). 2071 * When you need to pass an object to the current action, you use <tt>now</tt>, and your object will 2072 * vanish when the current action is done. 2073 * 2074 * Entries set via <tt>flash_now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>. 2075 */ 2076 var $flash = array(); 2077 var $flash_now = array(); 2078 var $flash_options = array(); 2079 var $_flash_handled = false; 2080 2081 function _handleFlashAttribute() 2082 { 2083 $this->_flash_handled = true; 2084 2085 $next_flash = empty($this->flash) ? false : $this->flash; 2086 $this->flash = array(); 2087 if(isset($_SESSION['__flash'])){ 2088 $this->flash = $_SESSION['__flash']; 2089 } 2090 $_SESSION['__flash'] = $next_flash; 2091 if(!empty($this->flash_now)){ 2092 $this->flash = array_merge((array)$this->flash,(array)$this->flash_now); 2093 } 2094 $this->_handleFlashOptions(); 2095 } 2096 2097 function _handleFlashOptions() 2098 { 2099 $next_flash_options = empty($this->flash_options) ? false : $this->flash_options; 2100 $this->flash_options = array(); 2101 if(isset($_SESSION['__flash_options'])){ 2102 $this->flash_options = $_SESSION['__flash_options']; 2103 } 2104 $_SESSION['__flash_options'] = $next_flash_options; 2105 if(!empty($this->flash_now_options)){ 2106 $this->flash_options = array_merge((array)$this->flash_options,(array)$this->flash_now_options); 2107 } 2108 } 2109 2110 2111 function _mergeFlashOnFlashNow() 2112 { 2113 $this->flash_now = array_merge($this->flash_now,$this->flash); 2114 } 2115 2116 2117 /** 2118 Pagination for Active Record collections 2119 ==================================================================== 2120 * 2121 * The Pagination module aids in the process of paging large collections of 2122 * Active Record objects. It offers macro-style automatic fetching of your 2123 * model for multiple views, or explicit fetching for single actions. And if 2124 * the magic isn't flexible enough for your needs, you can create your own 2125 * paginators with a minimal amount of code. 2126 * 2127 * The Pagination module can handle as much or as little as you wish. In the 2128 * controller, have it automatically query your model for pagination; or, 2129 * if you prefer, create Paginator objects yourself 2130 * 2131 * Pagination is included automatically for all controllers. 2132 * 2133 * For help rendering pagination links, see 2134 * Helpers/PaginationHelper. 2135 * 2136 * ==== Automatic pagination for every action in a controller 2137 * 2138 * class PersonController extends ApplicationController 2139 * { 2140 * var $model = 'person'; 2141 * var $paginate = array('people'=>array('order' => 'last_name, first_name', 2142 * 'per_page' => 20)); 2143 * } 2144 * 2145 * Each action in this controller now has access to a <tt>$this->people</tt> 2146 * instance variable, which is an ordered collection of model objects for the 2147 * current page (at most 20, sorted by last name and first name), and a 2148 * <tt>$this->person_pages</tt> Paginator instance. The current page is determined 2149 * by the <tt>$params['page']</tt> variable. 2150 * 2151 * ==== Pagination for a single action 2152 * 2153 * function show_all() 2154 * { 2155 * list($this->person_pages, $this->people) = 2156 * $this->paginate('people', array('order' => 'last_name, first_name')); 2157 * } 2158 * 2159 * Like the previous example, but explicitly creates <tt>$this->person_pages</tt> 2160 * and <tt>$this->people</tt> for a single action, and uses the default of 10 items 2161 * per page. 2162 * 2163 * ==== Custom/"classic" pagination 2164 * 2165 * function list() 2166 * { 2167 * $this->person_pages = new AkPaginator(&$this, $Person->count(), 10, $params['page']); 2168 * $this->people = $this->Person->find('all', array( 2169 * 'order'=> 'last_name, first_name', 2170 * 'limit' => $this->person_pages->items_per_page, 2171 * 'offset' => $this->person_pages->getOffset())); 2172 * } 2173 * 2174 * Explicitly creates the paginator from the previous example and uses 2175 * AkPaginator::toSql to retrieve <tt>$this->people</tt> from the model. 2176 */ 2177 // An array holding options for controllers using macro-style pagination 2178 var $_pagination_options = array( 2179 'class_name' => null, 2180 'singular_name' => null, 2181 'per_page' => 10, 2182 'conditions' => null, 2183 'order_by' => null, 2184 'order' => null, 2185 'join' => null, 2186 'joins' => null, 2187 'include' => null, 2188 'select' => null, 2189 'parameter' => 'page' 2190 ); 2191 2192 // The default options for pagination 2193 var $_pagination_default_options = array( 2194 'class_name' => null, 2195 'singular_name' => null, 2196 'per_page' => 10, 2197 'conditions' => null, 2198 'order_by' => null, 2199 'order' => null, 2200 'join' => null, 2201 'joins' => null, 2202 'include' => null, 2203 'select' => null, 2204 'parameter' => 'page' 2205 ); 2206 2207 var $_pagination_actions = array(); 2208 2209 function _paginationValidateOptions($collection_id, $options = array(), $in_action) 2210 { 2211 $this->_pagination_options = array_merge($this->_pagination_default_options, $this->_pagination_options); 2212 $valid_options = array_keys($this->_pagination_default_options); 2213 2214 $valid_options = !in_array($in_action, $valid_options) ? array_merge($valid_options, $this->_pagination_actions) : $valid_options; 2215 2216 $unknown_option_keys = array_diff(array_keys($this->_pagination_options) , $valid_options); 2217 2218 if(!empty($unknown_option_keys)){ 2219 trigger_error(Ak::t('Unknown options for pagination: %unknown_option',array('%unknown_option'=>join(', ',$unknown_option_keys))), E_USER_WARNING); 2220 } 2221 2222 $this->_pagination_options['singular_name'] = !empty($this->_pagination_options['singular_name']) ? $this->_pagination_options['singular_name'] : AkInflector::singularize($collection_id); 2223 $this->_pagination_options['class_name'] = !empty($this->_pagination_options['class_name']) ? $this->_pagination_options['class_name'] : AkInflector::camelize($this->_pagination_options['singular_name']); 2224 } 2225 2226 /** 2227 * Returns a paginator and a collection of Active Record model instances 2228 * for the paginator's current page. This is designed to be used in a 2229 * single action. 2230 * 2231 * +options+ are: 2232 * <tt>singular_name</tt>:: the singular name to use, if it can't be inferred by 2233 * singularizing the collection name 2234 * <tt>class_name</tt>:: the class name to use, if it can't be inferred by 2235 * camelizing the singular name 2236 * <tt>per_page</tt>:: the maximum number of items to include in a 2237 * single page. Defaults to 10 2238 * <tt>conditions</tt>:: optional conditions passed to Model::find('all', $this->params); and 2239 * Model::count() 2240 * <tt>order</tt>:: optional order parameter passed to Model::find('all', $this->params); 2241 * <tt>order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model::find('all', $this->params) 2242 * <tt>joins</tt>:: optional joins parameter passed to Model::find('all', $this->params) 2243 * and Model::count() 2244 * <tt>join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model::find('all', $this->params) 2245 * and Model::count() 2246 * <tt>include</tt>:: optional eager loading parameter passed to Model::find('all', $this->params) 2247 * and Model::count() 2248 * 2249 * Creates a +before_filter+ which automatically paginates an Active 2250 * Record model for all actions in a controller (or certain actions if 2251 * specified with the <tt>actions</tt> option). 2252 * 2253 * +options+ are the same as PaginationHelper::paginate, with the addition 2254 * of: 2255 * <tt>actions</tt>:: an array of actions for which the pagination is 2256 * active. Defaults to +null+ (i.e., every action) 2257 */ 2258 function paginate($collection_id, $options = array()) 2259 { 2260 $this->_paginationValidateOptions($collection_id, $options, true); 2261 $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options); 2262 $this->beforeFilter('_paginationCreateAndRetrieveCollections'); 2263 } 2264 2265 2266 function _paginationCreateAndRetrieveCollections() 2267 { 2268 foreach($this->_pagination_options[$this->class] as $collection_id=>$options){ 2269 if(!empty($options['actions']) && in_array($options['actions'], $action_name)){ 2270 continue; 2271 } 2272 2273 list($paginator, $collection) = $this->_paginationLoadPaginatorAndCollection($collection_id, $this->_pagination_options); 2274 2275 $this->{$options['singular_name'].'_pages'} =& $paginator; 2276 $this->$collection_name =& $collection; 2277 } 2278 } 2279 2280 /** 2281 * Returns the total number of items in the collection to be paginated for 2282 * the +model+ and given +conditions+. Override this method to implement a 2283 * custom counter. 2284 */ 2285 function _paginationCountCollection(&$model, $conditions, $joins) 2286 { 2287 return $model->count($conditions, $joins); 2288 } 2289 2290 /** 2291 * Returns a collection of items for the given +$model+ and +$options['conditions']+, 2292 * ordered by +$options['order']+, for the current page in the given +$paginator+. 2293 * Override this method to implement a custom finder. 2294 */ 2295 function _paginationFindCollection(&$model, $options, &$paginator) 2296 { 2297 return $model->find('all', array( 2298 'conditions' => $this->_pagination_options['conditions'], 2299 'order' => !empty($options['order_by']) ? $options['order_by'] : $options['order'], 2300 'joins' => !empty($options['join']) ? $options['join'] : $options['joins'], 2301 'include' => $this->_pagination_options['include'], 2302 'select' => $this->_pagination_options['select'], 2303 'limit' => $this->_pagination_options['per_page'], 2304 'offset' => $paginator->getOffset())); 2305 } 2306 2307 /** 2308 * @todo Fix this function 2309 */ 2310 function _paginationLoadPaginatorAndCollection($collection_id, $options) 2311 { 2312 $page = $this->params[$options['parameter']]; 2313 $count = $this->_paginationCountCollection($klass, $options['conditions'], 2314 empty($options['join']) ? $options['join'] : $options['joins']); 2315 2316 require_once(AK_LIB_DIR.DS.'AkActionController'.DS.'AkPaginator.php'); 2317 $paginator =& new AkPaginator($this, $count, $options['per_page'], $page); 2318 $collection =& $this->_paginationFindCollection($options['class_name'], $options, $paginator); 2319 2320 return array(&$paginator, &$collection); 2321 } 2322 2323 /** 2324 Protocol conformance 2325 ==================================================================== 2326 */ 2327 2328 /** 2329 * Specifies that the named actions requires an SSL connection to be performed (which is enforced by ensure_proper_protocol). 2330 */ 2331 function setSslRequiredActions($actions) 2332 { 2333 $this->_ssl_required_actions = empty($this->_ssl_required_actions) ? 2334 (is_string($actions) ? Ak::stringToArray($actions) : $actions) : 2335 array_merge($this->_ssl_required_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions)); 2336 } 2337 2338 function setSslAllowedActions($actions) 2339 { 2340 $this->_ssl_allowed_actions = empty($this->_ssl_allowed_actions) ? 2341 (is_string($actions) ? Ak::stringToArray($actions) : $actions) : 2342 array_merge($this->_ssl_allowed_actions, (is_string($actions) ? Ak::stringToArray($actions) : $actions)); 2343 } 2344 2345 /** 2346 * Returns true if the current action is supposed to run as SSL 2347 */ 2348 function _isSslRequired() 2349 { 2350 return !empty($this->_ssl_required_actions) && is_array($this->_ssl_required_actions) && isset($this->_action_name) ? 2351 in_array($this->_action_name, $this->_ssl_required_actions) : false; 2352 } 2353 2354 function _isSslAllowed() 2355 { 2356 return (!empty($this->ssl_for_all_actions) && empty($this->_ssl_allowed_actions)) || 2357 (!empty($this->_ssl_allowed_actions) && is_array($this->_ssl_allowed_actions) && isset($this->_action_name) ? 2358 in_array($this->_action_name, $this->_ssl_allowed_actions) : false); 2359 } 2360 2361 function _ensureProperProtocol() 2362 { 2363 if($this->_isSslAllowed()){ 2364 return true; 2365 } 2366 if ($this->_isSslRequired() && !$this->Request->isSsl()){ 2367 $this->redirectTo(substr_replace(AK_CURRENT_URL,'s:',4,1)); 2368 return false; 2369 }elseif($this->Request->isSsl() && !$this->_isSslRequired()){ 2370 $this->redirectTo(substr_replace(AK_CURRENT_URL,'',4,1)); 2371 return false; 2372 } 2373 } 2374 2375 /** 2376 Account Location 2377 ==================================================================== 2378 * 2379 * Account location is a set of methods that supports the account-key-as-subdomain 2380 * way of identifying the current scope. These methods allow you to easily produce URLs that 2381 * match this style and to get the current account key from the subdomain. 2382 * 2383 * The methods are: getAccountUrl, getAccountHost, and getAccountDomain. 2384 * 2385 * Example: 2386 * 2387 * include_once('AkAccountLocation.php'); 2388 * 2389 * class ApplicationController extends AkActionController 2390 * { 2391 * var $before_filter = '_findAccount'; 2392 * 2393 * function _findAccount() 2394 * { 2395 * $this->account = Account::find(array('conditions'=>array('username = ?', $this->account_domain))); 2396 * } 2397 * 2398 * class AccountController extends ApplicationController 2399 * { 2400 * function new_account() 2401 * { 2402 * $this->new_account = Account::create($this->params['new_account']); 2403 * $this->redirectTo(array('host' => $this->accountHost($this->new_account->username), 'controller' => 'weblog')); 2404 * } 2405 * 2406 * function authenticate() 2407 * { 2408 * $this->session[$this->account_domain] = 'authenticated'; 2409 * $this->redirectTo(array('controller => 'weblog')); 2410 * } 2411 * 2412 * function _isAuthenticated() 2413 * { 2414 * return !empty($this->session['account_domain']) ? $this->session['account_domain'] == 'authenticated' : false; 2415 * } 2416 * } 2417 * 2418 * // The view: 2419 * Your domain: {account_url?} 2420 * 2421 * By default, all the methods will query for $this->account->username as the account key, but you can 2422 * specialize that by overwriting defaultAccountSubdomain. You can of course also pass it in 2423 * as the first argument to all the methods. 2424 */ 2425 function defaultAccountSubdomain() 2426 { 2427 if(!empty($this->account)){ 2428 return $this->account->respondsTo('username'); 2429 } 2430 } 2431 2432 function accountUrl($account_subdomain = null, $use_ssl = null) 2433 { 2434 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain; 2435 $use_ssl = empty($use_ssl) ? $use_ssl : $this->Request->isSsl(); 2436 return ($use_ssl ? 'https://' : 'http://') . $this->accountHost($account_subdomain); 2437 } 2438 2439 function accountHost($account_subdomain = null) 2440 { 2441 $account_subdomain = empty($account_subdomain) ? 'default_account_subdomain' : $account_subdomain; 2442 $account_host = ''; 2443 $account_host .= $account_subdomain . '.'; 2444 $account_host .= $this->accountDomain(); 2445 return $account_host; 2446 } 2447 2448 function accountDomain() 2449 { 2450 $account_domain = ''; 2451 if(count($this->Request->getSubdomains()) > 1){ 2452 $account_domain .= join('.',$this->Request->getSubdomains()) . '.'; 2453 } 2454 $account_domain .= $this->Request->getDomain() . $this->Request->getPortString(); 2455 return $account_domain; 2456 } 2457 2458 function getAccountSubdomain() 2459 { 2460 return array_shift($this->Request->getSubdomains()); 2461 } 2462 2463 2464 /** 2465 Data streaming 2466 ==================================================================== 2467 Methods for sending files and streams to the browser instead of rendering. 2468 */ 2469 2470 var $default_send_file_options = array( 2471 'type' => 'application/octet-stream', 2472 'disposition' => 'attachment', 2473 'stream' => true, 2474 'buffer_size' => 4096 2475 ); 2476 2477 /** 2478 * Sends the file by streaming it 4096 bytes at a time. This way the 2479 * whole file doesn't need to be read into memory at once. This makes 2480 * it feasible to send even large files. 2481 * 2482 * Be careful to sanitize the path parameter if it coming from a web 2483 * page. sendFile($params['path']) allows a malicious user to 2484 * download any file on your server. 2485 * 2486 * Options: 2487 * * <tt>filename</tt> - suggests a filename for the browser to use. 2488 * Defaults to realpath($path). 2489 * * <tt>type</tt> - specifies an HTTP content type. 2490 * Defaults to 'application/octet-stream'. 2491 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded. 2492 * Valid values are 'inline' and 'attachment' (default). 2493 * * <tt>stream</tt> - whether to send the file to the user agent as it is read (true) 2494 * or to read the entire file before sending (false). Defaults to true. 2495 * * <tt>buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file. 2496 * Defaults to 4096. 2497 * 2498 * The default Content-Type and Content-Disposition headers are 2499 * set to download arbitrary binary files in as many browsers as 2500 * possible. IE versions 4, 5, 5.5, and 6 are all known to have 2501 * a variety of quirks (especially when downloading over SSL). 2502 * 2503 * Simple download: 2504 * sendFile('/path/to.zip'); 2505 * 2506 * Show a JPEG in browser: 2507 * sendFile('/path/to.jpeg', array('type' => 'image/jpeg', 'disposition' => 'inline')); 2508 * 2509 * Read about the other Content-* HTTP headers if you'd like to 2510 * provide the user with more information (such as Content-Description). 2511 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 2512 * 2513 * Also be aware that the document may be cached by proxies and browsers. 2514 * The Pragma and Cache-Control headers declare how the file may be cached 2515 * by intermediaries. They default to require clients to validate with 2516 * the server before releasing cached responses. See 2517 * http://www.mnot.net/cache_docs/ for an overview of web caching and 2518 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 2519 * for the Cache-Control header spec. 2520 */ 2521 function sendFile($path, $options = array()) 2522 { 2523 $path = realpath($path); 2524 if(!file_exists($path)){ 2525 trigger_error(Ak::t('Cannot read file %path',array('%path'=>$path)), E_USER_NOTICE); 2526 return false; 2527 } 2528 $options['length'] = empty($options['length']) ? filesize($path) : $options['length']; 2529 $options['filename'] = empty($options['filename']) ? basename($path) : $options['filename']; 2530 $options['type'] = empty($options['type']) ? Ak::mime_content_type($path) : $options['type']; 2531 2532 $this->performed_render = false; 2533 $this->_sendFileHeaders($options); 2534 2535 if(!empty($options['stream'])){ 2536 require_once(AK_LIB_DIR.DS.'AkStream.php'); 2537 $this->render(array('text'=> new AkStream($path,$options['buffer_size']))); 2538 }else{ 2539 $this->render(array('text'=> Ak::file_get_contents($path))); 2540 } 2541 } 2542 2543 /** 2544 * Send binary data to the user as a file download. May set content type, apparent file name, 2545 * and specify whether to show data inline or download as an attachment. 2546 * 2547 * Options: 2548 * * <tt>filename</tt> - Suggests a filename for the browser to use. 2549 * * <tt>type</tt> - specifies an HTTP content type. 2550 * Defaults to 'application/octet-stream'. 2551 * * <tt>disposition</tt> - specifies whether the file will be shown inline or downloaded. 2552 * Valid values are 'inline' and 'attachment' (default). 2553 * 2554 * Generic data download: 2555 * sendData($buffer) 2556 * 2557 * Download a dynamically-generated tarball: 2558 * sendData(Ak::compress('dir','tgz'), array('filename' => 'dir.tgz')); 2559 * 2560 * Display an image Active Record in the browser: 2561 * sendData($image_data, array('type' =>Ak::mime_content_type('image_name.png'), 'disposition' => 'inline')); 2562 * 2563 * See +sendFile+ for more information on HTTP Content-* headers and caching. 2564 */ 2565 function sendData($data, $options = array()) 2566 { 2567 $options['length'] = empty($options['length']) ? Ak::size($data) : $options['length']; 2568 $this->_sendFileHeaders($options); 2569 $this->performed_render = false; 2570 $this->renderText($data); 2571 } 2572 2573 /** 2574 * Creates a file for streaming from a file. 2575 * This way you might free memory usage is file is too large 2576 */ 2577 function sendDataAsStream($data, $options) 2578 { 2579 $temp_file_name = tempnam(AK_TMP_DIR, Ak::randomString()); 2580 $fp = fopen($temp_file_name, 'w'); 2581 fwrite($fp, $data); 2582 fclose($fp); 2583 $this->sendFile($temp_file_name, $options); 2584 } 2585 2586 2587 function _sendFileHeaders(&$options) 2588 { 2589 $options = array_merge($this->default_send_file_options,$options); 2590 foreach (array('length', 'type', 'disposition') as $arg){ 2591 empty($options[$arg]) ? trigger_error(Ak::t('%arg option required', array('%arg'=>$arg)), E_USER_ERROR) : null; 2592 } 2593 $disposition = empty($options['disposition']) ? 'attachment' : $options['disposition']; 2594 $disposition .= !empty($options['filename']) ? '; filename="'.$options['filename'].'"' : ''; 2595 $this->Response->addHeader(array( 2596 'Content-Length' => $options['length'], 2597 'Content-Type' => trim($options['type']), // fixes a problem with extra '\r' with some browsers 2598 'Content-Disposition' => $disposition, 2599 'Content-Transfer-Encoding' => 'binary' 2600 )); 2601 2602 } 2603 2604 2605 function redirectToLocale($locale) 2606 { 2607 if($this->Request->__internationalization_support_enabled){ 2608 $lang = isset($this->params['lang']) ? $this->params['lang'] : $locale; 2609 2610 if($locale != $lang){ 2611 $this->redirectTo(array_merge($this->Request->getParams(),array('lang'=>$locale))); 2612 return true; 2613 } 2614 } 2615 return false; 2616 } 2617 2618 2619 function api($protocol = 'xml_rpc') 2620 { 2621 $web_services = array_merge(Ak::toArray($this->web_services), Ak::toArray($this->web_service)); 2622 if(!empty($web_services)){ 2623 $web_services = array_unique($web_services); 2624 require_once(AK_LIB_DIR.DS.'AkActionWebService.php'); 2625 require_once(AK_LIB_DIR.DS.'AkActionWebService'.DS.'AkActionWebServiceServer.php'); 2626 $Server =& new AkActionWebServiceServer($protocol); 2627 foreach ($web_services as $web_service){ 2628 $Server->addService($web_service); 2629 } 2630 $Server->init(); 2631 $Server->serve(); 2632 exit; 2633 }else{ 2634 die(Ak::t('There is not any webservice configured at this endpoint')); 2635 } 2636 } 2637 2638 2639 2640 /** 2641 HTTP Authentication 2642 ==================================================================== 2643 * 2644 * Simple Basic example: 2645 * 2646 * class PostsController extends ApplicationController 2647 * { 2648 * var $_authorized_users = array('bermi' => 'secret'); 2649 * 2650 * function __construct(){ 2651 * $this->beforeFilter(array('authenticate' => array('except' => array('index')))); 2652 * } 2653 * 2654 * function index() { 2655 * $this->renderText("Everyone can see me!"); 2656 * } 2657 * 2658 * function edit(){ 2659 * $this->renderText("I'm only accessible if you know the password"); 2660 * } 2661 * 2662 * function authenticate(){ 2663 * return $this->_authenticateOrRequestWithHttpBasic('App name', $this->_authorized_users); 2664 * } 2665 * } 2666 * 2667 * Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, 2668 * the regular HTML interface is protected by a session approach: 2669 * 2670 * class ApplicationController extends AkActionController 2671 * { 2672 * var $models = 'account'; 2673 * 2674 * function __construct() { 2675 * $this->beforeFilter(array('_setAccount', 'authenticate')); 2676 * } 2677 * 2678 * function _setAccount() { 2679 * $this->Account = $this->account->findFirstBy('url_name', array_pop($this->Request->getSubdomains())); 2680 * } 2681 * 2682 * function authenticate() { 2683 * if($this->Request->isFormat('XML', 'ATOM')){ 2684 * if($User = $this->_authenticateWithHttpBasic($Account)){ 2685 * $this->CurrentUser = $User; 2686 * }else{ 2687 * $this->_requestHttpBasicAuthentication(); 2688 * } 2689 * }else{ 2690 * if($this->isSessionAuthenticated()){ 2691 * $this->CurrentUser = $Account->user->find($_SESSION['authenticated']['user_id']); 2692 * }else{ 2693 * $this->redirectTo(array('controller'=>'login')); 2694 * return false; 2695 * } 2696 * } 2697 * } 2698 * } 2699 * 2700 * On shared hosts, Apache sometimes doesn't pass authentication headers to 2701 * FCGI instances. If your environment matches this description and you cannot 2702 * authenticate, try this rule in public/.htaccess (replace the plain one): 2703 * 2704 * RewriteRule ^(.*)$ index.php [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] 2705 */ 2706 2707 function _authenticateOrRequestWithHttpBasic($realm = AK_APP_NAME, $login_procedure) 2708 { 2709 if($Result = $this->_authenticateWithHttpBasic($login_procedure)){ 2710 return $Result; 2711 } 2712 return $this->_requestHttpBasicAuthentication($realm); 2713 } 2714 2715 function _authenticateWithHttpBasic($login_procedure) 2716 { 2717 return $this->_authenticate($login_procedure); 2718 } 2719 2720 function _requestHttpBasicAuthentication($realm = AK_APP_NAME) 2721 { 2722 return $this->_authenticationRequest($realm); 2723 } 2724 2725 /** 2726 * This is method takes a $login_procedure for performing access authentication. 2727 * 2728 * If an array is given, it will check the key for a user and the value will be verified to match given password. 2729 * 2730 * You can pass and array like array('handler' => $Account, 'method' => 'verifyCredentials'), which will call 2731 * 2732 * $Account->verifyCredentials($user_name, $password, $Controller) 2733 * 2734 * You can also pass an object which implements an "authenticate" method. when calling 2735 * 2736 * $this->_authenticate(new User()); 2737 * 2738 * It will call the $User->authenticate($user_name, $password, $Controller) 2739 * 2740 * In both cases the authentication method should return true for valid credentials or false is invalid. 2741 * 2742 * @return bool 2743 */ 2744 function _authenticate($login_procedure) 2745 { 2746 if(!$this->_authorization()){ 2747 return false; 2748 }else{ 2749 list($user_name, $password) = $this->_getUserNameAndPassword(); 2750 if(is_array($login_procedure)){ 2751 if(!isset($login_procedure['handler'])){ 2752 return isset($login_procedure[$user_name]) && $login_procedure[$user_name] == $password; 2753 }elseif(is_a($login_procedure['handler']) && method_exists($login_procedure['handler'], $login_procedure['method'])){ 2754 return $login_procedure['handler']->$login_procedure['method']($user_name, $password, $this); 2755 } 2756 }elseif(method_exists($login_procedure, 'authenticate')){ 2757 return $login_procedure->authenticate($user_name, $password, $this); 2758 } 2759 } 2760 return false; 2761 } 2762 2763 function _getUserNameAndPassword() 2764 { 2765 $credentials = $this->_decodeCredentials(); 2766 return !is_array($credentials) ? split('/:/', $credentials , 2) : $credentials; 2767 } 2768 2769 function _authorization() 2770 { 2771 return 2772 empty($this->Request->env['PHP_AUTH_USER']) ? ( 2773 empty($this->Request->env['HTTP_AUTHORIZATION']) ? ( 2774 empty($this->Request->env['X-HTTP_AUTHORIZATION']) ? ( 2775 empty($this->Request->env['X_HTTP_AUTHORIZATION']) ? ( 2776 isset($this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION']) ? 2777 $this->Request->env['REDIRECT_X_HTTP_AUTHORIZATION'] : null 2778 ) : $this->Request->env['X_HTTP_AUTHORIZATION'] 2779 ) : $this->Request->env['X-HTTP_AUTHORIZATION'] 2780 ) : $this->Request->env['HTTP_AUTHORIZATION'] 2781 ) : array($this->Request->env['PHP_AUTH_USER'], $this->Request->env['PHP_AUTH_PW']); 2782 } 2783 2784 function _decodeCredentials() 2785 { 2786 $authorization = $this->_authorization(); 2787 if(is_array($authorization)){ 2788 return $authorization; 2789 } 2790 $credentials = (array)split(' ', $authorization); 2791 return base64_decode(array_pop($credentials)); 2792 } 2793 2794 function _encodeCredentials($user_name, $password) 2795 { 2796 return 'Basic '.base64_encode("$user_name:$password"); 2797 } 2798 2799 function _authenticationRequest($realm) 2800 { 2801 header('WWW-Authenticate: Basic realm="' . str_replace('"','',$realm) . '"'); 2802 2803 if(method_exists($this, 'access_denied')){ 2804 $this->access_denied(); 2805 }else{ 2806 header('HTTP/1.0 401 Unauthorized'); 2807 echo "HTTP Basic: Access denied.\n"; 2808 exit; 2809 } 2810 } 2811 2812 function _ensureActionExists() 2813 { 2814 if(!method_exists($this, $this->_action_name)){ 2815 if(AK_ENVIRONMENT == 'development'){ 2816 AK_LOG_EVENTS && !empty($this->_Logger) ? $this->_Logger->error('Action '.$this->_action_name.' not found on '.$this->getControllerName()) : null; 2817 trigger_error(Ak::t('Controller <i>%controller_name</i> can\'t handle action %action_name', 2818 array( 2819 '%controller_name' => $this->getControllerName(), 2820 '%action_name' => $this->_action_name, 2821 )), E_USER_ERROR); 2822 }elseif(@include(AK_PUBLIC_DIR.DS.'405.php')){ 2823 exit; 2824 }else{ 2825 header("HTTP/1.1 405 Method Not Allowed"); 2826 die('405 Method Not Allowed'); 2827 } 2828 } 2829 } 2830} 2831 2832 2833/** 2834 * Function for getting the singleton controller; 2835 * 2836 * @return AkActionController instance 2837 */ 2838function &AkActionController() 2839{ 2840 $params = func_num_args() == 0 ? null : func_get_args(); 2841 $AkActionController =& Ak::singleton('AkActionController', $params); 2842 return $AkActionController; 2843} 2844 2845 2846?>