Completed
Branch master (ead0c7)
by Atanas
01:54
created

HttpKernel::filterRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 0
loc 18
ccs 11
cts 11
cp 1
crap 4
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package   WPEmerge
4
 * @author    Atanas Angelov <[email protected]>
5
 * @copyright 2018 Atanas Angelov
6
 * @license   https://www.gnu.org/licenses/gpl-2.0.html GPL-2.0
7
 * @link      https://wpemerge.com/
8
 */
9
10
namespace WPEmerge\Kernels;
11
12
use Closure;
13
use Exception;
14
use Psr\Http\Message\ResponseInterface;
15
use WPEmerge\Application\Application;
16
use WPEmerge\Application\InjectionFactory;
17
use WPEmerge\Exceptions\ConfigurationException;
18
use WPEmerge\Exceptions\ErrorHandlerInterface;
19
use WPEmerge\Helpers\Handler;
20
use WPEmerge\Helpers\HandlerFactory;
21
use WPEmerge\Middleware\HasMiddlewareDefinitionsTrait;
22
use WPEmerge\Requests\RequestInterface;
23
use WPEmerge\Responses\ResponsableInterface;
24
use WPEmerge\Responses\ResponseService;
25
use WPEmerge\Routing\HasQueryFilterInterface;
26
use WPEmerge\Routing\Router;
27
use WPEmerge\Routing\SortsMiddlewareTrait;
28
29
/**
30
 * Describes how a request is handled.
31
 */
