diff --git a/src/Application/Application.php b/src/Application/Application.php index 9ac0c2a84..9de612977 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -67,13 +67,17 @@ class Application extends Nette\Object /** @var IRouter */ private $router; + /** @var IRequestStorage */ + private $requestStorage; - public function __construct(IPresenterFactory $presenterFactory, IRouter $router, Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) + + public function __construct(IPresenterFactory $presenterFactory, IRouter $router, Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse, IRequestStorage $requestStorage) { $this->httpRequest = $httpRequest; $this->httpResponse = $httpResponse; $this->presenterFactory = $presenterFactory; $this->router = $router; + $this->requestStorage = $requestStorage; } @@ -111,7 +115,7 @@ public function run() */ public function createInitialRequest() { - $request = $this->router->match($this->httpRequest); + $request = $this->requestStorage->getRequest($this->httpRequest) ?: $this->router->match($this->httpRequest); if (!$request instanceof Request) { throw new BadRequestException('No route for HTTP request.'); diff --git a/src/Application/IMessageStorage.php b/src/Application/IMessageStorage.php new file mode 100644 index 000000000..f2dd17147 --- /dev/null +++ b/src/Application/IMessageStorage.php @@ -0,0 +1,65 @@ +session = $session; + } + + + /** + * @param string + */ + public function setId($id) + { + $this->id = $id; + } + + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + + /** + * Checks if a storage contains messages. + * @return bool + */ + public function hasMessages() + { + return $this->id && $this->session->hasSection('Nette.Application.Flash/' . $this->id); + } + + + /** + * Stores flash message for given component path and returns it's value object for further modifications. + * @param string + * @param string + * @param string + * @return object + */ + public function addMessage($message, $type = 'info', $id = 'flash') + { + $messages = $this->getSession()->$id; + $messages[] = $flash = (object) array( + 'message' => $message, + 'type' => $type, + ); + $this->getSession()->$id = $messages; + + return $flash; + } + + + /** + * Returns array of stored flash messages for given component path. + * @param string + * @return object[] + */ + public function getMessages($id = NULL) + { + return $this->getSession()->$id; + } + + + /** + * Returns session namespace provided to pass temporary data between redirects. + * @internal + * @return Nette\Http\SessionSection + */ + public function getSession() + { + if (!$this->id) { + $this->id = Nette\Utils\Random::generate(4); + } + return $this->session->getSection('Nette.Application.Flash/' . $this->id); + } + + + + /** + * Sets expiration for currently opened messages + * @param string + * @return void + */ + public function setExpiration($expiration) + { + if ($this->hasMessages()) { + $this->getSession()->setExpiration($expiration); + } + } + +} diff --git a/src/Application/RequestStorage.php b/src/Application/RequestStorage.php new file mode 100644 index 000000000..cd7a3a8fe --- /dev/null +++ b/src/Application/RequestStorage.php @@ -0,0 +1,133 @@ +httpRequest = $httpRequest; + $this->session = $session; + $this->user = $user; + $this->flashStorage = $flashStorage; + } + + + /** + * Stores current request to session. + * @param Request application request + * @param string expiration time + * @return string key + */ + public function storeRequest(Request $request, $expiration = '+ 10 minutes') + { + $session = $this->session->getSection('Nette.Application/requests'); + do { + $key = Nette\Utils\Random::generate(5); + } while (isset($session[$key])); + + $url = clone $this->httpRequest->getUrl(); + + $session[$key] = array( + 'user' => $this->user->getId(), + 'url' => $url->appendQuery(array(static::REQUEST_KEY => $key))->getAbsoluteUrl(), + 'request' => $request, + ); + $session->setExpiration($expiration, $key); + return $key; + } + + + /** + * Restores request from session. + * @param string key + * @return Responses\RedirectResponse|NULL + */ + public function restoreRequest($key) + { + list($request, $url) = $this->loadRequestFromSession($key); + if ($request === NULL) { + return NULL; + } + + if ($this->flashStorage->hasMessages()) { + $url .= '&' . IMessageStorage::FLASH_KEY . '=' . $this->flashStorage->getId(); + } + + return new Responses\RedirectResponse($url); + } + + + /** + * Returns stored request. + * @param \Nette\Http\IRequest + * @return Request|NULL + */ + public function getRequest(Nette\Http\IRequest $httpRequest) + { + $key = $httpRequest->getQuery(static::REQUEST_KEY); + + list($request, $url) = $this->loadRequestFromSession($key); + if ($request === NULL) { + return NULL; + } + + $flash = $this->httpRequest->getUrl()->getQueryParameter(IMessageStorage::FLASH_KEY); + if ($flash !== NULL) { + $parameters = $request->getParameters(); + $request->setParameters($parameters + array(IMessageStorage::FLASH_KEY => $flash)); + } + + return $request; + } + + + /** + * Loads request from session by its key. + * @param string key + * @return array(Request, string) + */ + protected function loadRequestFromSession($key) + { + $session = $this->session->getSection('Nette.Application/requests'); + if (!isset($session[$key]) || ($session[$key]['user'] !== NULL && $session[$key]['user'] !== $this->user->getId())) { + return array(NULL, NULL); + } + + $request = clone $session[$key]['request']; + $request->setFlag(Request::RESTORED, TRUE); + + return array($request, $session[$key]['url']); + } + +} diff --git a/src/Application/UI/Control.php b/src/Application/UI/Control.php index 7a6a7d726..dda9721bf 100644 --- a/src/Application/UI/Control.php +++ b/src/Application/UI/Control.php @@ -87,14 +87,10 @@ public function templatePrepareFilters($template) */ public function flashMessage($message, $type = 'info') { + $storage = $this->getPresenter()->getFlashStorage(); $id = $this->getParameterId('flash'); - $messages = $this->getPresenter()->getFlashSession()->$id; - $messages[] = $flash = (object) array( - 'message' => $message, - 'type' => $type, - ); - $this->getTemplate()->flashes = $messages; - $this->getPresenter()->getFlashSession()->$id = $messages; + $flash = $storage->addMessage($message, $type, $id); + $this->getTemplate()->flashes = $storage->getMessages($id); return $flash; } diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 192dbc63b..cad45ba14 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -42,7 +42,7 @@ abstract class Presenter extends Control implements Application\IPresenter /** @internal special parameter key */ const SIGNAL_KEY = 'do', ACTION_KEY = 'action', - FLASH_KEY = '_fid', + FLASH_KEY = Application\IMessageStorage::FLASH_KEY, DEFAULT_ACTION = 'default'; /** @var int */ @@ -126,6 +126,12 @@ abstract class Presenter extends Control implements Application\IPresenter /** @var ITemplateFactory */ private $templateFactory; + /** @var Application\IMessageStorage */ + private $messageStorage; + + /** @var Application\IRequestStorage */ + private $requestStorage; + public function __construct() { @@ -180,6 +186,7 @@ public function run(Application\Request $request) } $this->initGlobalParameters(); + $this->messageStorage->setId($this->getParameter(self::FLASH_KEY)); $this->checkRequirements($this->getReflection()); $this->startup(); if (!$this->startupCheck) { @@ -234,9 +241,7 @@ public function run(Application\Request $request) } } catch (Application\AbortException $e) { } - if ($this->hasFlashSession()) { - $this->getFlashSession()->setExpiration($this->response instanceof Responses\RedirectResponse ? '+ 30 seconds' : '+ 3 seconds'); - } + $this->messageStorage->setExpiration($this->response instanceof Responses\RedirectResponse ? '+ 30 seconds' : '+ 3 seconds'); // SHUTDOWN $this->onShutdown($this, $this->response); @@ -954,7 +959,10 @@ protected function createRequest($component, $destination, array $args, $mode) $args[self::SIGNAL_KEY] = $component->getParameterId($signal); $current = $current && $args[self::SIGNAL_KEY] === $this->getParameter(self::SIGNAL_KEY); } - if (($mode === 'redirect' || $mode === 'forward') && $this->hasFlashSession()) { + if (($mode === 'redirect' || $mode === 'forward') && $this->messageStorage->hasMessages()) { + if (empty($this->params[self::FLASH_KEY])) { + $this->params[self::FLASH_KEY] = $this->messageStorage->getId(); + } $args[self::FLASH_KEY] = $this->getParameter(self::FLASH_KEY); } @@ -1077,14 +1085,7 @@ protected function handleInvalidLink(InvalidLinkException $e) */ public function storeRequest($expiration = '+ 10 minutes') { - $session = $this->session->getSection('Nette.Application/requests'); - do { - $key = Nette\Utils\Random::generate(5); - } while (isset($session[$key])); - - $session[$key] = array($this->user->getId(), $this->request); - $session->setExpiration($expiration, $key); - return $key; + return $this->requestStorage->storeRequest($this->request, $expiration); } @@ -1095,17 +1096,9 @@ public function storeRequest($expiration = '+ 10 minutes') */ public function restoreRequest($key) { - $session = $this->session->getSection('Nette.Application/requests'); - if (!isset($session[$key]) || ($session[$key][0] !== NULL && $session[$key][0] !== $this->user->getId())) { - return; + if ($response = $this->requestStorage->restoreRequest($key)) { + $this->sendResponse($response); } - $request = clone $session[$key][1]; - unset($session[$key]); - $request->setFlag(Application\Request::RESTORED, TRUE); - $params = $request->getParameters(); - $params[self::FLASH_KEY] = $this->getParameter(self::FLASH_KEY); - $request->setParameters($params); - $this->sendResponse(new Responses\ForwardResponse($request)); } @@ -1284,28 +1277,35 @@ public function popGlobalParameters($id) */ public function hasFlashSession() { - return !empty($this->params[self::FLASH_KEY]) - && $this->session->hasSection('Nette.Application.Flash/' . $this->params[self::FLASH_KEY]); + return $this->messageStorage->hasMessages(); } /** - * Returns session namespace provided to pass temporary data between redirects. + * @return Application\MessageStorage + */ + public function getFlashStorage() + { + return $this->messageStorage; + } + + + /** + * @deprecated * @return Nette\Http\SessionSection */ public function getFlashSession() { - if (empty($this->params[self::FLASH_KEY])) { - $this->params[self::FLASH_KEY] = Nette\Utils\Random::generate(4); - } - return $this->session->getSection('Nette.Application.Flash/' . $this->params[self::FLASH_KEY]); + return $this->getFlashStorage()->getSession(); } /********************* services ****************d*g**/ - public function injectPrimary(Nette\DI\Container $context, Nette\Application\IPresenterFactory $presenterFactory, Nette\Application\IRouter $router, Http\IRequest $httpRequest, Http\IResponse $httpResponse, Http\Session $session, Nette\Security\User $user, ITemplateFactory $templateFactory) + public function injectPrimary(Nette\DI\Container $context, Nette\Application\IPresenterFactory $presenterFactory, Nette\Application\IRouter $router, + Http\IRequest $httpRequest, Http\IResponse $httpResponse, Http\Session $session, Nette\Security\User $user, ITemplateFactory $templateFactory, + Application\IMessageStorage $messageStorage, Application\IRequestStorage $requestStorage) { if ($this->presenterFactory !== NULL) { throw new Nette\InvalidStateException("Method " . __METHOD__ . " is intended for initialization and should not be called more than once."); @@ -1319,6 +1319,8 @@ public function injectPrimary(Nette\DI\Container $context, Nette\Application\IPr $this->session = $session; $this->user = $user; $this->templateFactory = $templateFactory; + $this->messageStorage = $messageStorage; + $this->requestStorage = $requestStorage; } diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index fbb717557..eb3677b0d 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -85,9 +85,12 @@ public function createTemplate(UI\Control $control) $template->basePath = preg_replace('#https?://[^/]+#A', '', $template->baseUrl); $template->flashes = array(); - if ($presenter instanceof UI\Presenter && $presenter->hasFlashSession()) { - $id = $control->getParameterId('flash'); - $template->flashes = (array) $presenter->getFlashSession()->$id; + if ($presenter instanceof UI\Presenter) { + $flashStorage = $presenter->getFlashStorage(); + if ($flashStorage->hasMessages()) { + $id = $control->getParameterId('flash'); + $template->flashes = (array) $flashStorage->getMessages($id); + } } return $template; diff --git a/tests/Application/MessageStorage.phpt b/tests/Application/MessageStorage.phpt new file mode 100644 index 000000000..f09193fae --- /dev/null +++ b/tests/Application/MessageStorage.phpt @@ -0,0 +1,36 @@ +mockSection = new MockSessionSection; + +$messageStorage = new MessageStorage($session); + +Assert::false($messageStorage->hasMessages()); + +$messageStorage->addMessage('test', 'error'); + +$id = $messageStorage->getId(); + +// redirect with id in URL + +$session->sectionName = 'Nette.Application.Flash/' . $id; + +Assert::true($messageStorage->hasMessages()); + +$messages = $messageStorage->getMessages($id); +Assert::count(1, $messages); +Assert::equal((object) array('message' => 'test', 'type' => 'error'), $messages[0]); diff --git a/tests/Application/Presenter.link().phpt b/tests/Application/Presenter.link().phpt index 8ba91fd5c..abc8a79b5 100644 --- a/tests/Application/Presenter.link().phpt +++ b/tests/Application/Presenter.link().phpt @@ -171,7 +171,9 @@ $presenter->injectPrimary( new Http\Response, new MockSession, new MockUser, - new MockTemplateFactory + new MockTemplateFactory, + new MockMessageStorage, + new MockRequestStorage ); $presenter->invalidLinkMode = TestPresenter::INVALID_LINK_WARNING; diff --git a/tests/Application/Presenter.paramChecking.phpt b/tests/Application/Presenter.paramChecking.phpt index 4b6604c7e..4d4055338 100644 --- a/tests/Application/Presenter.paramChecking.phpt +++ b/tests/Application/Presenter.paramChecking.phpt @@ -36,7 +36,9 @@ $presenter->injectPrimary( new Http\Response, new MockSession, new MockUser, - new MockTemplateFactory + new MockTemplateFactory, + new MockMessageStorage, + new MockRequestStorage ); diff --git a/tests/Application/Presenter.storeRequest().phpt b/tests/Application/Presenter.storeRequest().phpt deleted file mode 100644 index 516a7bccb..000000000 --- a/tests/Application/Presenter.storeRequest().phpt +++ /dev/null @@ -1,136 +0,0 @@ -testSection; - } -} - -class MockSessionSection extends Nette\Object implements \ArrayAccess -{ - public $testedKeyExistence; - public $storedKey; - public $storedValue; - public $testExpiration; - public $testExpirationVariables; - - public function __isset($name) - { - $this->testedKeyExistence = $name; - return false; - } - - public function __set($name, $value) - { - $this->storedKey = $name; - $this->storedValue = $value; - } - - public function setExpiration($expiraton, $variables = NULL) - { - $this->testExpiration = $expiraton; - $this->testExpirationVariables = $variables; - } - - public function offsetExists($name) - { - return $this->__isset($name); - } - - public function offsetSet($name, $value) - { - $this->__set($name, $value); - } - - public function offsetGet($name) {} - public function offsetUnset($name) {} -} - -class MockUser extends Security\User -{ - public function __construct() {} - - public function getId() - { - return 'test_id'; - } -} - -class MockPresenterFactory extends Nette\Object implements Nette\Application\IPresenterFactory -{ - function getPresenterClass(& $name) {} - - function createPresenter($name) {} -} - -class MockRouter extends Nette\Object implements Nette\Application\IRouter -{ - function match(Nette\Http\IRequest $httpRequest) {} - - function constructUrl(Nette\Application\Request $appRequest, Nette\Http\Url $refUrl) {} -} - -class MockHttpRequest extends Http\Request -{ - public function __construct() {} -} - -class MockTemplateFactory extends Nette\Bridges\ApplicationLatte\TemplateFactory -{ - public function __construct() - {} -} - - -$presenter = new TestPresenter(); -$presenter->injectPrimary( - new DI\Container, - new MockPresenterFactory, - new MockRouter, - new MockHttpRequest, - new Http\Response, - $session = new MockSession, - $user = new MockUser, - new MockTemplateFactory -); - -$section = $session->testSection = new MockSessionSection($session); - -$applicationRequest = new Application\Request('', '', array()); -$presenter->run($applicationRequest); - -$expiration = '+1 year'; -$key = $presenter->storeRequest($expiration); - -Assert::same($expiration, $section->testExpiration); -Assert::same($key, $section->testExpirationVariables); -Assert::same($key, $section->testedKeyExistence); -Assert::same($key, $section->storedKey); -Assert::same(array($user->getId(), $applicationRequest), $section->storedValue); diff --git a/tests/Application/RequestStorage.phpt b/tests/Application/RequestStorage.phpt new file mode 100644 index 000000000..a2436f079 --- /dev/null +++ b/tests/Application/RequestStorage.phpt @@ -0,0 +1,50 @@ +mockSection = new MockSessionSection; + +$user = new MockUser; +$user->mockIdentity = new Identity(42); + +$requestStorage = new Application\RequestStorage($httpRequest, $session, $user, new MockMessageStorage); + +$applicationRequest = new Application\Request('Presenter', 'action', array('param' => 'value')); + +$key = $requestStorage->storeRequest($applicationRequest); + +// restore key + +Assert::null($requestStorage->restoreRequest('bad_key')); + +$redirect = $requestStorage->restoreRequest($key); +Assert::type('Nette\Application\Responses\RedirectResponse', $redirect); +Assert::contains($url, $redirect->getUrl()); + +// redirect to original URL + +$httpRequest = new Http\Request(new Http\UrlScript($url), array(Application\RequestStorage::REQUEST_KEY => $key)); + +$application = new Application\Application(new MockPresenterFactory, new MockRouter, $httpRequest, new MockResponse, $requestStorage); + +$applicationRequest->setFlag(Application\Request::RESTORED); +Assert::equal($applicationRequest, $application->createInitialRequest()); diff --git a/tests/Application/mocks.php b/tests/Application/mocks.php index a8b02fa44..1782f8d4a 100644 --- a/tests/Application/mocks.php +++ b/tests/Application/mocks.php @@ -3,15 +3,81 @@ class MockSession extends Nette\Http\Session { - public function __construct() + public $mockSection; + + public $sectionName; + + public function __construct() {} + + public function getSection($section, $class = 'Nette\Http\SessionSection') + { + return $this->mockSection; + } + + public function hasSection($section) + { + return $section === $this->sectionName; + } +} + + +class MockSessionSection extends Nette\Object implements \ArrayAccess +{ + public $name; + public $value; + + public function __isset($name) + { + return $name === $this->name; + } + + public function __set($name, $value) + { + $this->name = $name; + $this->value = $value; + } + + public function &__get($name) + { + return $this->value; + } + + public function setExpiration($expiraton, $variables = NULL) {} + + public function offsetExists($name) + { + return $this->__isset($name); + } + + public function offsetSet($name, $value) + { + $this->__set($name, $value); + } + + public function offsetGet($name) + { + return $this->__get($name); + } + + public function offsetUnset($name) + { + $this->__unset($name); + } } class MockUser extends Nette\Security\User { + public $mockIdentity; + public function __construct() {} + + public function getIdentity() + { + return $this->mockIdentity; + } } @@ -38,6 +104,25 @@ function constructUrl(Nette\Application\Request $appRequest, Nette\Http\Url $ref class MockHttpRequest extends Nette\Http\Request +{ + public function __construct() {} +} + + +class MockResponse extends \Nette\Http\Response +{ + public function __construct() {} +} + + +class MockMessageStorage extends Nette\Application\MessageStorage +{ + public function __construct() + {} +} + + +class MockRequestStorage extends Nette\Application\RequestStorage { public function __construct() {}