PageRenderTime 43ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/AkActionController.php

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