From 78804a1dc21baf4763116e17c4440501fe79a6bc Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 30 Mar 2014 20:55:15 -0400 Subject: [PATCH 01/16] Add RootController::StopWithRedirect() as a redirect shortcut. --- http/root_controller.php | 12 +++++++++++ testing/tests/http/root_controller_test.php | 23 ++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/http/root_controller.php b/http/root_controller.php index 97dd137..03e4747 100644 --- a/http/root_controller.php +++ b/http/root_controller.php @@ -145,6 +145,18 @@ class RootController $this->Stop(); } + /*! + Sets the response code to HTTP 302 FOUND and redirects the page to a new + location. + @param string The destination location of the redirect. + */ + public function StopWithRedirect($location) + { + $this->response->headers['Location'] = $location; + $this->StopWithCode(ResponseCode::FOUND); + } + + /*! Wrapper around PHP exit(). */ diff --git a/testing/tests/http/root_controller_test.php b/testing/tests/http/root_controller_test.php index 9a2d74c..bd10a93 100644 --- a/testing/tests/http/root_controller_test.php +++ b/testing/tests/http/root_controller_test.php @@ -129,7 +129,7 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $mock = $this->ConfigureMock(array('Stop'), $globals); $mock->request()->url = 'another/action'; - + $mock->expects($this->once()) ->method('Stop'); @@ -161,6 +161,27 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $mock->Stop(); } + public function testStopWithRedirect() + { + $globals = array(); + $mock = $this->ConfigureMock(array('_Exit'), $globals); + $mock->expects($this->once()) + ->method('_Exit'); + + $output_filter = $this->getMock('hoplite\http\OutputFilter', array(), array($mock)); + $output_filter->expects($this->once()) + ->method('FilterOutput') + ->with($this->isInstanceOf('hoplite\http\Request'), + $this->isInstanceOf('hoplite\http\Response')); + + $mock->set_output_filter($output_filter); + + $mock->StopWithRedirect('/foo/bar'); + + $this->assertEquals('/foo/bar', $mock->response()->headers['Location']); + $this->assertEquals(http\ResponseCode::FOUND, $mock->response()->response_code); + } + public function testAbsolutify() { $globals = array( -- 2.22.5 From dc08cff4b89d90466ebc2b3f2a8dc38b611558ab Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 31 Mar 2014 00:16:15 -0400 Subject: [PATCH 02/16] Do not call RootControllerDelegate through WeakInterface. This adds too much indirection in critical codepaths. --- http/root_controller.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/http/root_controller.php b/http/root_controller.php index 03e4747..f24b1f9 100644 --- a/http/root_controller.php +++ b/http/root_controller.php @@ -16,7 +16,6 @@ namespace hoplite\http; -require_once HOPLITE_ROOT . '/base/weak_interface.php'; require_once HOPLITE_ROOT . '/http/request.php'; require_once HOPLITE_ROOT . '/http/response.php'; require_once HOPLITE_ROOT . '/http/response_code.php'; @@ -39,7 +38,7 @@ class RootController /*! @var OutputFilter */ private $output_filter = NULL; - /*! @var WeakInterface */ + /*! @var RootControllerDelegate */ private $delegate = NULL; /*! @@ -59,7 +58,6 @@ class RootController '_COOKIE' => &$globals['_COOKIE'], '_SERVER' => &$globals['_SERVER'] ); - $this->delegate = new \hoplite\base\WeakInterface('hoplite\http\RootControllerDelegate'); } /*! Accessors */ @@ -78,7 +76,7 @@ class RootController /*! Sets the delegate. */ public function set_delegate($delegate) { - $this->delegate->Bind($delegate); + $this->delegate = $delegate; } public function delegate() { @@ -257,8 +255,7 @@ class RootController } /*! - Delegate for the root controller. The controller uses WeakInterface to call - these methods, so they're all optional. + Delegate for the root controller. All methods are optional. */ interface RootControllerDelegate { -- 2.22.5 From 9730b0f2489a9cba8950cdcb37ef65509f3ba1a0 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 31 Mar 2014 09:16:26 -0400 Subject: [PATCH 03/16] Check for the presence of a RootControllerDelegate before calling it. --- http/root_controller.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/http/root_controller.php b/http/root_controller.php index f24b1f9..75a25d7 100644 --- a/http/root_controller.php +++ b/http/root_controller.php @@ -80,7 +80,7 @@ class RootController } public function delegate() { - return $this->delegate->Get(); + return $this->delegate; } /*! @@ -113,7 +113,8 @@ class RootController // Register self as the active instance. $GLOBALS[__CLASS__] = $this; - $this->delegate->OnInitialRequest($this->request, $this->response); + if ($this->delegate) + $this->delegate->OnInitialRequest($this->request, $this->response); // Dispatch the request to an Action. $this->RouteRequest($this->request); @@ -129,7 +130,8 @@ class RootController */ public function Stop() { - $this->delegate->WillStop($this->request, $this->response); + if ($this->delegate) + $this->delegate->WillStop($this->request, $this->response); $this->output_filter->FilterOutput($this->request, $this->response); $this->_Exit(); } @@ -170,7 +172,8 @@ class RootController */ public function RouteRequest(Request $request) { - $this->delegate->WillRouteRequest($request, $this->response); + if ($this->delegate) + $this->delegate->WillRouteRequest($request, $this->response); $url_map_value = $this->url_map->Evaluate($request); @@ -193,13 +196,15 @@ class RootController */ public function InvokeAction(Action $action) { - $this->delegate->WillInvokeAction($action, $this->request, $this->response); + if ($this->delegate) + $this->delegate->WillInvokeAction($action, $this->request, $this->response); $action->FilterRequest($this->request, $this->response); $action->Invoke($this->request, $this->response); $action->FilterResponse($this->request, $this->response); - $this->delegate->DidInvokeAction($action, $this->request, $this->response); + if ($this->delegate) + $this->delegate->DidInvokeAction($action, $this->request, $this->response); } /*! -- 2.22.5 From 38d8c893ebfb727e6f70ee86a155431e8d9fd571 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 31 Mar 2014 09:18:30 -0400 Subject: [PATCH 04/16] In views\Template, do not use base/filter.php wrappers where it's not necessary. --- views/template.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/views/template.php b/views/template.php index c9ca39c..2aeb455 100644 --- a/views/template.php +++ b/views/template.php @@ -256,21 +256,20 @@ class Template if ($formatter_pos === FALSE) return 'echo hoplite\\base\\filter\\String(' . $macro . ')'; + $expression = trim(substr($macro, 0, $formatter_pos)); + // Otherwise, apply the right filter. $formatter = trim(substr($macro, $formatter_pos + 1)); $function = ''; switch (strtolower($formatter)) { - case 'int': $function = 'Int'; break; - case 'float': $function = 'Float'; break; - case 'str': $function = 'String'; break; - case 'raw': $function = 'RawString'; break; + case 'int': return "echo intval($expression)"; + case 'float': return "echo floatval($expression)"; + case 'str': return "echo hoplite\\base\\filter\\String($expression)"; + case 'raw': return "echo $expression"; + case 'json': return "echo json_encode($expression)"; default: throw new TemplateException('Invalid macro formatter "' . $formatter . '"'); } - - // Now get the expression and return a PHP statement. - $expression = trim(substr($macro, 0, $formatter_pos)); - return 'echo hoplite\\base\\filter\\' . $function . '(' . $expression . ')'; } protected function _ProcessBuiltin($macro) -- 2.22.5 From 87adeac143e78f72501a81353d91a01d18e157fa Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 25 May 2015 17:12:52 -0400 Subject: [PATCH 05/16] The default behavior for RestAction should be to error METHOD_NOT_ALLOWED. --- http/rest_action.php | 27 +++++++++++++++++++++------ testing/tests/http/fixtures.php | 4 ---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/http/rest_action.php b/http/rest_action.php index 35c5191..b3d2c5d 100644 --- a/http/rest_action.php +++ b/http/rest_action.php @@ -1,11 +1,11 @@ controller()->StopWithCode(ResponseCode::METHOD_NOT_ALLOWED); + } + + public function DoPost(Request $request, Response $response) + { + $this->controller()->StopWithCode(ResponseCode::METHOD_NOT_ALLOWED); + } + + public function DoDelete(Request $request, Response $response) + { + $this->controller()->StopWithCode(ResponseCode::METHOD_NOT_ALLOWED); + } + + public function DoPut(Request $request, Response $response) + { + $this->controller()->StopWithCode(ResponseCode::METHOD_NOT_ALLOWED); + } } diff --git a/testing/tests/http/fixtures.php b/testing/tests/http/fixtures.php index fcd3082..20dff31 100644 --- a/testing/tests/http/fixtures.php +++ b/testing/tests/http/fixtures.php @@ -26,22 +26,18 @@ class TestRestAction extends \hoplite\http\RestAction public function DoGet(http\Request $request, http\Response $response) { - parent::DoGet($request, $response); $this->did_get = TRUE; } public function DoPost(http\Request $request, http\Response $response) { - parent::DoPost($request, $response); $this->did_post = TRUE; } public function DoDelete(http\Request $request, http\Response $response) { - parent::DoDelete($request, $response); $this->did_delete = TRUE; } public function DoPut(http\Request $request, http\Response $response) { - parent::DoPut($request, $response); $this->did_put = TRUE; } } -- 2.22.5 From 2dbb698f2da9339190cc1e3d2e8bfb1f803ebc84 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 25 May 2015 17:44:48 -0400 Subject: [PATCH 06/16] Cache templates with the full template path rather than just the name. This allows multiple TemplateLoaders to operate on different template_paths but with the same template_cache. --- testing/tests/views/template_loader_test.php | 12 ++++++++---- views/template_loader.php | 13 ++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/testing/tests/views/template_loader_test.php b/testing/tests/views/template_loader_test.php index 8dfc66d..d903f46 100644 --- a/testing/tests/views/template_loader_test.php +++ b/testing/tests/views/template_loader_test.php @@ -40,16 +40,18 @@ class TemplateLoaderTest extends \PHPUnit_Framework_TestCase public function testCacheMiss() { + $file_path = sprintf($this->fixture->template_path(), 'cache_test'); + $this->cache->expects($this->once()) ->method('GetTemplateDataForName') - ->with($this->equalTo('cache_test')) + ->with($this->equalTo($file_path)) ->will($this->returnValue(NULL)); - $expected = file_get_contents(sprintf($this->fixture->template_path(), 'cache_test')); + $expected = file_get_contents($file_path); $this->cache->expects($this->once()) ->method('StoreCompiledTemplate') - ->with($this->equalTo('cache_test'), + ->with($this->equalTo($file_path), $this->greaterThan(0), $this->equalTo($expected)); @@ -59,10 +61,12 @@ class TemplateLoaderTest extends \PHPUnit_Framework_TestCase public function testCacheHit() { + $file_path = sprintf($this->fixture->template_path(), 'cache_test'); + $expected = 'Cache hit!'; $this->cache->expects($this->once()) ->method('GetTemplateDataForName') - ->with($this->equalTo('cache_test')) + ->with($this->equalTo($file_path)) ->will($this->returnValue($expected)); // The cache backend is only consulted once. diff --git a/views/template_loader.php b/views/template_loader.php index 84d461e..4a08b4a 100644 --- a/views/template_loader.php +++ b/views/template_loader.php @@ -114,20 +114,23 @@ class TemplateLoader if (!$this->cache_backend) return; - $fetch_templates = array(); + $fetch_templates = []; + $paths_to_names = []; foreach ($templates AS $name) { // Do not re-cache templates that have already been cached. if (isset($this->cache[$name])) continue; $tpl_path = $this->_TemplatePath($name); - $fetch_templates[$name] = filemtime($tpl_path); + $fetch_templates[$tpl_path] = filemtime($tpl_path); + $paths_to_names[$tpl_path] = $name; } $profile = Profiling::IsProfilingEnabled(); $cached_templates = $this->cache_backend->GetMultipleTemplates($fetch_templates); - foreach ($cached_templates AS $name => $data) { + foreach ($cached_templates AS $tpl_path => $data) { + $name = $paths_to_names[$tpl_path]; $this->cache[$name] = Template::NewWithCompiledData($name, $data); if ($profile) $this->usage[$name] = 0; @@ -162,7 +165,7 @@ class TemplateLoader if (!$this->cache_backend) return NULL; - $data = $this->cache_backend->GetTemplateDataForName($name, filemtime($tpl_path)); + $data = $this->cache_backend->GetTemplateDataForName($tpl_path, filemtime($tpl_path)); if (!$data) return NULL; @@ -189,7 +192,7 @@ class TemplateLoader if ($this->cache_backend) { $this->cache_backend->StoreCompiledTemplate( - $name, filemtime($tpl_path), $template->template()); + $tpl_path, filemtime($tpl_path), $template->template()); } return $template; -- 2.22.5 From 31600eeb381d24a83267ad4d892df3692ed0faea Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 25 May 2015 22:24:31 -0400 Subject: [PATCH 07/16] Use filter_var() over base/filter.php, which is going away soon. --- data/profiling_pdo.php | 3 +-- views/template.php | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/data/profiling_pdo.php b/data/profiling_pdo.php index 4b1a9d2..cd055b8 100644 --- a/data/profiling_pdo.php +++ b/data/profiling_pdo.php @@ -18,7 +18,6 @@ namespace hoplite\data; use \hoplite\base\Profiling; -require_once HOPLITE_ROOT . '/base/filter.php'; require_once HOPLITE_ROOT . '/base/profiling.php'; /*! @@ -118,7 +117,7 @@ class ProfilingPDO extends \PDO $debug .= "\n\t\t\t$query[query]\n\n"; if (isset($query['params'])) { $debug .= "\t\t\t
    \n\t\t\t\t
  1. "; - $debug .= implode("
  2. \n\t\t\t\t
  3. ", \hoplite\base\filter\String($query['params'])); + $debug .= implode("
  4. \n\t\t\t\t
  5. ", filter_var_array($query['params'], FILTER_SANITIZE_SPECIAL_CHARS)); $debug .= "
  6. \n\t\t\t
\n"; } $debug .= "\n\t\t\t
(" . diff --git a/views/template.php b/views/template.php index 2aeb455..da6857d 100644 --- a/views/template.php +++ b/views/template.php @@ -18,7 +18,6 @@ namespace hoplite\views; use \hoplite\base\Profiling; -require_once HOPLITE_ROOT . '/base/filter.php'; require_once HOPLITE_ROOT . '/base/profiling.php'; /*! @@ -254,7 +253,7 @@ class Template // No specifier defaults to escaped string. if ($formatter_pos === FALSE) - return 'echo hoplite\\base\\filter\\String(' . $macro . ')'; + return 'echo filter_var(' . $macro . ', FILTER_SANITIZE_STRING)'; $expression = trim(substr($macro, 0, $formatter_pos)); @@ -264,7 +263,7 @@ class Template switch (strtolower($formatter)) { case 'int': return "echo intval($expression)"; case 'float': return "echo floatval($expression)"; - case 'str': return "echo hoplite\\base\\filter\\String($expression)"; + case 'str': return "echo filter_var($expression, FILTER_SANITIZE_STRING)"; case 'raw': return "echo $expression"; case 'json': return "echo json_encode($expression)"; default: -- 2.22.5 From ac42824b920b4073ff7c1823779f10a10832edc9 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Wed, 27 May 2015 00:22:54 -0400 Subject: [PATCH 08/16] Add Request::Filter and ::FilterArray. --- http/request.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/http/request.php b/http/request.php index ff48b00..031518b 100644 --- a/http/request.php +++ b/http/request.php @@ -40,4 +40,24 @@ class Request extends \hoplite\base\StrictObject { $this->url = $url; } + + /*! + Wrapper around filter_input() that stores the result in the ::$data field. + */ + public function Filter($type, $name, $filter=FILTER_SANITIZE_STRING, $options=NULL) + { + $rv = filter_input($type, $name, $filter, $options); + $this->data[$name] = $rv; + return $rv; + } + + /*! + Wrapper around filter_input() that merges the result in the ::$data field. + */ + public function FilterArray($type, $definition, $add_empty=TRUE) + { + $rv = filter_input_array($type, $definition, $add_empty); + $this->data = array_merge($this->data, $rv); + return $rv; + } } -- 2.22.5 From 09c49802e246f2fe7e326d2afe3dfb832f43738c Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 31 May 2015 16:23:31 -0400 Subject: [PATCH 09/16] Introduce FrontController as the replacement for RootController. The FrontController has no delegate, but instead uses Interceptors to allow middleware to further process the request or to interrupt control flow. One integral change is that if no Action is found for the Request in the UrlMap, then no action will be taken by the FrontController. Instead, a NotFoundInterceptor should be used to send a HTTP 404 response in this event. --- data/controller.php | 4 +- http/action.php | 22 +-- http/action_controller.php | 3 +- ...ot_controller.php => front_controller.php} | 107 ++++-------- http/interceptor.php | 35 ++++ http/not_found_interceptor.php | 33 ++++ http/output_filter.php | 10 +- http/rest_adapter.php | 3 +- http/url_map.php | 12 +- testing/tests/http/action_controller_test.php | 6 +- ...ler_test.php => front_controller_test.php} | 157 +++++++++++------- testing/tests/http/output_filter_test.php | 4 +- testing/tests/http/rest_action_test.php | 10 +- testing/tests/http/rest_adapter_test.php | 28 ++-- testing/tests/http/url_map_test.php | 2 +- 15 files changed, 246 insertions(+), 190 deletions(-) rename http/{root_controller.php => front_controller.php} (69%) create mode 100644 http/interceptor.php create mode 100644 http/not_found_interceptor.php rename testing/tests/http/{root_controller_test.php => front_controller_test.php} (65%) diff --git a/data/controller.php b/data/controller.php index 79c338b..969c4cf 100644 --- a/data/controller.php +++ b/data/controller.php @@ -34,11 +34,13 @@ abstract class Controller extends http\RestAction protected $model = NULL; /*! Selects the Model object. */ - public function FilterRequest(http\Request $request, http\Response $response) + public function Invoke(http\Request $request, http\Response $response) { $this->model = $this->_SelectModel(); $this->model->SetFrom(array_merge( $request->data, $request->data['_POST'], $request->data['_GET'])); + + parent::Invoke($request, $response); } /*! Returns a new instance of the Model that this object will control. */ diff --git a/http/action.php b/http/action.php index 46f1859..0d8dd63 100644 --- a/http/action.php +++ b/http/action.php @@ -1,11 +1,11 @@ controller = $controller; } - /*! Accesses the RootController */ + /*! Accesses the FrontController */ public function controller() { return $this->controller; } - /*! - Called before the Action is Invoked(). - */ - public function FilterRequest(Request $request, Response $response) {} - /*! Performs the action and fills out the response's data model. */ public abstract function Invoke(Request $request, Response $response); - - /*! - Called after this has been Invoked(). - */ - public function FilterResponse(Request $request, Response $response) {} } diff --git a/http/action_controller.php b/http/action_controller.php index 3da7039..25a4b34 100644 --- a/http/action_controller.php +++ b/http/action_controller.php @@ -34,8 +34,7 @@ class ActionController extends Action { $method = $this->_GetActionMethod($request); if (!method_exists($this, $method)) { - $response->response_code = ResponseCode::NOT_FOUND; - $this->controller()->Stop(); + $this->controller()->SendResponseCode(ResponseCode::NOT_FOUND); return; } diff --git a/http/root_controller.php b/http/front_controller.php similarity index 69% rename from http/root_controller.php rename to http/front_controller.php index 75a25d7..b84f298 100644 --- a/http/root_controller.php +++ b/http/front_controller.php @@ -1,11 +1,11 @@ */ + private $interceptors = []; /*! Creates the controller with the request context information, typicallhy from the global scope ($GLOBALS), but can be injected for testing. - @param UrlMap The routing map - @param OutputFilter The object responsible for decorating output. - @param array& PHP globals array + @param array PHP globals array */ public function __construct(array $globals) { @@ -73,21 +72,18 @@ class RootController $this->output_filter = $output_filter; } - /*! Sets the delegate. */ - public function set_delegate($delegate) - { - $this->delegate = $delegate; - } - public function delegate() + /*! Registers an Interceptor that will be run before executing an Action. */ + public function AddInterceptor(Interceptor $interceptor) { - return $this->delegate; + $this->interceptors[] = $interceptor; } /*! - Createst the Request and Response that are used throughout the duration of - the execution. - */ - public function Run() + Called in index.php to process the current HTTP request. This initializes the + Request object from its data and then routes the request to generate the + response. + */ + public function ProcessRequest() { // The query rewriter module of the webserver rewrites a request from: // http://example.com/webapp/user/view/42 @@ -113,25 +109,20 @@ class RootController // Register self as the active instance. $GLOBALS[__CLASS__] = $this; - if ($this->delegate) - $this->delegate->OnInitialRequest($this->request, $this->response); - // Dispatch the request to an Action. $this->RouteRequest($this->request); // When control returns here, all actions have been invoked and it's time // to start the output filter and exit. - $this->Stop(); + $this->SendResponse(); } /*! Prevents any other Actions from executing. This starts the OutputFilter and then exits. */ - public function Stop() + public function SendResponse() { - if ($this->delegate) - $this->delegate->WillStop($this->request, $this->response); $this->output_filter->FilterOutput($this->request, $this->response); $this->_Exit(); } @@ -139,10 +130,10 @@ class RootController /*! Sets the response code and stops the controller. Returns void. */ - public function StopWithCode($code) + public function SendResponseCode($code) { $this->response->response_code = $code; - $this->Stop(); + $this->SendResponse(); } /*! @@ -150,13 +141,12 @@ class RootController location. @param string The destination location of the redirect. */ - public function StopWithRedirect($location) + public function SendResponseRedirect($location) { $this->response->headers['Location'] = $location; - $this->StopWithCode(ResponseCode::FOUND); + $this->SendResponseCode(ResponseCode::FOUND); } - /*! Wrapper around PHP exit(). */ @@ -166,45 +156,24 @@ class RootController } /*! - Invoked by Run() and can be invoked by others to evaluate and perform the - lookup in the UrlMap. This then calls InvokeAction(). + Given an Request object, this executes the action for the corresponding URL. + The action is located by performing a lookup in the UrlMap. Interceptors + are run before invoking the action. @param Request The Request whose URL will be routed */ public function RouteRequest(Request $request) { - if ($this->delegate) - $this->delegate->WillRouteRequest($request, $this->response); - $url_map_value = $this->url_map->Evaluate($request); $action = NULL; if ($url_map_value) $action = $this->url_map->LookupAction($url_map_value); - if (!$action) { - $this->response->response_code = ResponseCode::NOT_FOUND; - $this->Stop(); - return; - } + foreach ($this->interceptors as $interceptor) + $interceptor->DoIntercept($this, $action, $request, $this->response); - $this->InvokeAction($action); - } - - /*! - Used to run an Action and drive it through its states. - @param Action - */ - public function InvokeAction(Action $action) - { - if ($this->delegate) - $this->delegate->WillInvokeAction($action, $this->request, $this->response); - - $action->FilterRequest($this->request, $this->response); - $action->Invoke($this->request, $this->response); - $action->FilterResponse($this->request, $this->response); - - if ($this->delegate) - $this->delegate->DidInvokeAction($action, $this->request, $this->response); + if ($action) + $action->Invoke($request, $this->response); } /*! @@ -258,19 +227,3 @@ class RootController return $url . $new_path; } } - -/*! - Delegate for the root controller. All methods are optional. -*/ -interface RootControllerDelegate -{ - public function OnInitialRequest(Request $request, Response $response); - - public function WillRouteRequest(Request $request, Response $response); - - public function WillInvokeAction(Action $action, Request $request, Response $response); - - public function DidInvokeAction(Action $action, Request $request, Response $response); - - public function WillStop(Request $request, Response $response); -} diff --git a/http/interceptor.php b/http/interceptor.php new file mode 100644 index 0000000..15bf0ce --- /dev/null +++ b/http/interceptor.php @@ -0,0 +1,35 @@ +. + +namespace hoplite\http; + +/*! + An Interceptor runs as part of FrontController::RouteRequest() to provide + extra processing before the Action is Invoke()ed. The Action is provided to + allow the Interceptor to change the control flow based on characteristics + of the Action processor. An Interceptor may interrupt the control flow by + calling SendResponse() on the controller. +*/ +interface Interceptor +{ + /*! + Performs the action and fills out the response's data model. + */ + public function DoIntercept(FrontController $controller, + Action $action = NULL, + Request $request, + Response $response); +} diff --git a/http/not_found_interceptor.php b/http/not_found_interceptor.php new file mode 100644 index 0000000..a49fd41 --- /dev/null +++ b/http/not_found_interceptor.php @@ -0,0 +1,33 @@ +. + +namespace hoplite\http; + +require_once HOPLITE_ROOT . '/http/interceptor.php'; +require_once HOPLITE_ROOT . '/http/response_code.php'; + +class NotFoundInterceptor implements Interceptor +{ + public function DoIntercept(FrontController $controller, + Action $action, + Request $request, + Response $response) + { + if ($action === NULL) { + $controller->SendResponseCode(ResponseCode::NOT_FOUND); + } + } +} diff --git a/http/output_filter.php b/http/output_filter.php index 1e1925a..1ae998f 100644 --- a/http/output_filter.php +++ b/http/output_filter.php @@ -29,7 +29,7 @@ require_once HOPLITE_ROOT . '/views/template_loader.php'; */ class OutputFilter { - /*! @var RootController */ + /*! @var FrontController */ private $controller; /*! @var WeakInterface */ @@ -52,15 +52,15 @@ class OutputFilter const RENDER_TEMPLATE = 'template'; /*! - Constructor that takes a reference to the RootController. + Constructor that takes a reference to the FrontController. */ - public function __construct(RootController $controller) + public function __construct(FrontController $controller) { $this->controller = $controller; $this->delegate = new \hoplite\base\WeakInterface('hoplite\http\OutputFilterDelegate'); } - /*! Accessor for the RootController. */ + /*! Accessor for the FrontController. */ public function controller() { return $this->controller; } /*! Accessors for the delegate. */ @@ -71,7 +71,7 @@ class OutputFilter public function delegate() { return $this->delegate->Get(); } /*! @brief Main entry point for output filtering - This is called from the RootController to begin processing the output and + This is called from the FrontController to begin processing the output and generating the response. */ public function FilterOutput(Request $request, Response $response) diff --git a/http/rest_adapter.php b/http/rest_adapter.php index 0329177..aeec994 100644 --- a/http/rest_adapter.php +++ b/http/rest_adapter.php @@ -29,9 +29,10 @@ abstract class RestAdapter extends ActionController /*! @var RestAction The RESTful interface which will be adapted. */ protected $action = NULL; - public function FilterRequest(Request $request, Response $response) + public function Invoke(Request $request, Response $response) { $this->action = $this->_GetRestAction(); + parent::Invoke($request, $response); } /*! Gets the RestAction that will be adapted. */ diff --git a/http/url_map.php b/http/url_map.php index bc40a67..8bf450a 100644 --- a/http/url_map.php +++ b/http/url_map.php @@ -25,7 +25,7 @@ require_once HOPLITE_ROOT . '/base/functions.php'; */ class UrlMap { - /*! @var RootController */ + /*! @var FrontController */ private $controller; /*! @var array The map of URLs to actions. */ @@ -35,15 +35,15 @@ class UrlMap private $file_loader = NULL; /*! - Constructs the object with a reference to the RootController. - @param RootController + Constructs the object with a reference to the FrontController. + @param FrontController */ - public function __construct(RootController $controller) + public function __construct(FrontController $controller) { $this->controller = $controller; } - /*! Accessor for the RootController */ + /*! Accessor for the FrontController */ public function controller() { return $this->controller; } /*! Gets the URL map */ @@ -55,7 +55,7 @@ class UrlMap The keys can be either a URL prefix or a regular expression. For URL prefixes, the pattern is matched relative to the root of the entry- - point as specified by the RootController. Patterns should not begin with a + point as specified by the FrontController. Patterns should not begin with a slash. Path fragment parameter extraction can be performed as well. For example, the pattern 'user/view/{id}' will match a URL like http://example.com/webapp/user/view/42 diff --git a/testing/tests/http/action_controller_test.php b/testing/tests/http/action_controller_test.php index 5781b97..6e74c12 100644 --- a/testing/tests/http/action_controller_test.php +++ b/testing/tests/http/action_controller_test.php @@ -34,7 +34,7 @@ class ActionControllerTest extends \PHPUnit_Framework_TestCase public function setUp() { $globals = array(); - $this->fixture = new TestActionController(new http\RootController($globals)); + $this->fixture = new TestActionController(new http\FrontController($globals)); $this->request = new http\Request(); $this->response = new http\Response(); } @@ -49,11 +49,11 @@ class ActionControllerTest extends \PHPUnit_Framework_TestCase public function testFailedDispatch() { $globals = array(); - $mock = $this->getMock('hoplite\http\RootController', array(), array($globals)); + $mock = $this->getMock('hoplite\http\FrontController', array('SendResponse'), array($globals)); $this->fixture = new TestActionController($mock); $mock->expects($this->once()) - ->method('Stop'); + ->method('SendResponse'); $this->request->data['action'] = 'nothing'; $this->fixture->Invoke($this->request, $this->response); diff --git a/testing/tests/http/root_controller_test.php b/testing/tests/http/front_controller_test.php similarity index 65% rename from testing/tests/http/root_controller_test.php rename to testing/tests/http/front_controller_test.php index bd10a93..e3a86bd 100644 --- a/testing/tests/http/root_controller_test.php +++ b/testing/tests/http/front_controller_test.php @@ -1,11 +1,11 @@ did_filter_request = TRUE; + $this->did_invoke = TRUE; } +} - public function Invoke(http\Request $q, http\Response $s) +class ClosureInterceptor implements http\Interceptor +{ + private $interceptor; + public $did_intercept = FALSE; + + public function __construct($interceptor) { - $this->did_invoke = TRUE; + $this->interceptor = $interceptor; } - public function FilterResponse(http\Request $q, http\Response $s) + public function DoIntercept(http\FrontController $controller, + http\Action $action = NULL, + http\Request $request, + http\Response $response) { - $this->did_filter_response = TRUE; + $this->did_intercept = TRUE; + if ($this->interceptor) { + $interceptor = $this->interceptor; + $interceptor($controller, $action, $request, $response); + } } } -class RootControllerTest extends \PHPUnit_Framework_TestCase +class FrontControllerTest extends \PHPUnit_Framework_TestCase { /*! - Configures a mock RootControler. + Configures a mock FrontControler. @param array|NULL Array of methods to mock @param varargs Constructor parameters. - @return Mock RootControler + @return Mock FrontControler */ public function ConfigureMock() { $args = func_get_args(); - return $this->getMock('hoplite\http\RootController', $args[0], array_slice($args, 1)); + return $this->getMock('hoplite\http\FrontController', $args[0], array_slice($args, 1)); } - public function testRun() + public function testProcessRequest() { $globals = array('_SERVER' => array( 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/some/action/42' )); - $mock = $this->ConfigureMock(array('RouteRequest', 'Stop'), $globals); + $mock = $this->ConfigureMock(array('RouteRequest', 'SendResponse'), $globals); $mock->request()->url = 'some/action/42'; @@ -73,42 +85,20 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase ->with($this->equalTo($mock->request())); $mock->expects($this->once()) - ->method('Stop'); - - $mock->Run(); - } - - public function testInvokeAction() - { - $globals = array(); - $fixture = new http\RootController($globals); - - $action = new ActionReporter($fixture); + ->method('SendResponse'); - $this->assertFalse($action->did_filter_request); - $this->assertFalse($action->did_invoke); - $this->assertFalse($action->did_filter_response); - - $fixture->InvokeAction($action); - - $this->assertTrue($action->did_filter_request); - $this->assertTrue($action->did_invoke); - $this->assertTrue($action->did_filter_response); + $mock->ProcessRequest(); } public function testRouteRequest() { $globals = array(); - $mock = $this->ConfigureMock(array('Stop', 'InvokeAction'), $globals); + $mock = $this->ConfigureMock(array('SendResponse', 'InvokeAction'), $globals); $mock->request()->url = 'some/action/42'; $map_value = 'ActionReporter'; $action = new ActionReporter($mock); - $mock->expects($this->once()) - ->method('InvokeAction') - ->with($this->isInstanceOf('hoplite\test\ActionReporter')); - $url_map = $this->getMock('hoplite\http\UrlMap', array(), array($mock)); $url_map->expects($this->once()) ->method('Evaluate') @@ -121,17 +111,19 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $mock->set_url_map($url_map); $mock->RouteRequest($mock->request()); + + $this->assertTrue($action->did_invoke); } public function testRouteRequestInvalid() { $globals = array(); - $mock = $this->ConfigureMock(array('Stop'), $globals); + $mock = $this->ConfigureMock(array('SendResponse'), $globals); $mock->request()->url = 'another/action'; - $mock->expects($this->once()) - ->method('Stop'); + $mock->expects($this->never()) + ->method('SendResponse'); $url_map = $this->getMock('hoplite\http\UrlMap', array(), array($mock)); $url_map->expects($this->once()) @@ -140,10 +132,10 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $mock->set_url_map($url_map); $mock->RouteRequest($mock->request()); - $this->assertEquals(http\ResponseCode::NOT_FOUND, $mock->response()->response_code); + // Nothing should happen for a non-routed request. } - public function testStop() + public function testSendResponse() { $globals = array(); $mock = $this->ConfigureMock(array('_Exit'), $globals); @@ -158,7 +150,7 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $this->isInstanceOf('hoplite\http\Response')); $mock->set_output_filter($output_filter); - $mock->Stop(); + $mock->SendResponse(); } public function testStopWithRedirect() @@ -176,7 +168,7 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $mock->set_output_filter($output_filter); - $mock->StopWithRedirect('/foo/bar'); + $mock->SendResponseRedirect('/foo/bar'); $this->assertEquals('/foo/bar', $mock->response()->headers['Location']); $this->assertEquals(http\ResponseCode::FOUND, $mock->response()->response_code); @@ -192,7 +184,7 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase 'SERVER_PORT' => 80, ), ); - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/'), '/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/', TRUE), 'http://www.bluestatic.org/hoplite/webapp/'); @@ -202,7 +194,7 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $globals['_SERVER']['HTTPS'] = 'on'; $globals['_SERVER']['SERVER_PORT'] = 443; - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/'), '/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/', TRUE), 'https://www.bluestatic.org/hoplite/webapp/'); @@ -210,13 +202,13 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($mock->MakeURL('/path/3', TRUE), 'https://www.bluestatic.org/hoplite/webapp/path/3'); $globals['_SERVER']['SERVER_PORT'] = 8080; - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/path/2'), '/hoplite/webapp/path/2'); $this->assertEquals($mock->MakeURL('/', TRUE), 'https://www.bluestatic.org:8080/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/path/3', TRUE), 'https://www.bluestatic.org:8080/hoplite/webapp/path/3'); } - public function testAbsolutifyRoot() + public function testAbsolutifyFront() { $globals = array( '_SERVER' => array( @@ -226,14 +218,14 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase 'SERVER_PORT' => 80, ), ); - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/'), '/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/', TRUE), 'http://www.bluestatic.org/hoplite/webapp/'); $globals['_SERVER']['HTTPS'] = 'on'; $globals['_SERVER']['SERVER_PORT'] = 443; - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/'), '/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/', TRUE), 'https://www.bluestatic.org/hoplite/webapp/'); @@ -241,9 +233,60 @@ class RootControllerTest extends \PHPUnit_Framework_TestCase $this->assertEquals($mock->MakeURL('/path/3', TRUE), 'https://www.bluestatic.org/hoplite/webapp/path/3'); $globals['_SERVER']['SERVER_PORT'] = 8080; - $mock = new \hoplite\http\RootController($globals); + $mock = new \hoplite\http\FrontController($globals); $this->assertEquals($mock->MakeURL('/path/2'), '/hoplite/webapp/path/2'); $this->assertEquals($mock->MakeURL('/', TRUE), 'https://www.bluestatic.org:8080/hoplite/webapp/'); $this->assertEquals($mock->MakeURL('/path/3', TRUE), 'https://www.bluestatic.org:8080/hoplite/webapp/path/3'); } + + public function testThreeInterceptors() + { + $interceptors = [ + new ClosureInterceptor(NULL), + new ClosureInterceptor(NULL), + new ClosureInterceptor(NULL), + ]; + + $mock = new \hoplite\http\FrontController([]); + $mock->set_url_map(new http\UrlMap($mock)); + + foreach ($interceptors as $interceptor) { + $mock->AddInterceptor($interceptor); + } + + $mock->RouteRequest($mock->request()); + + foreach ($interceptors as $interceptor) { + $this->assertTrue($interceptor->did_intercept); + } + } + + public function testInterruptInterceptor() + { + $i1 = new ClosureInterceptor(NULL); + $i2 = new ClosureInterceptor(function($controller, $action, $request, $response) { + $controller->SendResponseCode(400); + }); + $test = $this; + $i3 = new ClosureInterceptor(function($controller, $action, $request, $response) use ($test) { + $test->assertEquals(400, $response->response_code); + }); + + $mock = $this->ConfigureMock(['SendResponse'], []); + $mock->set_url_map(new http\UrlMap($mock)); + $mock->AddInterceptor($i1); + $mock->AddInterceptor($i2); + $mock->AddInterceptor($i3); + + $mock->expects($this->once()) + ->method('SendResponse'); + + $mock->RouteRequest($mock->request()); + + $this->assertEquals(400, $mock->response()->response_code); + $this->assertTrue($i1->did_intercept); + $this->assertTrue($i2->did_intercept); + // i3 would not normally run but because _Exit is mocked, the script does + // not finish. + } } diff --git a/testing/tests/http/output_filter_test.php b/testing/tests/http/output_filter_test.php index b101e24..dba9e74 100644 --- a/testing/tests/http/output_filter_test.php +++ b/testing/tests/http/output_filter_test.php @@ -31,7 +31,7 @@ class OutputFilterTest extends \PHPUnit_Framework_TestCase { public function setUp() { - $this->fixture = new TestOutputFilter(new http\RootController(array())); + $this->fixture = new TestOutputFilter(new http\FrontController(array())); } public function testEncodeXML() @@ -50,7 +50,7 @@ class OutputFilterTest extends \PHPUnit_Framework_TestCase 1bar<strong>baz</strong>moobaa XML; - + $this->assertEquals($expected, $this->fixture->T_EncodeXML($array)); $obj = new \stdClass(); diff --git a/testing/tests/http/rest_action_test.php b/testing/tests/http/rest_action_test.php index fb6c36b..afae0db 100644 --- a/testing/tests/http/rest_action_test.php +++ b/testing/tests/http/rest_action_test.php @@ -1,11 +1,11 @@ fixture = new TestRestAction(new http\RootController($globals)); + $this->fixture = new TestRestAction(new http\FrontController($globals)); $this->request = new http\Request(); $this->response = new http\Response(); } @@ -72,7 +72,7 @@ class RestActionTest extends \PHPUnit_Framework_TestCase public function testInvalid() { $globals = array(); - $mock = $this->getMock('hoplite\http\RootController', array('Stop'), array($globals)); + $mock = $this->getMock('hoplite\http\FrontController', array('Stop'), array($globals)); $this->fixture = new TestRestAction($mock); diff --git a/testing/tests/http/rest_adapter_test.php b/testing/tests/http/rest_adapter_test.php index 629d52a..7a4d01b 100644 --- a/testing/tests/http/rest_adapter_test.php +++ b/testing/tests/http/rest_adapter_test.php @@ -39,7 +39,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase public function setUp() { $globals = array(); - $this->controller = new http\RootController($globals); + $this->controller = new http\FrontController($globals); $this->fixture = new TestRestAdapter($this->controller); $this->request = $this->controller->request(); $this->response = $this->controller->response(); @@ -59,7 +59,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'GET'; $this->request->data['action'] = 'fetch'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue('did_get'); } @@ -67,7 +67,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'POST'; $this->request->data['action'] = 'fetch'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue('did_get'); } @@ -75,7 +75,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'PUT'; $this->request->data['action'] = 'fetch'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue(NULL); $this->assertEquals(http\ResponseCode::METHOD_NOT_ALLOWED, $this->response->response_code); } @@ -84,7 +84,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'POST'; $this->request->data['action'] = 'update'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue('did_post'); } @@ -92,7 +92,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'GET'; $this->request->data['action'] = 'update'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue(NULL); $this->assertEquals(http\ResponseCode::METHOD_NOT_ALLOWED, $this->response->response_code); } @@ -101,7 +101,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'POST'; $this->request->data['action'] = 'delete'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue('did_delete'); } @@ -109,7 +109,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'GET'; $this->request->data['action'] = 'delete'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue(NULL); $this->assertEquals(http\ResponseCode::METHOD_NOT_ALLOWED, $this->response->response_code); } @@ -118,7 +118,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'POST'; $this->request->data['action'] = 'insert'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue('did_put'); } @@ -126,7 +126,7 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase { $this->request->http_method = 'GET'; $this->request->data['action'] = 'insert'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($this->request, $this->response); $this->RestExpectSingleTrue(NULL); $this->assertEquals(http\ResponseCode::METHOD_NOT_ALLOWED, $this->response->response_code); } @@ -134,17 +134,17 @@ class RestAdapterTest extends \PHPUnit_Framework_TestCase public function testInvalid() { $globals = array(); - $mock = $this->getMock('hoplite\http\RootController', array('Stop'), array($globals)); + $mock = $this->getMock('hoplite\http\FrontController', array('SendResponse'), array($globals)); $this->fixture = new TestRestAdapter($mock); $mock->expects($this->once()) - ->method('Stop'); + ->method('SendResponse'); $this->request->http_method = 'HEAD'; - $this->controller->InvokeAction($this->fixture); + $this->fixture->Invoke($mock->request(), $mock->response()); $this->RestExpectSingleTrue('___none___'); - $this->assertEquals(http\ResponseCode::NOT_FOUND, $this->response->response_code); + $this->assertEquals(http\ResponseCode::NOT_FOUND, $mock->response()->response_code); } } diff --git a/testing/tests/http/url_map_test.php b/testing/tests/http/url_map_test.php index 72fbaf2..01fd813 100644 --- a/testing/tests/http/url_map_test.php +++ b/testing/tests/http/url_map_test.php @@ -29,7 +29,7 @@ class UrlMapTest extends \PHPUnit_Framework_TestCase public function setUp() { $globals = array(); - $this->fixture = new http\UrlMap(new http\RootController($globals)); + $this->fixture = new http\UrlMap(new http\FrontController($globals)); } public function testSimpleEvaluate() -- 2.22.5 From ba8a84f9e7b94fe006209a901293d900851ca6a9 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 31 May 2015 17:20:28 -0400 Subject: [PATCH 10/16] Fix views\Template accessing RootController rather than FrontController. --- views/template.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/views/template.php b/views/template.php index da6857d..3b4b9c9 100644 --- a/views/template.php +++ b/views/template.php @@ -328,14 +328,14 @@ class TemplateBuiltins echo TemplateLoader::Fetch($template)->Render($vars); } - /*! @brief Creates a URL via RootController::MakeURL(). + /*! @brief Creates a URL via FrontController::MakeURL(). This requires the root controller be set in the $GLOBALS as - hoplite\http\RootController. + hoplite\http\FrontController. @param string Path. */ static public function MakeURL($path) { - echo $GLOBALS['hoplite\http\RootController']->MakeURL($path, FALSE); + echo $GLOBALS['hoplite\http\FrontController']->MakeURL($path, FALSE); } } -- 2.22.5 From dde46188cf710ef8aa053f580238dbbe20a8f42e Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 31 May 2015 17:20:55 -0400 Subject: [PATCH 11/16] Restore FrontController::InvokeAction(). --- http/front_controller.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/http/front_controller.php b/http/front_controller.php index b84f298..d31f1ff 100644 --- a/http/front_controller.php +++ b/http/front_controller.php @@ -173,7 +173,15 @@ class FrontController $interceptor->DoIntercept($this, $action, $request, $this->response); if ($action) - $action->Invoke($request, $this->response); + $this->InvokeAction($action); + } + + /*! + Invokes the action with the Controller's request and response objects. + */ + public function InvokeAction(Action $action) + { + $action->Invoke($this->request, $this->response); } /*! -- 2.22.5 From db920ccbd814f3068a7447f2f28673d437007bd4 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sun, 31 May 2015 17:26:17 -0400 Subject: [PATCH 12/16] NotFoundInterceptor needs to permit a NULL Action. --- http/not_found_interceptor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http/not_found_interceptor.php b/http/not_found_interceptor.php index a49fd41..31f0fa4 100644 --- a/http/not_found_interceptor.php +++ b/http/not_found_interceptor.php @@ -22,7 +22,7 @@ require_once HOPLITE_ROOT . '/http/response_code.php'; class NotFoundInterceptor implements Interceptor { public function DoIntercept(FrontController $controller, - Action $action, + Action $action = NULL, Request $request, Response $response) { -- 2.22.5 From 4468833c46108f3ca451796a6a5f6824e6abb021 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Mon, 28 Nov 2016 02:13:47 -0500 Subject: [PATCH 13/16] Initial implementation of a new HTTP library based on pipelines. A Pipeline is a series of chained Middleware that execute in order to produce a response. The goal is for the http2 subsystem to implement the same concepts as http, but with looser coupling between the controllers and output filters. --- http2/api.php | 61 +++++++++++++++++++++++++++++++++ http2/middleware.php | 62 ++++++++++++++++++++++++++++++++++ http2/pipeline.php | 80 ++++++++++++++++++++++++++++++++++++++++++++ http2/request.php | 64 +++++++++++++++++++++++++++++++++++ http2/response.php | 66 ++++++++++++++++++++++++++++++++++++ 5 files changed, 333 insertions(+) create mode 100644 http2/api.php create mode 100644 http2/middleware.php create mode 100644 http2/pipeline.php create mode 100644 http2/request.php create mode 100644 http2/response.php diff --git a/http2/api.php b/http2/api.php new file mode 100644 index 0000000..0a901ac --- /dev/null +++ b/http2/api.php @@ -0,0 +1,61 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class DataResponse extends Response { + public $data; + + public function __construct($data) { + parent::__construct(); + $this->data = $data; + } + + public function generate() { + throw new \Exception(__CLASS__ . ' cannot generate a response'); + } +} + +class JsonResponse extends Response { + private $data; + + public function __construct(DataResponse $r) { + $this->code = $r->code; + $this->data = $r->data; + } + + public function generate() { + $this->headers['Content-Type'] = 'application/json'; + parent::generate(); + print json_encode($this->data); + } +} + +class JsonResponseFilter extends Middleware { + public function execute(Request $request) { + $response = $this->next->execute($request); + + if ($response instanceof DataResponse) { + return new JsonResponse($response); + } + + return $response; + } +} diff --git a/http2/middleware.php b/http2/middleware.php new file mode 100644 index 0000000..4e51841 --- /dev/null +++ b/http2/middleware.php @@ -0,0 +1,62 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/pipeline.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +abstract class Middleware { + protected $pipeline; + protected $next; + + public function __construct(Pipeline $pipeline, + Middleware $next) { + $this->pipeline = $pipeline; + $this->next = $next; + } + + public abstract /*http\Response*/ function execute(Request $request); +} + +class Sentinel extends Middleware { + private $response; + + public function __construct(Response $response) { + $this->response = $response; + } + + public function execute(Request $request) { + return $this->response; + } +} + +class ClosureMiddleware extends Middleware { + private $closure; + + public function __construct(Pipeline $pipeline, + Middleware $next, + \Closure $closure) { + parent::__construct($pipeline, $next); + $this->closure = $closure; + } + + public function execute(Request $request) { + $closure = $this->closure; + return $closure($request, $this->next); + } +} diff --git a/http2/pipeline.php b/http2/pipeline.php new file mode 100644 index 0000000..5d7f885 --- /dev/null +++ b/http2/pipeline.php @@ -0,0 +1,80 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class Pipeline { + private $middleware = []; + + public function add($middleware) { + /* + if (!($middleware instanceof Middleware || + $middleware instanceof \Closure || + is_array($middleware))) { + // TODO(php7): TypeError + throw new \Exception(__FUNCTION__ . ' requires a Middleware or Closure'); + } + */ + $this->middleware[] = $middleware; + return $this; + } + + public function build() { + $chain = new Sentinel(new NotFoundResponse()); + for ($i = count($this->middleware) - 1; $i >= 0; --$i) { + $args = $this->middleware[$i]; + if ($args instanceof \Closure) { + $chain = new ClosureMiddleware($this, $chain, $args); + } else if (is_array($args)) { + $class = array_shift($args); + array_unshift($args, $this, $chain); + $chain = new $class(...$args); + } else { + $chain = new $args($this, $chain); + } + } + + return $chain; + } + + public function run(Middleware $chain) { + $response = $chain->execute(new Request()); + $response->generate(); + } + + public function buildAndRun() { + $this->run($this->build()); + } +} + +class SubPipeline extends Middleware { + public function __construct(Pipeline $subpipe) { + // Skip calling super since this is a new pipeline and the + // next middleware will be selected by building the subpipe. + // It would be possible to build the middleware chain here, but + // doing it lazily on execution is more efficient, since SubPipeline + // should be used to partition applications. + $this->subpipe = $subpipe; + } + + public function execute(Request $request) { + return $this->subpipe->build()->execute($request); + } +} diff --git a/http2/request.php b/http2/request.php new file mode 100644 index 0000000..f1ad7eb --- /dev/null +++ b/http2/request.php @@ -0,0 +1,64 @@ +. + +namespace hoplite\http2; + +/*! + A Request represents a HTTP request and holds the data and context associated + with it. +*/ +class Request +{ + /*! @var string The request method (upper case). */ + public $http_method = NULL; + + /*! @var string The URL, relataive to the RootController. */ + public $url = ''; + + /*! @var array HTTP request data. */ + public $data = []; + + /*! @var array Context data. */ + public $context = []; + + /*! + Constructor. Takes an optional URL. + */ + public function __construct($url='') + { + $this->url = $url; + } + + /*! + Wrapper around filter_input() that stores the result in the ::$data field. + */ + public function filter($type, $name, $filter=FILTER_SANITIZE_STRING, $options=NULL) + { + $rv = filter_input($type, $name, $filter, $options); + $this->data[$name] = $rv; + return $rv; + } + + /*! + Wrapper around filter_input() that merges the result in the ::$data field. + */ + public function filterArray($type, $definition, $add_empty=TRUE) + { + $rv = filter_input_array($type, $definition, $add_empty); + $this->data = array_merge($this->data, $rv); + return $rv; + } +} diff --git a/http2/response.php b/http2/response.php new file mode 100644 index 0000000..f29f5f8 --- /dev/null +++ b/http2/response.php @@ -0,0 +1,66 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http/response_code.php'; + +class Response +{ + /*! @var integer The HTTP response code to return. */ + public $code; + + /*! @var array A map of headers to values to be sent with the response. */ + public $headers = []; + + public function __construct($code=\hoplite\http\ResponseCode::OK) { + $this->code = $code; + } + + /*! @var string Raw HTTP response body. */ + public function generate() { + http_response_code($this->code); + foreach ($this->headers as $header => $value) { + header("$header: $value"); + } + } +} + +class NotFoundResponse extends Response { + public function __construct() { + parent::__construct(\hoplite\http\ResponseCode::NOT_FOUND); + } + + public function generate() { + parent::generate(); + print '

404 - Not Found

'; + } +} + +class TextResponse extends Response { + private $text; + + public function __construct($text) { + parent::__construct(); + $this->text = $text; + $this->headers['Content-Type'] = 'text/plain'; + } + + public function generate() { + parent::generate(); + print $this->text; + } +} -- 2.22.5 From 8d42991e2dad0d3deb8f7b50276a1eb452be3ced Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Tue, 29 Nov 2016 02:14:54 -0500 Subject: [PATCH 14/16] Add the Router and RouteMap components for http2. These are similar to the UrlMap and FrontController request processor in http, but have been adapted to the Pipeline/Middleware architecture. --- http2/action.php | 50 ++++++++++++++ http2/pipeline.php | 60 +++++++---------- http2/route_map.php | 155 ++++++++++++++++++++++++++++++++++++++++++++ http2/router.php | 71 ++++++++++++++++++++ 4 files changed, 301 insertions(+), 35 deletions(-) create mode 100644 http2/action.php create mode 100644 http2/route_map.php create mode 100644 http2/router.php diff --git a/http2/action.php b/http2/action.php new file mode 100644 index 0000000..68a8788 --- /dev/null +++ b/http2/action.php @@ -0,0 +1,50 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/request.php'; +require_once HOPLITE_ROOT . '/http2/response.php'; + +class Action extends Middleware { + protected $request; + + public function beforeRequest() {} + + public function afterRequest(Response $response) {} + + public function defaultRequest(Request $request) { + return $this->next->execute($request); + } + + public function execute(Request $request) { + $this->request = $request; + + $this->beforeRequest(); + + $action = @$request->context[Router::class]['data']['action']; + if (!$action || !method_exists($this, $action)) { + return $this->defaultRequest($request); + } + + $response = $this->{$action}(); + + $this->afterRequest($response); + + return $response; + } +} diff --git a/http2/pipeline.php b/http2/pipeline.php index 5d7f885..96c1e2f 100644 --- a/http2/pipeline.php +++ b/http2/pipeline.php @@ -24,33 +24,38 @@ class Pipeline { private $middleware = []; public function add($middleware) { - /* - if (!($middleware instanceof Middleware || - $middleware instanceof \Closure || - is_array($middleware))) { - // TODO(php7): TypeError - throw new \Exception(__FUNCTION__ . ' requires a Middleware or Closure'); - } - */ - $this->middleware[] = $middleware; + array_push($this->middleware, $middleware); return $this; } public function build() { + return $this->_build(new Sentinel(new NotFoundResponse())); + } + + public function buildWithTail($tail) { $chain = new Sentinel(new NotFoundResponse()); - for ($i = count($this->middleware) - 1; $i >= 0; --$i) { - $args = $this->middleware[$i]; - if ($args instanceof \Closure) { - $chain = new ClosureMiddleware($this, $chain, $args); - } else if (is_array($args)) { - $class = array_shift($args); - array_unshift($args, $this, $chain); - $chain = new $class(...$args); - } else { - $chain = new $args($this, $chain); - } + $chain = $this->buildMiddleware($tail, $chain); + return $this->_build($chain); + } + + public function buildMiddleware($args, $next) { + if ($args instanceof \Closure) { + return new ClosureMiddleware($this, $next, $args); + } else if ($args instanceof Pipeline) { + return $args->_build($next); + } else if (is_array($args)) { + $class = array_shift($args); + array_unshift($args, $this, $next); + return new $class(...$args); + } else { + return new $args($this, $next); } + } + private function _build($chain) { + for ($i = count($this->middleware) - 1; $i >= 0; --$i) { + $chain = $this->buildMiddleware($this->middleware[$i], $chain); + } return $chain; } @@ -63,18 +68,3 @@ class Pipeline { $this->run($this->build()); } } - -class SubPipeline extends Middleware { - public function __construct(Pipeline $subpipe) { - // Skip calling super since this is a new pipeline and the - // next middleware will be selected by building the subpipe. - // It would be possible to build the middleware chain here, but - // doing it lazily on execution is more efficient, since SubPipeline - // should be used to partition applications. - $this->subpipe = $subpipe; - } - - public function execute(Request $request) { - return $this->subpipe->build()->execute($request); - } -} diff --git a/http2/route_map.php b/http2/route_map.php new file mode 100644 index 0000000..92f00a1 --- /dev/null +++ b/http2/route_map.php @@ -0,0 +1,155 @@ +. + +namespace hoplite\http2; + +/*! + A RouteMap parses a HTTP Request URL, matches it against its set of rules, and + finds one that matches. If it is successful, data are extracted from the URL, + based on the specified pattern. + + The map is an associative array of URL prefix patterns to a result value. + When used with the Router Middleware, the value should be a Middleware that + can be built by Pipeline::buildMiddleware(). An Action subclass typical. + + The keys can be either a URL prefix or a regular expression. + + For URL prefixes, the pattern is matched relative to the Request::url field. + Patterns should not begin with a slash. Path fragment parameter extraction + can be performed as well. For example, the pattern 'user/view/{id}' will + match a URL like + http://example.com/webapp/user/view/42 + And the match result data will have a key called 'id' with value 42. + + Typically prefix matching will be done for the above patterns. This may be + unwanted if a more general path needs to execute before a more descendent + one. Using strict matching, invoked by ending a pattern with two slashes, + pattern matching becomes exact in that all path components must match. + + Regular expression matching is not limited to prefix patterns and can match + any part of the URL (though prefix matching can be enforced using the + the standard regex '^' character). Regex patterns must begin and end with + '/' characters. During evaluation, if any pattern groups are matched, the + resulting matches will be placed in Request data via the 'url_pattern' key. + The following will match the same URL as above: + '/^user\/view\/([0-9]+)/' +*/ +class RouteMap { + /*! @var array The map of URLs to actions. */ + private $map; + + public function __construct(array $map) { + $this->map = $map; + } + + /*! @brief Evalutes the URL map and finds a match. + This will take the incoming URL from the request and will match it against + the patterns in the internal map. + + Matching occurs in a linear scan of the URL map, so precedence can be + enforced by ordering rules appropriately. Regular expressions are matched + and URL parameter extraction occurs each time a different rule is evaluated. + + If a match is made, an associative array with this stucture is returned: + [ + 'result' => , + 'data' => [ ], + 'regexp' => + ] + + @return array|NULL A matched value in the ::map or NULL if no match. + */ + public function match(Request $request) { + $fragments = explode('/', $request->url); + $path_length = strlen($request->url); + + $result = []; + + foreach ($this->map as $rule => $action) { + // First check if this is the empty rule. + if (empty($rule)) { + if (empty($request->url)) + return [ 'result' => $action ]; + else + continue; + } + + // Check if this is a regular expression rule and match it. + if ($rule[0] == '/' && substr($rule, -1) == '/') { + $matches = array(); + if (preg_match($rule, $request->url, $matches)) { + // This pattern matched, so fill out the request and return. + return [ + 'result' => $action, + 'regexp' => $matches, + ]; + } + } + // Otherwise, this is just a normal string match. + else { + // Patterns that end with two slashes are exact. + $is_strict = substr($rule, -2) == '//'; + if ($is_strict) + $rule = substr($rule, 0, -2); + + // Set up some variables for the loop. + $is_match = TRUE; + $rule_fragments = explode('/', $rule); + $count_rule_fragments = count($rule_fragments); + $count_fragments = count($fragments); + $extractions = array(); + + // If this is a strict matcher, then do a quick test based on fragments. + if ($is_strict && $count_rule_fragments != $count_fragments) + continue; + + // Loop over the pieces of the rule, matching the fragments to that of + // the request. + foreach ($rule_fragments as $i => $rule_frag) { + // Don't iterate past the length of the request. Prefix matching means + // that this can still be a match. + if ($i >= $count_fragments) + break; + + // If this fragment is a key to be extracted, do so into a temporary + // array. + if (strlen($rule_frag) && $rule_frag[0] == '{' && substr($rule_frag, -1) == '}') { + $key = substr($rule_frag, 1, -1); + $extractions[$key] = $fragments[$i]; + } + // Otherwise, the path components mutch match. + else if ($rule_frag != $fragments[$i]) { + $is_match = FALSE; + break; + } + } + + // If no match was made, try the next rule. + if (!$is_match) + continue; + + // A match was made, so merge the path components that were extracted by + // key and return the match. + return [ + 'result' => $action, + 'data' => $extractions, + ]; + } + } + + return NULL; + } +} diff --git a/http2/router.php b/http2/router.php new file mode 100644 index 0000000..d991add --- /dev/null +++ b/http2/router.php @@ -0,0 +1,71 @@ +. + +namespace hoplite\http2; + +require_once HOPLITE_ROOT . '/http2/middleware.php'; +require_once HOPLITE_ROOT . '/http2/pipeline.php'; +require_once HOPLITE_ROOT . '/http2/route_map.php'; + +/*! + The Router is a middleware that operates on a RouteMap object. If a Request + matches, the value in the RouteMap is treated as a Middleware to be built by + Pipeline::buildMiddleware(). + + If the secondary Pipeline Router::subpipe is provided, the matched value will + be treated as if it were Pipeline::add()ed immediately before being built. + This allows additional middleware to execute IFF a route match occurs. + + If no route mach is made, then the Router runs the next middleware. +*/ +class Router extends Middleware { + private $map; + private $subpipe; + + public function __construct(Pipeline $pipeline, + Middleware $next, + RouteMap $map, + Pipeline $subpipe=NULL) { + parent::__construct($pipeline, $next); + $this->map = $map; + $this->subpipe = $subpipe ? $subpipe : new Pipeline(); + } + + public function execute(Request $request) { + // The query rewriter module of the webserver rewrites a request from: + // http://example.com/webapp/user/view/42 + // to: + // http://example.com/webapp/index.php/user/view/42 + // ... which then becomes accessible from PATH_INFO. + if (isset($_SERVER['PATH_INFO'])) + $url = $_SERVER['PATH_INFO']; + else + $url = '/'; + if ($url[0] == '/') + $url = substr($url, 1); + + $request->url = $url; + $route_result = $this->map->match($request); + + if ($route_result) { + $request->context[self::class] = $route_result; + $next = $this->subpipe->buildWithTail($route_result['result']); + return $next->execute($request); + } + + return $this->next->execute($request); + } +} -- 2.22.5 From 3f7966c4030934d8043c9a191b649d9d840629d7 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 3 Dec 2016 23:29:14 -0500 Subject: [PATCH 15/16] Upgrade PHPUnit to 5.6.7. --- testing/{test_runner.php => bootstrap.php} | 26 +++------------------- testing/phpunit.xml | 19 +++++++++++----- testing/test_listener.php | 11 ++++++++- 3 files changed, 27 insertions(+), 29 deletions(-) rename testing/{test_runner.php => bootstrap.php} (62%) diff --git a/testing/test_runner.php b/testing/bootstrap.php similarity index 62% rename from testing/test_runner.php rename to testing/bootstrap.php index 0be30f8..eb090e9 100644 --- a/testing/test_runner.php +++ b/testing/bootstrap.php @@ -1,11 +1,11 @@ Run($_SERVER['argv'], $exit); - } - - protected function HandleCustomTestSuite() - { - $this->arguments['printer'] = new TestListener(); - } -} - -HopliteTestRunner::Main(); diff --git a/testing/phpunit.xml b/testing/phpunit.xml index fc4de5b..5a630a1 100644 --- a/testing/phpunit.xml +++ b/testing/phpunit.xml @@ -1,24 +1,28 @@ - + + colors="TRUE" + bootstrap="bootstrap.php" + printerFile="test_listener.php" + printerClass="hoplite\test\TestListener" + verbose="TRUE"> @@ -27,12 +31,17 @@ + + ../ + ./ + diff --git a/testing/test_listener.php b/testing/test_listener.php index 1c65d93..1504d5a 100644 --- a/testing/test_listener.php +++ b/testing/test_listener.php @@ -48,6 +48,7 @@ class TestListener extends \PHPUnit_Util_Printer implements \PHPUnit_Framework_T // Array of incomplete tests. private $incomplete = array(); + private $risky = array(); // An error occurred. public function addError(\PHPUnit_Framework_Test $test, @@ -70,7 +71,7 @@ class TestListener extends \PHPUnit_Util_Printer implements \PHPUnit_Framework_T $this->_Print(' ', $e->GetMessage()); if ($e instanceof \PHPUnit_Framework_ExpectationFailedException) { $comp = $e->GetComparisonFailure(); - if ($comp instanceof \PHPUnit_Framework_ComparisonFailure) { + if ($comp) { $this->_Print(' ==> ', $comp->GetExpectedAsString()); $this->_Print('', 'does not match'); $this->_Print(' ==> ', $comp->GetActualAsString()); @@ -81,6 +82,14 @@ class TestListener extends \PHPUnit_Util_Printer implements \PHPUnit_Framework_T $this->failing[] = $test->ToString(); } + // Risky test. + public function addRiskyTest(\PHPUnit_Framework_Test $test, + \Exception $e, $time) + { + $this->risky[] = $test->ToString(); + $this->_Print('RISKY', $e->GetMessage(), self::COLOR_PURPLE); + } + // Incomplete test. public function addIncompleteTest(\PHPUnit_Framework_Test $test, \Exception $e, $time) -- 2.22.5 From 9dc44dcfa82e438f53202b1ee255815f57868253 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 3 Dec 2016 23:56:40 -0500 Subject: [PATCH 16/16] Middleware no longer takes a Pipeline. --- http2/action.php | 2 +- http2/middleware.php | 10 +++------- http2/pipeline.php | 6 +++--- http2/router.php | 5 ++--- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/http2/action.php b/http2/action.php index 68a8788..4b14e53 100644 --- a/http2/action.php +++ b/http2/action.php @@ -41,7 +41,7 @@ class Action extends Middleware { return $this->defaultRequest($request); } - $response = $this->{$action}(); + $response = $this->{$action}($request); $this->afterRequest($response); diff --git a/http2/middleware.php b/http2/middleware.php index 4e51841..c393d46 100644 --- a/http2/middleware.php +++ b/http2/middleware.php @@ -21,12 +21,9 @@ require_once HOPLITE_ROOT . '/http2/request.php'; require_once HOPLITE_ROOT . '/http2/response.php'; abstract class Middleware { - protected $pipeline; protected $next; - public function __construct(Pipeline $pipeline, - Middleware $next) { - $this->pipeline = $pipeline; + public function __construct(Middleware $next) { $this->next = $next; } @@ -48,10 +45,9 @@ class Sentinel extends Middleware { class ClosureMiddleware extends Middleware { private $closure; - public function __construct(Pipeline $pipeline, - Middleware $next, + public function __construct(Middleware $next, \Closure $closure) { - parent::__construct($pipeline, $next); + parent::__construct($next); $this->closure = $closure; } diff --git a/http2/pipeline.php b/http2/pipeline.php index 96c1e2f..ffd0154 100644 --- a/http2/pipeline.php +++ b/http2/pipeline.php @@ -40,15 +40,15 @@ class Pipeline { public function buildMiddleware($args, $next) { if ($args instanceof \Closure) { - return new ClosureMiddleware($this, $next, $args); + return new ClosureMiddleware($next, $args); } else if ($args instanceof Pipeline) { return $args->_build($next); } else if (is_array($args)) { $class = array_shift($args); - array_unshift($args, $this, $next); + array_unshift($args, $next); return new $class(...$args); } else { - return new $args($this, $next); + return new $args($next); } } diff --git a/http2/router.php b/http2/router.php index d991add..352a8b2 100644 --- a/http2/router.php +++ b/http2/router.php @@ -35,11 +35,10 @@ class Router extends Middleware { private $map; private $subpipe; - public function __construct(Pipeline $pipeline, - Middleware $next, + public function __construct(Middleware $next, RouteMap $map, Pipeline $subpipe=NULL) { - parent::__construct($pipeline, $next); + parent::__construct($next); $this->map = $map; $this->subpipe = $subpipe ? $subpipe : new Pipeline(); } -- 2.22.5