PageRenderTime 61ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/Nette/Application/Presenter.php

https://github.com/DocX/nette
PHP | 1372 lines | 1046 code | 158 blank | 168 comment | 104 complexity | 22e2e6e80432075194cb3a5a4d72a88e MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /**
  3. * Nette Framework
  4. *
  5. * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  6. *
  7. * This source file is subject to the "Nette license" that is bundled
  8. * with this package in the file license.txt.
  9. *
  10. * For more information please see http://nettephp.com
  11. *
  12. * @copyright Copyright (c) 2004, 2009 David Grudl
  13. * @license http://nettephp.com/license Nette license
  14. * @link http://nettephp.com
  15. * @category Nette
  16. * @package Nette\Application
  17. */
  18. /*namespace Nette\Application;*/
  19. /*use Nette\Environment;*/
  20. require_once dirname(__FILE__) . '/../Application/Control.php';
  21. require_once dirname(__FILE__) . '/../Application/IPresenter.php';
  22. /**
  23. * Presenter object represents a webpage instance. It executes all the logic for the request.
  24. *
  25. * @author David Grudl
  26. * @copyright Copyright (c) 2004, 2009 David Grudl
  27. * @package Nette\Application
  28. *
  29. * @property-read PresenterRequest $request
  30. * @property-read int $phase
  31. * @property-read array $signal
  32. * @property-read string $action
  33. * @property string $view
  34. * @property string $layout
  35. * @property-read mixed $payload
  36. * @property-read Application $application
  37. */
  38. abstract class Presenter extends Control implements IPresenter
  39. {
  40. /**#@+ life cycle phases {@link Presenter::getPhase()} */
  41. const PHASE_STARTUP = 1;
  42. const PHASE_SIGNAL = 3;
  43. const PHASE_RENDER = 4;
  44. const PHASE_SHUTDOWN = 5;
  45. /**#@-*/
  46. /**#@+ bad link handling {@link Presenter::$invalidLinkMode} */
  47. const INVALID_LINK_SILENT = 1;
  48. const INVALID_LINK_WARNING = 2;
  49. const INVALID_LINK_EXCEPTION = 3;
  50. /**#@-*/
  51. /**#@+ special parameter key */
  52. const SIGNAL_KEY = 'do';
  53. const ACTION_KEY = 'action';
  54. const FLASH_KEY = '_fid';
  55. /**#@-*/
  56. /** @var string */
  57. public static $defaultAction = 'default';
  58. /** @var int */
  59. public static $invalidLinkMode;
  60. /** @var array of function(Presenter $sender, IPresenterResponse $response = NULL); Occurs when the presenter is shutting down */
  61. public $onShutdown;
  62. /** @var bool (experimental) */
  63. public $oldLayoutMode = TRUE;
  64. /** @var bool (experimental) */
  65. public $oldModuleMode = TRUE;
  66. /** @var PresenterRequest */
  67. private $request;
  68. /** @var IPresenterResponse */
  69. private $response;
  70. /** @var int */
  71. private $phase;
  72. /** @var bool automatically call canonicalize() */
  73. public $autoCanonicalize = TRUE;
  74. /** @var bool use absolute Urls or paths? */
  75. public $absoluteUrls = FALSE;
  76. /** @var array */
  77. private $globalParams;
  78. /** @var array */
  79. private $globalState;
  80. /** @var array */
  81. private $globalStateSinces;
  82. /** @var string */
  83. private $action;
  84. /** @var string */
  85. private $view;
  86. /** @var string */
  87. private $layout;
  88. /** @var stdClass */
  89. private $payload;
  90. /** @var string */
  91. private $signalReceiver;
  92. /** @var string */
  93. private $signal;
  94. /** @var bool */
  95. private $ajaxMode;
  96. /** @var bool */
  97. private $startupCheck;
  98. /** @var PresenterRequest */
  99. private $lastCreatedRequest;
  100. /** @var array */
  101. private $lastCreatedRequestFlag;
  102. /**
  103. * @return PresenterRequest
  104. */
  105. final public function getRequest()
  106. {
  107. return $this->request;
  108. }
  109. /**
  110. * Returns self.
  111. * @return Presenter
  112. */
  113. final public function getPresenter($need = TRUE)
  114. {
  115. return $this;
  116. }
  117. /**
  118. * Returns a name that uniquely identifies component.
  119. * @return string
  120. */
  121. final public function getUniqueId()
  122. {
  123. return '';
  124. }
  125. /********************* interface IPresenter ****************d*g**/
  126. /**
  127. * @param PresenterRequest
  128. * @return IPresenterResponse
  129. */
  130. public function run(PresenterRequest $request)
  131. {
  132. try {
  133. // PHASE 1: STARTUP
  134. $this->phase = self::PHASE_STARTUP;
  135. $this->request = $request;
  136. $this->payload = (object) NULL;
  137. $this->setParent($this->getParent(), $request->getPresenterName());
  138. $this->initGlobalParams();
  139. $this->startup();
  140. if (!$this->startupCheck) {
  141. $class = $this->reflection->getMethod('startup')->getDeclaringClass()->getName();
  142. trigger_error("Method $class::startup() or its descendant doesn't call parent::startup().", E_USER_WARNING);
  143. }
  144. // calls $this->action<Action>()
  145. $this->tryCall($this->formatActionMethod($this->getAction()), $this->params);
  146. if ($this->autoCanonicalize) {
  147. $this->canonicalize();
  148. }
  149. if ($this->getHttpRequest()->isMethod('head')) {
  150. $this->terminate();
  151. }
  152. // back compatibility
  153. if (method_exists($this, 'beforePrepare')) {
  154. $this->beforePrepare();
  155. trigger_error('beforePrepare() is deprecated; use createComponent{Name}() instead.', E_USER_WARNING);
  156. }
  157. if ($this->tryCall('prepare' . $this->getView(), $this->params)) {
  158. trigger_error('prepare' . ucfirst($this->getView()) . '() is deprecated; use createComponent{Name}() instead.', E_USER_WARNING);
  159. }
  160. // PHASE 2: SIGNAL HANDLING
  161. $this->phase = self::PHASE_SIGNAL;
  162. // calls $this->handle<Signal>()
  163. $this->processSignal();
  164. // PHASE 3: RENDERING VIEW
  165. $this->phase = self::PHASE_RENDER;
  166. $this->beforeRender();
  167. // calls $this->render<View>()
  168. $this->tryCall($this->formatRenderMethod($this->getView()), $this->params);
  169. $this->afterRender();
  170. // save component tree persistent state
  171. $this->saveGlobalState();
  172. if ($this->isAjax()) {
  173. $this->payload->state = $this->getGlobalState();
  174. }
  175. // finish template rendering
  176. $this->sendTemplate();
  177. } catch (AbortException $e) {
  178. // continue with shutting down
  179. } /* finally */ {
  180. // PHASE 4: SHUTDOWN
  181. $this->phase = self::PHASE_SHUTDOWN;
  182. // back compatibility for use terminate() instead of sendPayload()
  183. if ($this->isAjax() && !($this->response instanceof ForwardingResponse || $this->response instanceof JsonResponse) && (array) $this->payload) {
  184. try { $this->sendPayload(); }
  185. catch (AbortException $e) { }
  186. }
  187. if ($this->hasFlashSession()) {
  188. $this->getFlashSession()->setExpiration($this->response instanceof RedirectingResponse ? '+ 30 seconds': '+ 3 seconds');
  189. }
  190. $this->onShutdown($this, $this->response);
  191. $this->shutdown($this->response);
  192. return $this->response;
  193. }
  194. }
  195. /**
  196. * Returns current presenter life cycle phase.
  197. * @return int
  198. */
  199. final public function getPhase()
  200. {
  201. return $this->phase;
  202. }
  203. /**
  204. * @return void
  205. */
  206. protected function startup()
  207. {
  208. $this->startupCheck = TRUE;
  209. }
  210. /**
  211. * Common render method.
  212. * @return void
  213. */
  214. protected function beforeRender()
  215. {
  216. }
  217. /**
  218. * Common render method.
  219. * @return void
  220. */
  221. protected function afterRender()
  222. {
  223. }
  224. /**
  225. * @param IPresenterResponse optional catched exception
  226. * @return void
  227. */
  228. protected function shutdown($response)
  229. {
  230. }
  231. /********************* signal handling ****************d*g**/
  232. /**
  233. * @return void
  234. * @throws BadSignalException
  235. */
  236. public function processSignal()
  237. {
  238. if ($this->signal === NULL) return;
  239. $component = $this->signalReceiver === '' ? $this : $this->getComponent($this->signalReceiver, FALSE);
  240. if ($component === NULL) {
  241. throw new BadSignalException("The signal receiver component '$this->signalReceiver' is not found.");
  242. } elseif (!$component instanceof ISignalReceiver) {
  243. throw new BadSignalException("The signal receiver component '$this->signalReceiver' is not ISignalReceiver implementor.");
  244. }
  245. // auto invalidate
  246. if ($this->oldLayoutMode && $component instanceof IRenderable) {
  247. $component->invalidateControl();
  248. }
  249. $component->signalReceived($this->signal);
  250. $this->signal = NULL;
  251. }
  252. /**
  253. * Returns pair signal receiver and name.
  254. * @return array|NULL
  255. */
  256. final public function getSignal()
  257. {
  258. return $this->signal === NULL ? NULL : array($this->signalReceiver, $this->signal);
  259. }
  260. /**
  261. * Checks if the signal receiver is the given one.
  262. * @param mixed component or its id
  263. * @param string signal name (optional)
  264. * @return bool
  265. */
  266. final public function isSignalReceiver($component, $signal = NULL)
  267. {
  268. if ($component instanceof /*Nette\*/Component) {
  269. $component = $component === $this ? '' : $component->lookupPath(__CLASS__, TRUE);
  270. }
  271. if ($this->signal === NULL) {
  272. return FALSE;
  273. } elseif ($signal === TRUE) {
  274. return $component === ''
  275. || strncmp($this->signalReceiver . '-', $component . '-', strlen($component) + 1) === 0;
  276. } elseif ($signal === NULL) {
  277. return $this->signalReceiver === $component;
  278. } else {
  279. return $this->signalReceiver === $component && strcasecmp($signal, $this->signal) === 0;
  280. }
  281. }
  282. /********************* rendering ****************d*g**/
  283. /**
  284. * Returns current action name.
  285. * @return string
  286. */
  287. final public function getAction($fullyQualified = FALSE)
  288. {
  289. return $fullyQualified ? ':' . $this->getName() . ':' . $this->action : $this->action;
  290. }
  291. /**
  292. * Changes current action. Only alphanumeric characters are allowed.
  293. * @param string
  294. * @return void
  295. */
  296. public function changeAction($action)
  297. {
  298. if (preg_match("#^[a-zA-Z0-9][a-zA-Z0-9_\x7f-\xff]*$#", $action)) {
  299. $this->action = $action;
  300. $this->view = $action;
  301. } else {
  302. throw new BadRequestException("Action name '$action' is not alphanumeric string.");
  303. }
  304. }
  305. /**
  306. * Returns current view.
  307. * @return string
  308. */
  309. final public function getView()
  310. {
  311. return $this->view;
  312. }
  313. /**
  314. * Changes current view. Any name is allowed.
  315. * @param string
  316. * @return Presenter provides a fluent interface
  317. */
  318. public function setView($view)
  319. {
  320. $this->view = (string) $view;
  321. return $this;
  322. }
  323. /**
  324. * Returns current layout name.
  325. * @return string|FALSE
  326. */
  327. final public function getLayout()
  328. {
  329. return $this->layout;
  330. }
  331. /**
  332. * Changes or disables layout.
  333. * @param string|FALSE
  334. * @return Presenter provides a fluent interface
  335. */
  336. public function setLayout($layout)
  337. {
  338. $this->layout = (string) $layout;
  339. return $this;
  340. }
  341. /**
  342. * @return void
  343. * @throws BadRequestException if no template found
  344. * @throws AbortException
  345. */
  346. public function sendTemplate()
  347. {
  348. $template = $this->getTemplate();
  349. if (!$template) return;
  350. if ($template instanceof /*Nette\Templates\*/IFileTemplate && !$template->getFile()) {
  351. // content template
  352. $files = $this->formatTemplateFiles($this->getName(), $this->view);
  353. foreach ($files as $file) {
  354. if (is_file($file)) {
  355. $template->setFile($file);
  356. break;
  357. }
  358. }
  359. if (!$template->getFile()) {
  360. $file = str_replace(Environment::getVariable('appDir'), "\xE2\x80\xA6", reset($files));
  361. throw new BadRequestException("Page not found. Missing template '$file'.");
  362. }
  363. // layout template
  364. if ($this->layout !== FALSE) {
  365. $files = $this->formatLayoutTemplateFiles($this->getName(), $this->layout ? $this->layout : 'layout');
  366. foreach ($files as $file) {
  367. if (is_file($file)) {
  368. $template->layout = $file;
  369. if ($this->oldLayoutMode) {
  370. $template->content = clone $template;
  371. $template->setFile($file);
  372. } else {
  373. $template->_extends = $file;
  374. }
  375. break;
  376. }
  377. }
  378. if (empty($template->layout) && $this->layout !== NULL) {
  379. $file = str_replace(Environment::getVariable('appDir'), "\xE2\x80\xA6", reset($files));
  380. throw new BadRequestException("Layout not found. Missing template '$file'.");
  381. }
  382. }
  383. }
  384. if ($this->isAjax()) { // TODO!
  385. /*Nette\Templates\*/SnippetHelper::$outputAllowed = FALSE;
  386. $template->render();
  387. $this->sendPayload();
  388. }
  389. $this->terminate(new RenderResponse($template));
  390. }
  391. /**
  392. * Formats layout template file names.
  393. * @param string
  394. * @param string
  395. * @return array
  396. */
  397. public function formatLayoutTemplateFiles($presenter, $layout)
  398. {
  399. if ($this->oldModuleMode) {
  400. $root = Environment::getVariable('templatesDir', Environment::getVariable('appDir') . '/templates'); // back compatibility
  401. $presenter = str_replace(':', 'Module/', $presenter);
  402. $module = substr($presenter, 0, (int) strrpos($presenter, '/'));
  403. $base = '';
  404. if ($root === Environment::getVariable('appDir') . '/presenters') {
  405. $base = 'templates/';
  406. if ($module === '') {
  407. $presenter = 'templates/' . $presenter;
  408. } else {
  409. $presenter = substr_replace($presenter, '/templates', strrpos($presenter, '/'), 0);
  410. }
  411. }
  412. return array(
  413. "$root/$presenter/@$layout.phtml",
  414. "$root/$presenter.@$layout.phtml",
  415. "$root/$module/$base@$layout.phtml",
  416. "$root/$base@$layout.phtml",
  417. );
  418. }
  419. $appDir = Environment::getVariable('appDir');
  420. $path = '/' . str_replace(':', 'Module/', $presenter);
  421. $pathP = substr_replace($path, '/templates', strrpos($path, '/'), 0);
  422. $list = array(
  423. "$appDir$pathP/@$layout.phtml",
  424. "$appDir$pathP.@$layout.phtml",
  425. );
  426. while (($path = substr($path, 0, strrpos($path, '/'))) !== FALSE) {
  427. $list[] = "$appDir$path/templates/@$layout.phtml";
  428. }
  429. return $list;
  430. }
  431. /**
  432. * Formats view template file names.
  433. * @param string
  434. * @param string
  435. * @return array
  436. */
  437. public function formatTemplateFiles($presenter, $view)
  438. {
  439. if ($this->oldModuleMode) {
  440. $root = Environment::getVariable('templatesDir', Environment::getVariable('appDir') . '/templates'); // back compatibility
  441. $presenter = str_replace(':', 'Module/', $presenter);
  442. $dir = '';
  443. if ($root === Environment::getVariable('appDir') . '/presenters') { // special supported case
  444. $pos = strrpos($presenter, '/');
  445. $presenter = $pos === FALSE ? 'templates/' . $presenter : substr_replace($presenter, '/templates', $pos, 0);
  446. $dir = 'templates/';
  447. }
  448. return array(
  449. "$root/$presenter/$view.phtml",
  450. "$root/$presenter.$view.phtml",
  451. "$root/$dir@global.$view.phtml",
  452. );
  453. }
  454. $appDir = Environment::getVariable('appDir');
  455. $path = '/' . str_replace(':', 'Module/', $presenter);
  456. $pathP = substr_replace($path, '/templates', strrpos($path, '/'), 0);
  457. $path = substr_replace($path, '/templates', strrpos($path, '/'));
  458. return array(
  459. "$appDir$pathP/$view.phtml",
  460. "$appDir$pathP.$view.phtml",
  461. "$appDir$path/@global.$view.phtml",
  462. );
  463. }
  464. /**
  465. * Formats action method name.
  466. * @param string
  467. * @return string
  468. */
  469. protected static function formatActionMethod($action)
  470. {
  471. return 'action' . $action;
  472. }
  473. /**
  474. * Formats render view method name.
  475. * @param string
  476. * @return string
  477. */
  478. protected static function formatRenderMethod($view)
  479. {
  480. return 'render' . $view;
  481. }
  482. /**
  483. * @deprecated
  484. */
  485. protected function renderTemplate()
  486. {
  487. throw new /*\*/DeprecatedException(__METHOD__ . '() is deprecated; use $presenter->sendTemplate() instead.');
  488. }
  489. /********************* partial AJAX rendering ****************d*g**/
  490. /**
  491. * @return stdClass
  492. */
  493. final public function getPayload()
  494. {
  495. return $this->payload;
  496. }
  497. /**
  498. * Is AJAX request?
  499. * @return bool
  500. */
  501. public function isAjax()
  502. {
  503. if ($this->ajaxMode === NULL) {
  504. $this->ajaxMode = $this->getHttpRequest()->isAjax();
  505. }
  506. return $this->ajaxMode;
  507. }
  508. /**
  509. * Sends AJAX payload to the output.
  510. * @return void
  511. * @throws AbortException
  512. */
  513. protected function sendPayload()
  514. {
  515. $this->terminate(new JsonResponse($this->payload));
  516. }
  517. /**
  518. * @deprecated
  519. */
  520. public function getAjaxDriver()
  521. {
  522. throw new /*\*/DeprecatedException(__METHOD__ . '() is deprecated; use $presenter->payload instead.');
  523. }
  524. /********************* navigation & flow ****************d*g**/
  525. /**
  526. * Forward to another presenter or action.
  527. * @param string|PresenterRequest
  528. * @param array|mixed
  529. * @return void
  530. * @throws AbortException
  531. */
  532. public function forward($destination, $args = array())
  533. {
  534. if ($destination instanceof PresenterRequest) {
  535. $this->terminate(new ForwardingResponse($destination));
  536. } elseif (!is_array($args)) {
  537. $args = func_get_args();
  538. array_shift($args);
  539. }
  540. $this->createRequest($this, $destination, $args, 'forward');
  541. $this->terminate(new ForwardingResponse($this->lastCreatedRequest));
  542. }
  543. /**
  544. * Redirect to another URL and ends presenter execution.
  545. * @param string
  546. * @param int HTTP error code
  547. * @return void
  548. * @throws AbortException
  549. */
  550. public function redirectUri($uri, $code = NULL)
  551. {
  552. if ($this->isAjax()) {
  553. $this->payload->redirect = (string) $uri;
  554. $this->sendPayload();
  555. } elseif (!$code) {
  556. $code = $this->getHttpRequest()->isMethod('post') ? /*Nette\Web\*/IHttpResponse::S303_POST_GET : /*Nette\Web\*/IHttpResponse::S302_FOUND;
  557. }
  558. $this->terminate(new RedirectingResponse($uri, $code));
  559. }
  560. /**
  561. * Link to myself.
  562. * @return string
  563. */
  564. public function backlink()
  565. {
  566. return $this->getAction(TRUE);
  567. }
  568. /**
  569. * Returns the last created PresenterRequest.
  570. * @return PresenterRequest
  571. */
  572. public function getLastCreatedRequest()
  573. {
  574. return $this->lastCreatedRequest;
  575. }
  576. /**
  577. * Returns the last created PresenterRequest flag.
  578. * @param string
  579. * @return bool
  580. */
  581. public function getLastCreatedRequestFlag($flag)
  582. {
  583. return !empty($this->lastCreatedRequestFlag[$flag]);
  584. }
  585. /**
  586. * Correctly terminates presenter.
  587. * @param IPresenterResponse
  588. * @return void
  589. * @throws AbortException
  590. */
  591. public function terminate(IPresenterResponse $response = NULL)
  592. {
  593. $this->response = $response;
  594. throw new AbortException();
  595. }
  596. /**
  597. * Conditional redirect to canonicalized URI.
  598. * @return void
  599. * @throws AbortException
  600. */
  601. public function canonicalize()
  602. {
  603. if (!$this->isAjax() && ($this->request->isMethod('get') || $this->request->isMethod('head'))) {
  604. $uri = $this->createRequest($this, $this->action, $this->getGlobalState() + $this->request->params, 'redirectX');
  605. if ($uri !== NULL && !$this->getHttpRequest()->getUri()->isEqual($uri)) {
  606. $this->terminate(new RedirectingResponse($uri, /*Nette\Web\*/IHttpResponse::S301_MOVED_PERMANENTLY));
  607. }
  608. }
  609. }
  610. /**
  611. * Attempts to cache the sent entity by its last modification date
  612. * @param int last modified time as unix timestamp
  613. * @param string strong entity tag validator
  614. * @param mixed optional expiration time
  615. * @return int date of the client's cache version, if available
  616. * @throws AbortException
  617. */
  618. public function lastModified($lastModified, $etag = NULL, $expire = NULL)
  619. {
  620. if (!Environment::isProduction()) {
  621. return NULL;
  622. }
  623. $httpResponse = $this->getHttpResponse();
  624. $match = FALSE;
  625. if ($lastModified > 0) {
  626. $httpResponse->setHeader('Last-Modified', /*Nette\Web\*/HttpResponse::date($lastModified));
  627. }
  628. if ($etag != NULL) { // intentionally ==
  629. $etag = '"' . addslashes($etag) . '"';
  630. $httpResponse->setHeader('ETag', $etag);
  631. }
  632. if ($expire !== NULL) {
  633. $httpResponse->expire($expire);
  634. }
  635. $ifNoneMatch = $this->getHttpRequest()->getHeader('if-none-match');
  636. $ifModifiedSince = $this->getHttpRequest()->getHeader('if-modified-since');
  637. if ($ifModifiedSince !== NULL) {
  638. $ifModifiedSince = strtotime($ifModifiedSince);
  639. }
  640. if ($ifNoneMatch !== NULL) {
  641. if ($ifNoneMatch === '*') {
  642. $match = TRUE; // match, check if-modified-since
  643. } elseif ($etag == NULL || strpos(' ' . strtr($ifNoneMatch, ",\t", ' '), ' ' . $etag) === FALSE) {
  644. return $ifModifiedSince; // no match, ignore if-modified-since
  645. } else {
  646. $match = TRUE; // match, check if-modified-since
  647. }
  648. }
  649. if ($ifModifiedSince !== NULL) {
  650. if ($lastModified > 0 && $lastModified <= $ifModifiedSince) {
  651. $match = TRUE;
  652. } else {
  653. return $ifModifiedSince;
  654. }
  655. }
  656. if ($match) {
  657. $httpResponse->setCode(/*Nette\Web\*/IHttpResponse::S304_NOT_MODIFIED);
  658. $httpResponse->setHeader('Content-Length', '0');
  659. $this->terminate();
  660. } else {
  661. return $ifModifiedSince;
  662. }
  663. return NULL;
  664. }
  665. /**
  666. * PresenterRequest/URL factory.
  667. * @param PresenterComponent base
  668. * @param string destination in format "[[module:]presenter:]action" or "signal!"
  669. * @param array array of arguments
  670. * @param string forward|redirect|link
  671. * @return string URL
  672. * @throws InvalidLinkException
  673. * @internal
  674. */
  675. final protected function createRequest($component, $destination, array $args, $mode)
  676. {
  677. // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final
  678. // cached services for better performance
  679. static $presenterLoader, $router, $httpRequest;
  680. if ($presenterLoader === NULL) {
  681. $presenterLoader = $this->getApplication()->getPresenterLoader();
  682. $router = $this->getApplication()->getRouter();
  683. $httpRequest = $this->getHttpRequest();
  684. }
  685. $this->lastCreatedRequest = $this->lastCreatedRequestFlag = NULL;
  686. // PARSE DESTINATION
  687. // 1) fragment
  688. $a = strpos($destination, '#');
  689. if ($a === FALSE) {
  690. $fragment = '';
  691. } else {
  692. $fragment = substr($destination, $a);
  693. $destination = substr($destination, 0, $a);
  694. }
  695. // 2) ?query syntax
  696. $a = strpos($destination, '?');
  697. if ($a !== FALSE) {
  698. parse_str(substr($destination, $a + 1), $args); // requires disabled magic quotes
  699. $destination = substr($destination, 0, $a);
  700. }
  701. // 3) URL scheme
  702. $a = strpos($destination, '//');
  703. if ($a === FALSE) {
  704. $scheme = FALSE;
  705. } else {
  706. $scheme = substr($destination, 0, $a);
  707. $destination = substr($destination, $a + 2);
  708. }
  709. // 4) signal or empty
  710. if (!($component instanceof Presenter) || substr($destination, -1) === '!') {
  711. $signal = rtrim($destination, '!');
  712. $a = strrpos($signal, ':');
  713. if ($a !== FALSE) {
  714. $component = $component->getComponent(strtr(substr($signal, 0, $a), ':', '-'));
  715. $signal = (string) substr($signal, $a + 1);
  716. }
  717. if ($signal == NULL) { // intentionally ==
  718. throw new InvalidLinkException("Signal must be non-empty string.");
  719. }
  720. $destination = 'this';
  721. }
  722. if ($destination == NULL) { // intentionally ==
  723. throw new InvalidLinkException("Destination must be non-empty string.");
  724. }
  725. // 5) presenter: action
  726. $current = FALSE;
  727. $a = strrpos($destination, ':');
  728. if ($a === FALSE) {
  729. $action = $destination === 'this' ? $this->action : $destination;
  730. $presenter = $this->getName();
  731. $presenterClass = $this->getClass();
  732. } else {
  733. $action = (string) substr($destination, $a + 1);
  734. if ($destination[0] === ':') { // absolute
  735. if ($a < 2) {
  736. throw new InvalidLinkException("Missing presenter name in '$destination'.");
  737. }
  738. $presenter = substr($destination, 1, $a - 1);
  739. } else { // relative
  740. $presenter = $this->getName();
  741. $b = strrpos($presenter, ':');
  742. if ($b === FALSE) { // no module
  743. $presenter = substr($destination, 0, $a);
  744. } else { // with module
  745. $presenter = substr($presenter, 0, $b + 1) . substr($destination, 0, $a);
  746. }
  747. }
  748. $presenterClass = $presenterLoader->getPresenterClass($presenter);
  749. }
  750. // PROCESS SIGNAL ARGUMENTS
  751. if (isset($signal)) { // $component must be IStatePersistent
  752. $class = get_class($component);
  753. if ($signal === 'this') { // means "no signal"
  754. $signal = '';
  755. if (array_key_exists(0, $args)) {
  756. throw new InvalidLinkException("Extra parameter for signal '$class:$signal!'.");
  757. }
  758. } elseif (strpos($signal, self::NAME_SEPARATOR) === FALSE) { // TODO: AppForm exception
  759. // counterpart of signalReceived() & tryCall()
  760. $method = $component->formatSignalMethod($signal);
  761. if (!PresenterHelpers::isMethodCallable($class, $method)) {
  762. throw new InvalidLinkException("Unknown signal '$class:$signal!'.");
  763. }
  764. if ($args) { // convert indexed parameters to named
  765. PresenterHelpers::argsToParams($class, $method, $args);
  766. }
  767. }
  768. // counterpart of IStatePersistent
  769. if ($args && array_intersect_key($args, PresenterHelpers::getPersistentParams($class))) {
  770. $component->saveState($args);
  771. }
  772. if ($args && $component !== $this) {
  773. $prefix = $component->getUniqueId() . self::NAME_SEPARATOR;
  774. foreach ($args as $key => $val) {
  775. unset($args[$key]);
  776. $args[$prefix . $key] = $val;
  777. }
  778. }
  779. }
  780. // PROCESS ARGUMENTS
  781. if (is_subclass_of($presenterClass, __CLASS__)) {
  782. if ($action === '') {
  783. /*$action = $presenterClass::$defaultAction;*/ // in PHP 5.3
  784. /**/$action = self::$defaultAction;/**/
  785. }
  786. $current = ($action === '*' || $action === $this->action) && $presenterClass === $this->getClass(); // TODO
  787. if ($args || $destination === 'this') {
  788. // counterpart of run() & tryCall()
  789. /*$method = $presenterClass::formatActionMethod($action);*/ // in PHP 5.3
  790. /**/$method = call_user_func(array($presenterClass, 'formatActionMethod'), $action);/**/
  791. if (!PresenterHelpers::isMethodCallable($presenterClass, $method)) {
  792. /*$method = $presenterClass::formatRenderMethod($action);*/ // in PHP 5.3
  793. /**/$method = call_user_func(array($presenterClass, 'formatRenderMethod'), $action);/**/
  794. if (!PresenterHelpers::isMethodCallable($presenterClass, $method)) {
  795. $method = NULL;
  796. }
  797. }
  798. // convert indexed parameters to named
  799. if ($method === NULL) {
  800. if (array_key_exists(0, $args)) {
  801. throw new InvalidLinkException("Extra parameter for '$presenter:$action'.");
  802. }
  803. } elseif ($destination === 'this') {
  804. PresenterHelpers::argsToParams($presenterClass, $method, $args, $this->params);
  805. } else {
  806. PresenterHelpers::argsToParams($presenterClass, $method, $args);
  807. }
  808. }
  809. // counterpart of IStatePersistent
  810. if ($args && array_intersect_key($args, PresenterHelpers::getPersistentParams($presenterClass))) {
  811. $this->saveState($args, $presenterClass);
  812. }
  813. $globalState = $this->getGlobalState($destination === 'this' ? NULL : $presenterClass);
  814. if ($current && $args) {
  815. $tmp = $globalState + $this->params;
  816. foreach ($args as $key => $val) {
  817. if ((string) $val !== (isset($tmp[$key]) ? (string) $tmp[$key] : '')) {
  818. $current = FALSE;
  819. break;
  820. }
  821. }
  822. }
  823. $args += $globalState;
  824. }
  825. // ADD ACTION & SIGNAL & FLASH
  826. $args[self::ACTION_KEY] = $action;
  827. if (!empty($signal)) {
  828. $args[self::SIGNAL_KEY] = $component->getParamId($signal);
  829. $current = $current && $args[self::SIGNAL_KEY] === $this->getParam(self::SIGNAL_KEY);
  830. }
  831. if (($mode === 'redirect' || $mode === 'forward') && $this->hasFlashSession()) {
  832. $args[self::FLASH_KEY] = $this->getParam(self::FLASH_KEY);
  833. }
  834. $this->lastCreatedRequest = new PresenterRequest(
  835. $presenter,
  836. PresenterRequest::FORWARD,
  837. $args,
  838. array(),
  839. array()
  840. );
  841. $this->lastCreatedRequestFlag = array('current' => $current);
  842. if ($mode === 'forward') return;
  843. // CONSTRUCT URL
  844. $uri = $router->constructUrl($this->lastCreatedRequest, $httpRequest);
  845. if ($uri === NULL) {
  846. unset($args[self::ACTION_KEY]);
  847. $params = urldecode(http_build_query($args, NULL, ', '));
  848. throw new InvalidLinkException("No route for $presenter:$action($params)");
  849. }
  850. // make URL relative if possible
  851. if ($mode === 'link' && $scheme === FALSE && !$this->absoluteUrls) {
  852. $hostUri = $httpRequest->getUri()->getHostUri();
  853. if (strncmp($uri, $hostUri, strlen($hostUri)) === 0) {
  854. $uri = substr($uri, strlen($hostUri));
  855. }
  856. }
  857. return $uri . $fragment;
  858. }
  859. /**
  860. * Invalid link handler. Descendant can override this method to change default behaviour.
  861. * @param InvalidLinkException
  862. * @return string
  863. * @throws InvalidLinkException
  864. */
  865. protected function handleInvalidLink($e)
  866. {
  867. if (self::$invalidLinkMode === NULL) {
  868. self::$invalidLinkMode = Environment::isProduction()
  869. ? self::INVALID_LINK_SILENT : self::INVALID_LINK_WARNING;
  870. }
  871. if (self::$invalidLinkMode === self::INVALID_LINK_SILENT) {
  872. return '#';
  873. } elseif (self::$invalidLinkMode === self::INVALID_LINK_WARNING) {
  874. return 'error: ' . htmlSpecialChars($e->getMessage());
  875. } else { // self::INVALID_LINK_EXCEPTION
  876. throw $e;
  877. }
  878. }
  879. /********************* interface IStatePersistent ****************d*g**/
  880. /**
  881. * Returns array of persistent components.
  882. * This default implementation detects components by class-level annotation @persistent(cmp1, cmp2).
  883. * @return array
  884. */
  885. public static function getPersistentComponents()
  886. {
  887. return (array) /*Nette\*/Annotations::get(new /*\*/ReflectionClass(/**/func_get_arg(0)/**//*get_called_class()*/), 'persistent');
  888. }
  889. /**
  890. * Saves state information for all subcomponents to $this->globalState.
  891. * @return array
  892. */
  893. private function getGlobalState($forClass = NULL)
  894. {
  895. $sinces = & $this->globalStateSinces;
  896. if ($this->globalState === NULL) {
  897. if ($this->phase >= self::PHASE_SHUTDOWN) {
  898. throw new /*\*/InvalidStateException("Presenter is shutting down, cannot save state.");
  899. }
  900. $state = array();
  901. foreach ($this->globalParams as $id => $params) {
  902. $prefix = $id . self::NAME_SEPARATOR;
  903. foreach ($params as $key => $val) {
  904. $state[$prefix . $key] = $val;
  905. }
  906. }
  907. $this->saveState($state, $forClass);
  908. if ($sinces === NULL) {
  909. $sinces = array();
  910. foreach (PresenterHelpers::getPersistentParams(get_class($this)) as $nm => $meta) {
  911. $sinces[$nm] = $meta['since'];
  912. }
  913. }
  914. $components = PresenterHelpers::getPersistentComponents(get_class($this));
  915. $iterator = $this->getComponents(TRUE, 'Nette\Application\IStatePersistent');
  916. foreach ($iterator as $name => $component)
  917. {
  918. if ($iterator->getDepth() === 0) {
  919. // counts with RecursiveIteratorIterator::SELF_FIRST
  920. $since = isset($components[$name]['since']) ? $components[$name]['since'] : FALSE; // FALSE = nonpersistent
  921. }
  922. $prefix = $component->getUniqueId() . self::NAME_SEPARATOR;
  923. $params = array();
  924. $component->saveState($params);
  925. foreach ($params as $key => $val) {
  926. $state[$prefix . $key] = $val;
  927. $sinces[$prefix . $key] = $since;
  928. }
  929. }
  930. } else {
  931. $state = $this->globalState;
  932. }
  933. if ($forClass !== NULL) {
  934. $since = NULL;
  935. foreach ($state as $key => $foo) {
  936. if (!isset($sinces[$key])) {
  937. $x = strpos($key, self::NAME_SEPARATOR);
  938. $x = $x === FALSE ? $key : substr($key, 0, $x);
  939. $sinces[$key] = isset($sinces[$x]) ? $sinces[$x] : FALSE;
  940. }
  941. if ($since !== $sinces[$key]) {
  942. $since = $sinces[$key];
  943. $ok = $since && (is_subclass_of($forClass, $since) || $forClass === $since);
  944. }
  945. if (!$ok) {
  946. unset($state[$key]);
  947. }
  948. }
  949. }
  950. return $state;
  951. }
  952. /**
  953. * Permanently saves state information for all subcomponents to $this->globalState.
  954. * @return void
  955. */
  956. protected function saveGlobalState()
  957. {
  958. // load lazy components
  959. foreach ($this->globalParams as $id => $foo) {
  960. $this->getComponent($id, FALSE);
  961. }
  962. $this->globalParams = array();
  963. $this->globalState = $this->getGlobalState();
  964. }
  965. /**
  966. * Initializes $this->globalParams, $this->signal & $this->signalReceiver, $this->action, $this->view. Called by run().
  967. * @return void
  968. * @throws BadRequestException if action name is not valid
  969. */
  970. private function initGlobalParams()
  971. {
  972. // init $this->globalParams
  973. $this->globalParams = array();
  974. $selfParams = array();
  975. $params = $this->request->getParams();
  976. if ($this->isAjax()) {
  977. $params = $this->request->getPost() + $params;
  978. }
  979. foreach ($params as $key => $value) {
  980. $a = strlen($key) > 2 ? strrpos($key, self::NAME_SEPARATOR, -2) : FALSE;
  981. if ($a === FALSE) {
  982. $selfParams[$key] = $value;
  983. } else {
  984. $this->globalParams[substr($key, 0, $a)][substr($key, $a + 1)] = $value;
  985. }
  986. }
  987. // init & validate $this->action & $this->view
  988. $this->changeAction(isset($selfParams[self::ACTION_KEY]) ? $selfParams[self::ACTION_KEY] : self::$defaultAction);
  989. // init $this->signalReceiver and key 'signal' in appropriate params array
  990. $this->signalReceiver = $this->getUniqueId();
  991. if (!empty($selfParams[self::SIGNAL_KEY])) {
  992. $param = $selfParams[self::SIGNAL_KEY];
  993. $pos = strrpos($param, '-');
  994. if ($pos) {
  995. $this->signalReceiver = substr($param, 0, $pos);
  996. $this->signal = substr($param, $pos + 1);
  997. } else {
  998. $this->signalReceiver = $this->getUniqueId();
  999. $this->signal = $param;
  1000. }
  1001. if ($this->signal == NULL) { // intentionally ==
  1002. $this->signal = NULL;
  1003. }
  1004. }
  1005. $this->loadState($selfParams);
  1006. }
  1007. /**
  1008. * Pops parameters for specified component.
  1009. * @param string component id
  1010. * @return array
  1011. */
  1012. final public function popGlobalParams($id)
  1013. {
  1014. if (isset($this->globalParams[$id])) {
  1015. $res = $this->globalParams[$id];
  1016. unset($this->globalParams[$id]);
  1017. return $res;
  1018. } else {
  1019. return array();
  1020. }
  1021. }
  1022. /********************* flash session ****************d*g**/
  1023. /**
  1024. * Checks if a flash session namespace exists.
  1025. * @return bool
  1026. */
  1027. public function hasFlashSession()
  1028. {
  1029. return !empty($this->params[self::FLASH_KEY])
  1030. && $this->getSession()->hasNamespace('Nette.Application.Flash/' . $this->params[self::FLASH_KEY]);
  1031. }
  1032. /**
  1033. * Returns session namespace provided to pass temporary data between redirects.
  1034. * @return Nette\Web\SessionNamespace
  1035. */
  1036. public function getFlashSession()
  1037. {
  1038. if (empty($this->params[self::FLASH_KEY])) {
  1039. $this->params[self::FLASH_KEY] = substr(md5(lcg_value()), 0, 4);
  1040. }
  1041. return $this->getSession()->getNamespace('Nette.Application.Flash/' . $this->params[self::FLASH_KEY]);
  1042. }
  1043. /********************* backend ****************d*g**/
  1044. /**
  1045. * @return Nette\Web\IHttpRequest
  1046. */
  1047. protected function getHttpRequest()
  1048. {
  1049. return Environment::getHttpRequest();
  1050. }
  1051. /**
  1052. * @return Nette\Web\IHttpResponse
  1053. */
  1054. protected function getHttpResponse()
  1055. {
  1056. return Environment::getHttpResponse();
  1057. }
  1058. /**
  1059. * @return Application
  1060. */
  1061. public function getApplication()
  1062. {
  1063. return Environment::getApplication();
  1064. }
  1065. /**
  1066. * @return Nette\Web\Session
  1067. */
  1068. private function getSession()
  1069. {
  1070. return Environment::getSession();
  1071. }
  1072. }