32
class HttpKernel implements HttpKernelInterface {
33
	use HasMiddlewareDefinitionsTrait;
34
	use SortsMiddlewareTrait;
35
36
	/**
37
	 * Application.
38
	 *
39
	 * @var Application
40
	 */
41
	protected $app = null;
42
43
	/**
44
	 * Injection factory.
45
	 *
46
	 * @var InjectionFactory
47
	 */
48
	protected $injection_factory = null;
49
50
	/**
51
	 * Handler factory.
52
	 *
53
	 * @var HandlerFactory
54
	 */
55
	protected $handler_factory = null;
56
57
	/**
58
	 * Response service.
59
	 *
60
	 * @var ResponseService
61
	 */
62
	protected $response_service = null;
63
64
	/**
65
	 * Request.
66
	 *
67
	 * @var RequestInterface
68
	 */
69
	protected $request = null;
70
71
	/**
72
	 * Router.
73
	 *
74
	 * @var Router
75
	 */
76
	protected $router = null;
77
78
	/**
79
	 * Error handler.
80
	 *
81
	 * @var ErrorHandlerInterface
82
	 */
83
	protected $error_handler = null;
84
85
	/**
86
	 * Constructor.
87
	 *
88
	 * @codeCoverageIgnore
89
	 * @param Application           $app
90
	 * @param InjectionFactory      $injection_factory
91
	 * @param HandlerFactory        $handler_factory
92
	 * @param ResponseService       $response_service
93
	 * @param RequestInterface      $request
94
	 * @param Router                $router
95
	 * @param ErrorHandlerInterface $error_handler
96
	 */
97
	public function __construct(
98
		Application $app,
99
		InjectionFactory $injection_factory,
100
		HandlerFactory $handler_factory,
101
		ResponseService $response_service,
102
		RequestInterface $request,
103
		Router $router,
104
		ErrorHandlerInterface $error_handler
105
	) {
106
		$this->app = $app;
107
		$this->injection_factory = $injection_factory;
108
		$this->handler_factory = $handler_factory;
109
		$this->response_service = $response_service;
110
		$this->request = $request;
111
		$this->router = $router;
112
		$this->error_handler = $error_handler;
113
	}
114
115
	/**
116
	 * {@inheritDoc}
117
	 * @codeCoverageIgnore
118
	 */
119
	public function bootstrap() {
120
		// Web. Use 3100 so it's high enough and has uncommonly used numbers
121
		// before and after. For example, 1000 is too common and it would have 999 before it
122
		// which is too common as well.).
123
		add_action( 'request', [$this, 'filterRequest'], 3100 );
124
		add_action( 'template_include', [$this, 'filterTemplateInclude'], 3100 );
125
126
		// Ajax.
127
		add_action( 'admin_init', [$this, 'registerAjaxAction'] );
128
129
		// Admin.
130
		add_action( 'admin_init', [$this, 'registerAdminAction'] );
131
	}
132
133
	/**
134
	 * Convert a user returned response to a ResponseInterface instance if possible.
135
	 * Return the original value if unsupported.
136
	 *
137
	 * @param  mixed $response
138
	 * @return mixed
139
	 */
140 4
	protected function toResponse( $response ) {
141 4
		if ( is_string( $response ) ) {
142 1
			return $this->response_service->output( $response );
143
		}
144
145 3
		if ( is_array( $response ) ) {
146 1
			return $this->response_service->json( $response );
147
		}
148
149 2
		if ( $response instanceof ResponsableInterface ) {
150 1
			return $response->toResponse();
151
		}
152
153 1
		return $response;
154
	}
155
156
	/**
157
	 * Execute a handler.
158
	 *
159
	 * @param  Handler           $handler
160
	 * @param  array             $arguments
161
	 * @return ResponseInterface
162
	 */
163 2
	protected function executeHandler( Handler $handler, $arguments = [] ) {
164 2
		$response = call_user_func_array( [$handler, 'execute'], $arguments );
165 2
		$response = $this->toResponse( $response );
166
167 2
		if ( ! $response instanceof ResponseInterface ) {
168 1
			throw new ConfigurationException(
169
				'Response returned by controller is not valid ' .
170 1
				'(expected ' . ResponseInterface::class . '; received ' . gettype( $response ) . ').'
171 1
			);
172
		}
173
174 1
		return $response;
175
	}
176
177
	/**
178
	 * Execute an array of middleware recursively (last in, first out).
179
	 *
180
	 * @param  array<array<string>> $middleware
181
	 * @param  RequestInterface     $request
182
	 * @param  Closure              $next
183
	 * @return ResponseInterface
184
	 */
185 3
	protected function executeMiddleware( $middleware, RequestInterface $request, Closure $next ) {
186 3
		$top_middleware = array_shift( $middleware );
187
188 3
		if ( $top_middleware === null ) {
189 3
			return $next( $request );
190
		}
191
192
		$top_middleware_next = function ( $request ) use ( $middleware, $next ) {
193 2
			return $this->executeMiddleware( $middleware, $request, $next );
194 2
		};
195
196 2
		$class = $top_middleware[0];
197 2
		$instance = $this->injection_factory->make( $class );
198 2
		$arguments = array_merge(
199 2
			[$request, $top_middleware_next],
200 2
			array_slice( $top_middleware, 1 )
201 2
		);
202
203 2
		return call_user_func_array( [$instance, 'handle'], $arguments );
204
	}
205
206
	/**
207
	 * {@inheritDoc}
208
	 */
209 2
	public function run( RequestInterface $request, $middleware, $handler, $arguments = [] ) {
210 2
		$this->error_handler->register();
211
212
		try {
213 2
			$middleware = $this->expandMiddleware( $middleware );
214 2
			$middleware = $this->uniqueMiddleware( $middleware );
215 2
			$middleware = $this->sortMiddleware( $middleware );
216
217
			$response = $this->executeMiddleware( $middleware, $request, function () use ( $handler, $arguments ) {
218 2
				$handler = $handler instanceof Handler ? $handler : $this->handler_factory->make( $handler );
219 2
				return $this->executeHandler( $handler, $arguments );
220 2
			} );
221 2
		} catch ( Exception $exception ) {
222 1
			$response = $this->error_handler->getResponse( $request, $exception );
223
		}
224
225 1
		$this->error_handler->unregister();
226
227 1
		return $response;
228
	}
229
230
	/**
231
	 * {@inheritDoc}
232
	 */
233 2
	public function handle( RequestInterface $request, $arguments = [] ) {
234 2
		$route = $this->router->execute( $request );
235
236 2
		if ( $route === null ) {
237 1
			return null;
238
		}
239
240 1
		$route_arguments = $route->getArguments( $request );
241
242
		$request = $request
243 1
			->withAttribute( 'route', $route )
244 1
			->withAttribute( 'route_arguments', $route_arguments );
245
246 1
		$handler = function ( $request ) use ( $route ) {
247 1
			return call_user_func( [$route, 'handle'], $request, func_get_args() );
248 1
		};
249
250 1
		$response = $this->run(
251 1
			$request,
252 1
			$route->getMiddleware(),
253 1
			$handler,
254 1
			array_merge(
255 1
				[$request],
256 1
				$arguments,
257
				$route_arguments
258 1
			)
259 1
		);
260
261 1
		$container = $this->app->getContainer();
262 1
		$container[ WPEMERGE_RESPONSE_KEY ] = $response;
263
264 1
		return $response;
265
	}
266
267
	/**
268
	 * Respond with the current response.
269
	 *
270
	 * @return void
271
	 */
272
	public function respond() {
273
		$response = $this->app->resolve( WPEMERGE_RESPONSE_KEY );
274
275
		if ( $response instanceof ResponseInterface ) {
276
			$this->response_service->respond( $response );
277
		}
278
	}
279
280
	/**
281
	 * Filter the main query vars.
282
	 *
283
	 * @param  array $query_vars
284
	 * @return array
285
	 */
286 2
	public function filterRequest( $query_vars ) {
287
		/** @var $routes \WPEmerge\Routing\RouteInterface[] */
288 2
		$routes = $this->router->getRoutes();
289
290 2
		foreach ( $routes as $route ) {
291 2
			if ( ! $route instanceof HasQueryFilterInterface ) {
292 2
				continue;
293
			}
294
295 2
			if ( ! $route->isSatisfied( $this->request ) ) {
296 1
				continue;
297
			}
298
299 2
			$query_vars = $route->applyQueryFilter( $this->request, $query_vars );
300 2
			break;
301 2
		}
302
303 2
		return $query_vars;
304
	}
305
306
	/**
307
	 * Filter the main template file.
308
	 *
309
	 * @param  string $view
310
	 * @return string
311
	 */
312 3
	public function filterTemplateInclude( $view ) {
313
		/** @var $wp_query \WP_Query */
314 3
		global $wp_query;
315
316 3
		$response = $this->handle( $this->request, [$view] );
317
318 3
		if ( $response instanceof ResponseInterface ) {
319 2
			if ( $response->getStatusCode() === 404 ) {
320 1
				$wp_query->set_404();
321 1
			}
322
323 2
			add_action( 'wpemerge.respond', [$this, 'respond'] );
324
325 2
			return WPEMERGE_DIR . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'view.php';
326
		}
327
328 1
		return $view;
329
	}
330
331
	/**
332
	 * Register ajax action to hook into current one.
333
	 *
334
	 * @return void
335
	 */
336
	public function registerAjaxAction() {
337
		if ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) {
338
			return;
339
		}
340
341
		$action = $this->request->body( 'action', $this->request->query( 'action' ) );
342
		$action = sanitize_text_field( $action );
343
344
		add_action( "wp_ajax_{$action}", [$this, 'actionAjax'] );
345
		add_action( "wp_ajax_nopriv_{$action}", [$this, 'actionAjax'] );
346
	}
