diff --git a/main/Base/AbstractCollection.class.php b/main/Base/AbstractCollection.class.php index ebd988cd8a..c9cb38b020 100644 --- a/main/Base/AbstractCollection.class.php +++ b/main/Base/AbstractCollection.class.php @@ -9,7 +9,7 @@ * * ***************************************************************************/ - abstract class AbstractCollection implements Collection + abstract class AbstractCollection implements Collection, IteratorAggregate { protected $items = array(); @@ -49,7 +49,7 @@ public function isEmpty() { return (count($this->items) == 0); } - + public function size() { return count($this->items); @@ -94,5 +94,10 @@ public function has($name) { return isset($this->items[$name]); } + + public function getIterator() + { + return new ArrayIterator($this->items); + } } ?> \ No newline at end of file diff --git a/main/Flow/HttpController.class.php b/main/Flow/HttpController.class.php new file mode 100644 index 0000000000..85b9054d39 --- /dev/null +++ b/main/Flow/HttpController.class.php @@ -0,0 +1,55 @@ + + setUrl($url)-> + setStatus(new HttpStatus($status)) + ; + } + + /** + * @return ModelAndView + **/ + protected function createNotFoundResponse() + { + return + ModelAndView::create()-> + setStatus( + new HttpStatus(HttpStatus::CODE_404) + ) + ; + } + + /** + * @return ModelAndView + **/ + protected function createForbiddenResponse() + { + return + ModelAndView::create()-> + setStatus( + new HttpStatus(HttpStatus::CODE_403) + ) + ; + } + } +?> diff --git a/main/Flow/HttpFrontController.class.php b/main/Flow/HttpFrontController.class.php new file mode 100644 index 0000000000..e1d3b6edc8 --- /dev/null +++ b/main/Flow/HttpFrontController.class.php @@ -0,0 +1,80 @@ +resolver = $resolver; + } + + public function handleRequest(HttpRequest $request) + { + $this->request = $request; + $this->response = $this->getController()->handleRequest($request); + + $this->render()->terminateRequest(); + + return $this; + } + + protected function render() + { + if (is_string($this->response->getView())) { + $this->response->setView( + $this->resolver->resolveViewName( + $this->response->getView() + ) + ); + } + + $this->response->render(); + + return $this; + } + + protected function terminateRequest() + { + if (function_exists('fastcgi_finish_request')) + fastcgi_finish_request(); + + return $this; + } + } +?> diff --git a/main/Flow/ModelAndView.class.php b/main/Flow/ModelAndView.class.php index 615eaa78bb..fde8865001 100644 --- a/main/Flow/ModelAndView.class.php +++ b/main/Flow/ModelAndView.class.php @@ -12,23 +12,52 @@ /** * @ingroup Flow **/ - class ModelAndView + class ModelAndView implements HttpResponse { - private $model = null; - - private $view = null; - + /** + * @var Model + **/ + private $model = null; + + /** + * @var View|string + **/ + private $view = null; + + /** + * @var HttpStatus + **/ + private $status = null; + + /** + * @var HttpHeaderCollection + **/ + private $headerCollection = null; + + /** + * @var CookieCollection + **/ + private $cookieCollection = null; + + /** + * @var bool + **/ + private $enabledContentLength = false; + /** * @return ModelAndView **/ public static function create() { - return new self; + return new static(); } public function __construct() { $this->model = new Model(); + $this->status = new HttpStatus(HttpStatus::CODE_200); + $this->headerCollection = new HttpHeaderCollection(); + $this->cookieCollection = new CookieCollection(); } /** @@ -68,7 +97,10 @@ public function setView($view) return $this; } - + + /** + * @deprecated + **/ public function viewIsRedirect() { return @@ -78,7 +110,10 @@ public function viewIsRedirect() && strpos($this->view, 'redirect') === 0 ); } - + + /** + * @deprecated + **/ public function viewIsNormal() { return ( @@ -86,5 +121,138 @@ public function viewIsNormal() && $this->view !== View::ERROR_VIEW ); } + + /** + * @return HttpHeaderCollection + **/ + public function getHeaderCollection() + { + return $this->headerCollection; + } + + /** + * @return CookieCollection + **/ + public function getCookieCollection() + { + return $this->cookieCollection; + } + + public function enableContentLength() + { + $this->enabledContentLength = true; + + return $this; + } + + public function disableContentLength() + { + $this->enabledContentLength = false; + + return $this; + } + + public function setStatus(HttpStatus $status) + { + $this->status = $status; + + return $this; + } + + /** + * @return HttpStatus + **/ + public function getStatus() + { + return $this->status; + } + + public function getReasonPhrase() + { + return $this->status->getName(); + } + + public function getHeaders() + { + return $this->headerCollection->getAll(); + } + + public function hasHeader($name) + { + return $this->headerCollection->has($name); + } + + public function getHeader($name) + { + return $this->headerCollection->get($name); + } + + /** + * @throws RuntimeException + **/ + public function getBody() + { + if (!$this->view) + return null; + + if (is_string($this->view)) { + throw new RuntimeException( + sprintf('View "%s" must be resolved', $this->view) + ); + } + + ob_start(); + + try { + $this->view->render($this->model); + } catch (Exception $e) { + ob_end_clean(); + + throw new RuntimeException( + 'Error while rendering view', + (int) $e->getCode(), + $e + ); + } + + return ob_get_clean(); + } + + public function render() + { + if ($this->enabledContentLength) { + $content = $this->getBody(); + $this->headerCollection->set('Content-Length', strlen($content)); + $this->sendHeaders(); + + echo $content; + } else { + Assert::isInstance($this->view, 'View'); + + $this->sendHeaders(); + $this->view->render($this->model); + } + + return $this; + } + + public function sendHeaders() + { + if (headers_sent($file, $line)) { + throw new LogicException( + sprintf('Headers are gone at %s:%d', $file, $line) + ); + } + + header($this->status->toString()); + + foreach ($this->headerCollection as $name => $valueList) + foreach ($valueList as $value) + header($name.': '.$value, true); + + $this->cookieCollection->httpSetAll(); + + return $this; + } } ?> \ No newline at end of file diff --git a/main/Net/Http/HttpHeaderCollection.class.php b/main/Net/Http/HttpHeaderCollection.class.php new file mode 100644 index 0000000000..224caa1029 --- /dev/null +++ b/main/Net/Http/HttpHeaderCollection.class.php @@ -0,0 +1,110 @@ + $value) + $this->set($name, $value); + } + + public function set($name, $value) + { + $this->headers[$this->normalizeName($name)]= + array_values((array) $value); + + return $this; + } + + public function add($name, $value) + { + $name = $this->normalizeName($name); + + if (array_key_exists($name, $this->headers)) + $this->headers[$name][] = $value; + else + $this->set($name, $value); + + return $this; + } + + public function remove($name) + { + if (!$this->has($name)) { + throw new MissingElementException( + sprintf('Header "%s" does not exist', $name) + ); + } + + unset($this->headers[$this->normalizeName($name)]); + + return $this; + } + + public function get($name) + { + $valueList = $this->getRaw($name); + + return count($valueList) > 1 ? $valueList : $valueList[0]; + } + + public function has($name) + { + return + array_key_exists( + $this->normalizeName($name), + $this->headers + ); + } + + public function getRaw($name) + { + if (!$this->has($name)) { + throw new MissingElementException( + sprintf('Header "%s" does not exist', $name) + ); + } + + return $this->headers[$this->normalizeName($name)]; + } + + public function getAll() + { + return $this->headers; + } + + public function getIterator() + { + return new ArrayIterator($this->headers); + } + + private function normalizeName($name) + { + return + preg_replace_callback( + '/(?[^-]+)/', + function ($match) { + return + strtoupper(substr($match['name'], 0, 1)) + .strtolower(substr($match['name'], 1)) + ; + }, + $name + ); + } + } +?> diff --git a/main/Net/Http/HttpResponse.class.php b/main/Net/Http/HttpResponse.class.php index 10178477e6..a9dbeac4be 100644 --- a/main/Net/Http/HttpResponse.class.php +++ b/main/Net/Http/HttpResponse.class.php @@ -26,7 +26,10 @@ public function getReasonPhrase(); public function getHeaders(); public function hasHeader($name); public function getHeader($name); - + + /** + * @throws RuntimeException + **/ public function getBody(); } ?> \ No newline at end of file diff --git a/main/Net/Http/HttpStatus.class.php b/main/Net/Http/HttpStatus.class.php index 91f4bf0ed8..fe684812d1 100644 --- a/main/Net/Http/HttpStatus.class.php +++ b/main/Net/Http/HttpStatus.class.php @@ -111,5 +111,10 @@ public function toString() { return 'HTTP/1.1 '.$this->id.' '.$this->name; } + + public function isRedirection() + { + return $this->getId() >= 300 && $this->getId() < 400; + } } ?> \ No newline at end of file diff --git a/main/Net/Http/RawResponse.class.php b/main/Net/Http/RawResponse.class.php new file mode 100644 index 0000000000..99774048d9 --- /dev/null +++ b/main/Net/Http/RawResponse.class.php @@ -0,0 +1,24 @@ +setView(new RawView($content)); + + return $this; + } + } +?> diff --git a/main/Net/Http/RedirectResponse.class.php b/main/Net/Http/RedirectResponse.class.php new file mode 100644 index 0000000000..d4a4e2603a --- /dev/null +++ b/main/Net/Http/RedirectResponse.class.php @@ -0,0 +1,38 @@ +setStatus(new HttpStatus(HttpStatus::CODE_302)); + } + + public function setUrl($url) + { + $this->getHeaderCollection()->set('Location', $url); + + return $this; + } + + public function setStatus(HttpStatus $status) + { + Assert::isTrue($status->isRedirection()); + + return parent::setStatus($status); + } + } +?> diff --git a/main/UI/View/CleanRedirectView.class.php b/main/UI/View/CleanRedirectView.class.php index 1c0fc39a88..a948e8ff0d 100644 --- a/main/UI/View/CleanRedirectView.class.php +++ b/main/UI/View/CleanRedirectView.class.php @@ -10,6 +10,7 @@ ***************************************************************************/ /** + * @deprecated use RedirectResponse instead * @ingroup Flow **/ class CleanRedirectView implements View diff --git a/main/UI/View/RawView.class.php b/main/UI/View/RawView.class.php new file mode 100644 index 0000000000..b5eded77f5 --- /dev/null +++ b/main/UI/View/RawView.class.php @@ -0,0 +1,31 @@ +content = $content; + } + + public function render($model = null) + { + echo $this->content; + + return $this; + } + } +?> diff --git a/main/UI/View/RedirectToView.class.php b/main/UI/View/RedirectToView.class.php index e255e23010..98002600cc 100644 --- a/main/UI/View/RedirectToView.class.php +++ b/main/UI/View/RedirectToView.class.php @@ -10,6 +10,7 @@ ***************************************************************************/ /** + * @deprecated * @ingroup Flow **/ final class RedirectToView extends RedirectView diff --git a/main/UI/View/RedirectView.class.php b/main/UI/View/RedirectView.class.php index 6c4b70187f..cf1e8b2d6c 100644 --- a/main/UI/View/RedirectView.class.php +++ b/main/UI/View/RedirectView.class.php @@ -10,6 +10,7 @@ ***************************************************************************/ /** + * @deprecated * @ingroup Flow **/ class RedirectView extends CleanRedirectView diff --git a/test/AllTests.php b/test/AllTests.php index 4f8e126fe4..60a482f1c4 100644 --- a/test/AllTests.php +++ b/test/AllTests.php @@ -27,6 +27,7 @@ ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Autoloader'.DIRECTORY_SEPARATOR, ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Ip'.DIRECTORY_SEPARATOR, ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Net'.DIRECTORY_SEPARATOR, + ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Net'.DIRECTORY_SEPARATOR.'Http'.DIRECTORY_SEPARATOR, ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Utils'.DIRECTORY_SEPARATOR, ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Utils'.DIRECTORY_SEPARATOR.'Routers'.DIRECTORY_SEPARATOR, ONPHP_TEST_PATH.'main'.DIRECTORY_SEPARATOR.'Utils'.DIRECTORY_SEPARATOR.'AMQP'.DIRECTORY_SEPARATOR, diff --git a/test/main/Net/Http/HttpHeaderCollectionTest.class.php b/test/main/Net/Http/HttpHeaderCollectionTest.class.php new file mode 100644 index 0000000000..6a282f66a7 --- /dev/null +++ b/test/main/Net/Http/HttpHeaderCollectionTest.class.php @@ -0,0 +1,67 @@ + 42) + ); + + return $collection; + } + + /** + * @depends testSetter + */ + public function testAddition(HttpHeaderCollection $collection) + { + $collection->add('x-foo', 'bar')->add('x-foo', 'baz'); + + return $collection; + } + + /** + * @depends testAddition + */ + public function testGetter(HttpHeaderCollection $collection) + { + $this->assertEquals(42, $collection->get('content-LeNgTh')); + $this->assertEquals(array(42), $collection->getRaw('content-LeNgTh')); + $this->assertEquals(array('bar', 'baz'), $collection->get('x-foo')); + + return $collection; + } + + /** + * @depends testGetter + */ + public function testRemoving(HttpHeaderCollection $collection) + { + $collection->remove('x-foo'); + + return $collection; + } + + /** + * @depends testRemoving + * @expectedException MissingElementException + */ + public function testFailedRemoving(HttpHeaderCollection $collection) + { + $collection->remove('x-foo'); + + return $collection; + } + } +?> diff --git a/test/main/Net/Http/RawResponseTest.class.php b/test/main/Net/Http/RawResponseTest.class.php new file mode 100644 index 0000000000..28f2eff7f3 --- /dev/null +++ b/test/main/Net/Http/RawResponseTest.class.php @@ -0,0 +1,26 @@ + + setContent('Goodbye, world!'); + $response->getHeaderCollection()->set('Content-Type', 'text/plain'); + + $this->assertEquals('text/plain', $response->getHeader('cOnTeNt-tYpE')); + $this->assertEquals('Goodbye, world!', $response->getBody()); + } + } + +?> diff --git a/test/main/Net/Http/RedirectResponseTest.class.php b/test/main/Net/Http/RedirectResponseTest.class.php new file mode 100644 index 0000000000..adf6149eb6 --- /dev/null +++ b/test/main/Net/Http/RedirectResponseTest.class.php @@ -0,0 +1,37 @@ + + setUrl('http://example.com/')-> + setStatus(new HttpStatus(HttpStatus::CODE_301)) + ; + + $this->assertEquals('http://example.com/', $response->getHeader('Location')); + } + + /** + * @expectedException WrongArgumentException + **/ + public function testInvalidStatus() + { + $response = + RedirectResponse::create()-> + setUrl('http://example.com/')-> + setStatus(new HttpStatus(HttpStatus::CODE_404)) + ; + } + } +?>