347
348
	/**
349
	 * Act on ajax action.
350
	 *
351
	 * @return void
352
	 */
353
	public function actionAjax() {
354
		$response = $this->handle( $this->request, [''] );
355
356
		if ( ! $response instanceof ResponseInterface ) {
357
			return;
358
		}
359
360
		$this->response_service->respond( $response );
361
362
		wp_die( '', '', ['response' => null] );
363
	}
364
365
	/**
366
	 * Get page hook.
367
	 * Slightly modified version of code from wp-admin/admin.php.
368
	 *
369
	 * @return string
370
	 */
371
	protected function getAdminPageHook() {
372
		global $pagenow, $typenow, $plugin_page;
373
374
		$page_hook = '';
375
376
		if ( isset( $plugin_page ) ) {
377
			$the_parent = $pagenow;
378
379
			if ( ! empty( $typenow ) ) {
380
				$the_parent = $pagenow . '?post_type=' . $typenow;
381
			}
382
383
			$page_hook = get_plugin_page_hook( $plugin_page, $the_parent );
384
		}
385
386
		return $page_hook;
387
	}
388
389
	/**
390
	 * Get admin page hook.
391
	 * Slightly modified version of code from wp-admin/admin.php.
392
	 *
393
	 * @param  string $page_hook
394
	 * @return string
395
	 */
396
	protected function getAdminHook( $page_hook ) {
397
		global $pagenow, $plugin_page;
398
399
		if ( ! empty( $page_hook ) ) {
400
			return $page_hook;
401
		}
402
403
		if ( isset( $plugin_page ) ) {
404
			return $plugin_page;
405
		}
406
407
		if ( isset( $pagenow ) ) {
408
			return $pagenow;
409
		}
410
411
		return '';
412
	}
413
414
	/**
415
	 * Register admin action to hook into current one.
416
	 *
417
	 * @return void
418
	 */
419
	public function registerAdminAction() {
420
		$page_hook = $this->getAdminPageHook();
421
		$hook_suffix = $this->getAdminHook( $page_hook );
422
423
		add_action( "load-{$hook_suffix}", [$this, 'actionAdminLoad'] );
424
		add_action( $hook_suffix, [$this, 'actionAdmin'] );
425
	}
426
427
	/**
428
	 * Act on admin action load.
429
	 *
430
	 * @return void
431
	 */
432
	public function actionAdminLoad() {
433
		$response = $this->handle( $this->request, [''] );
434
435
		if ( ! $response instanceof ResponseInterface ) {
436
			return;
437
		}
438
439
		if ( ! headers_sent() ) {
440
			$this->response_service->sendHeaders( $response );
441
		}
442
	}
443
444
	/**
445
	 * Act on admin action.
446
	 *
447
	 * @return void
448
	 */
449
	public function actionAdmin() {
450
		$response = $this->app->resolve( WPEMERGE_RESPONSE_KEY );
451
452
		if ( ! $response instanceof ResponseInterface ) {
453
			return;
454
		}
455
456
		$this->response_service->sendBody( $response );
457
	}
458
}
459