1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-16 03:34:33 +02:00

Add path hook support to ProcessPageView module plus some general refactoring and optimization of the class

This commit is contained in:
Ryan Cramer
2021-03-05 15:13:15 -05:00
parent 652a8a58e3
commit 184b3b6255

View File

@@ -8,7 +8,7 @@
* For more details about how Process modules work, please see: * For more details about how Process modules work, please see:
* /wire/core/Process.php * /wire/core/Process.php
* *
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @method string execute($internal = true) * @method string execute($internal = true)
@@ -18,6 +18,7 @@
* @method failed(\Exception $e, $reason = '', $page = null, $url = '') * @method failed(\Exception $e, $reason = '', $page = null, $url = '')
* @method sendFile($page, $basename) * @method sendFile($page, $basename)
* @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null) * @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null)
* @method string|bool|array|Page pathHooks($path, $out)
* *
*/ */
class ProcessPageView extends Process { class ProcessPageView extends Process {
@@ -42,6 +43,8 @@ class ProcessPageView extends Process {
const responseTypeFile = 4; const responseTypeFile = 4;
const responseTypeRedirect = 8; const responseTypeRedirect = 8;
const responseTypeExternal = 16; const responseTypeExternal = 16;
const responseTypeNoPage = 32;
const responseTypePathHook = 64;
/** /**
* Response type (see response type codes above) * Response type (see response type codes above)
@@ -85,12 +88,6 @@ class ProcessPageView extends Process {
*/ */
protected $requestFile = ''; protected $requestFile = '';
/**
* Prefixes allowed for page numbers in URLs
*
*/
protected $pageNumUrlPrefixes = array();
/** /**
* Page number found in the URL or null if not found * Page number found in the URL or null if not found
* *
@@ -103,6 +100,20 @@ class ProcessPageView extends Process {
*/ */
protected $pageNumPrefix = null; protected $pageNumPrefix = null;
/**
* @var Page|null
*
*/
protected $http404Page = null;
/**
* Return value from first iteration of pathHooks() method (when applicable)
*
* @var mixed
*
*/
protected $pathHooksReturnValue = false;
/** /**
* Construct * Construct
* *
@@ -145,99 +156,191 @@ class ProcessPageView extends Process {
if(!$internal) return $this->executeExternal(); if(!$internal) return $this->executeExternal();
$this->responseType = self::responseTypeNormal; $this->responseType = self::responseTypeNormal;
$config = $this->config; $config = $this->wire()->config;
$debug = $config->debug; $timerKey = $config->debug ? 'ProcessPageView.getPage()' : '';
if($config->usePoweredBy !== null) header('X-Powered-By:' . ($config->usePoweredBy ? ' ProcessWire CMS' : '')); if($config->usePoweredBy !== null) header('X-Powered-By:' . ($config->usePoweredBy ? ' ProcessWire CMS' : ''));
$pageNumUrlPrefixes = $config->pageNumUrlPrefixes; $this->wire()->pages->setOutputFormatting(true);
if(!is_array($pageNumUrlPrefixes)) $pageNumUrlPrefixes = array();
if(count($pageNumUrlPrefixes)) { if($timerKey) Debug::timer($timerKey);
foreach($pageNumUrlPrefixes as $prefix) { $page = $this->getPage();
$this->pageNumUrlPrefixes[$prefix] = $prefix; if($timerKey) Debug::saveTimer($timerKey, ($page && $page->id ? $page->path : ''));
}
} else { if(!$page || !$page->id) return $this->renderNoPage();
$prefix = $config->pageNumUrlPrefix;
if(strlen($prefix)) $this->pageNumUrlPrefixes[$prefix] = $prefix; return $this->renderPage($page);
}
/**
* Render Page
*
* @param Page $page
* @return bool|mixed|string
* @throws WireException
* @since 3.0.173
*
*/
protected function renderPage(Page $page) {
$config = $this->wire()->config;
$page->setOutputFormatting(true);
$_page = $page;
$page = $this->checkAccess($page);
if(!$page || $_page->id == $config->http404PageID) {
$s = 'access not allowed';
$e = new Wire404Exception($s, Wire404Exception::codePermission);
return $this->pageNotFound($_page, $this->requestPath, true, $s, $e);
} }
$this->pages->setOutputFormatting(true); if(!$this->delayRedirects) {
if($debug) Debug::timer('ProcessPageView.getPage()'); $this->checkProtocol($page);
$page = $this->getPage(); if($this->redirectURL) $this->wire()->session->redirect($this->redirectURL);
}
if($page && $page->id) { $this->wire('page', $page);
if($debug) Debug::saveTimer('ProcessPageView.getPage()', $page->path); $this->ready();
$page->setOutputFormatting(true); $page = $this->wire('page'); // in case anything changed it
$_page = $page;
$page = $this->checkAccess($page); if($this->delayRedirects) {
if(!$page || $_page->id == $config->http404PageID) { $this->checkProtocol($page);
$s = 'access not allowed'; if($this->redirectURL) $this->wire()->session->redirect($this->redirectURL);
$e = new Wire404Exception($s, Wire404Exception::codePermission); }
return $this->pageNotFound($_page, $this->requestPath, true, $s, $e);
} try {
if(!$this->delayRedirects) { if($this->requestFile) {
$this->checkProtocol($page);
if($this->redirectURL) $this->session->redirect($this->redirectURL); $this->responseType = self::responseTypeFile;
} $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $this->requestFile));
$this->sendFile($page, $this->requestFile);
$this->wire('page', $page);
$this->ready(); } else {
$page = $this->wire('page'); // in case anything changed it
$contentType = $this->contentTypeHeader($page, true);
if($this->delayRedirects) { $this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => $contentType));
$this->checkProtocol($page); if($config->ajax) $this->responseType = self::responseTypeAjax;
if($this->redirectURL) $this->session->redirect($this->redirectURL);
return $page->render();
} }
try { } catch(Wire404Exception $e) {
return $this->pageNotFound($page, $this->requestPath, false, '404 thrown during page render', $e);
if($this->requestFile) { } catch(\Exception $e) {
$this->responseType = self::responseTypeError;
$this->responseType = self::responseTypeFile; $this->failed($e, "Thrown during page render", $page);
$this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $this->requestFile)); throw $e; // re-throw non-404 Exceptions
$this->sendFile($page, $this->requestFile);
} else {
$contentType = $page->template->contentType;
if($contentType) {
if(strpos($contentType, '/') === false) {
if(isset($config->contentTypes[$contentType])) {
$contentType = $config->contentTypes[$contentType];
} else {
$contentType = '';
}
}
if($contentType) header("Content-Type: $contentType");
}
$this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => $contentType));
if($config->ajax) {
$this->responseType = self::responseTypeAjax;
return $page->render();
} else {
return $page->render();
}
}
} catch(Wire404Exception $e) {
return $this->pageNotFound($page, $this->requestPath, false, '404 thrown during page render', $e);
} catch(\Exception $e) {
$this->responseType = self::responseTypeError;
$this->failed($e, "Thrown during page render", $page);
throw $e; // re-throw non-404 Exceptions
}
} else {
return $this->pageNotFound(new NullPage(), $this->requestPath, true, 'Requested URL did not resolve to a Page');
} }
return ''; return '';
} }
/**
* Render when no page mapped to request URL
*
* @return array|bool|false|string
* @throws WireException
* @since 3.0.173
*
*/
protected function renderNoPage() {
$config = $this->wire()->config;
$hooks = $this->wire()->hooks;
$page = null;
$out = false;
$page404 = $this->http404Page();
$this->setResponseType(self::responseTypeNoPage);
$this->wire('page', $page404);
// run up to 2 times, once before ready state and once after
for($n = 1; $n <= 2; $n++) {
// call ready() on 2nd iteration only, allows for ready hooks to set $page
if($n === 2) $this->ready();
if(!$hooks->hasPathHooks()) continue;
$this->setResponseType(self::responseTypePathHook);
$out = $this->pathHooks($this->requestPath, $out);
// allow for pathHooks() $event->return to persist between init and ready states
// this makes it possible for ready() call to examine $event->return from init() call
// in case it wants to concatenate it or something
if($n === 1) $this->pathHooksReturnValue = $out;
if(is_object($out) && $out instanceof Page) {
// hook returned Page object to set as page to render
$page = $out;
$out = true;
} else {
// check if hooks changed $page API variable instead
$page = $this->wire()->page;
}
// first hook that determines the $page wins
if($page && $page->id && $page->id !== $page404->id) break;
$this->setResponseType(self::responseTypeNoPage);
}
$this->pathHooksReturnValue = false; // no longer applicable
if($page && $page->id && $page->id !== $page404->id && $page instanceof Page) {
// one of the path hooks set the page
$this->wire('page', $page);
return $this->renderPage($page);
}
if($out === false) {
// hook failed to handle request
return $this->pageNotFound(new NullPage(), $this->requestPath, false, 'Requested URL did not resolve to a Page');
} else if($out === true) {
// hook silently handled the request
$out = '';
} else if(is_array($out)) {
// hook returned array to convert to JSON
$jsonFlags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
$contentTypes = $config->contentTypes;
if(isset($contentTypes['json'])) header("Content-Type: $contentTypes[json]");
$out = json_encode($out, $jsonFlags);
}
return $out;
}
/**
* Get and optionally send the content-type header
*
* @param Page $page
* @param bool $send
* @return string
*
*/
protected function contentTypeHeader(Page $page, $send = false) {
$config = $this->wire()->config;
$contentType = $page->template->contentType;
if(!$contentType) return '';
if(strpos($contentType, '/') === false) {
if(isset($config->contentTypes[$contentType])) {
$contentType = $config->contentTypes[$contentType];
} else {
$contentType = '';
}
}
if($contentType && $send) header("Content-Type: $contentType");
return $contentType;
}
/** /**
* Method executed when externally bootstrapped * Method executed when externally bootstrapped
* *
@@ -510,15 +613,16 @@ class ProcessPageView extends Process {
protected function checkPageNumPath($path) { protected function checkPageNumPath($path) {
$hasPrefix = false; $hasPrefix = false;
$pageNumUrlPrefixes = $this->pageNumUrlPrefixes();
foreach($this->pageNumUrlPrefixes as $prefix) { foreach($pageNumUrlPrefixes as $prefix) {
if(strpos($path, '/' . $prefix) !== false) { if(strpos($path, '/' . $prefix) !== false) {
$hasPrefix = true; $hasPrefix = true;
break; break;
} }
} }
if($hasPrefix && preg_match('{/(' . implode('|', $this->pageNumUrlPrefixes) . ')(\d+)/?$}', $path, $matches)) { if($hasPrefix && preg_match('{/(' . implode('|', $pageNumUrlPrefixes) . ')(\d+)/?$}', $path, $matches)) {
// URL contains a page number, but we'll let it be handled by the checkUrlSegments function later // URL contains a page number, but we'll let it be handled by the checkUrlSegments function later
$this->pageNumPrefix = $matches[1]; $this->pageNumPrefix = $matches[1];
$this->pageNum = (int) $matches[2]; $this->pageNum = (int) $matches[2];
@@ -1036,12 +1140,10 @@ class ProcessPageView extends Process {
$this->failed($exception, $reason, $page, $url); $this->failed($exception, $reason, $page, $url);
$this->responseType = self::responseTypeError; $this->responseType = self::responseTypeError;
$config = $this->config;
$this->header404(); $this->header404();
if($config->http404PageID) { $page = $this->http404Page();
$page = $this->pages->get($config->http404PageID); if($page->id) {
if(!$page) throw new WireException("config::http404PageID does not exist - please check your config");
$this->wire('page', $page); $this->wire('page', $page);
if($triggerReady) $this->ready(); if($triggerReady) $this->ready();
return $page->render(); return $page->render();
@@ -1050,6 +1152,40 @@ class ProcessPageView extends Process {
} }
} }
/**
* Handler for path hooks
*
* No need to hook this method directly, instead use a path hook.
*
* #pw-internal
*
* @param string $path
* @param bool|string|array|Page Output so far, or false if none
* @return bool|string|array
* Return false if path cannot be handled
* Return true if path handled silently
* Return string for output to send
* Return array for JSON output to send
* Return Page object to make it the page that is rendered
*
*/
protected function ___pathHooks($path, $out) {
if($path && $out) {} // ignore
return $this->pathHooksReturnValue;
}
/**
* @return NullPage|Page
*
*/
protected function http404Page() {
if($this->http404Page) return $this->http404Page;
$config = $this->config;
$pages = $this->wire()->pages;
$this->http404Page = $config->http404PageID ? $pages->get($config->http404PageID) : $pages->newNullPage();
return $this->http404Page;
}
/** /**
* Send a 404 header, but not more than once per request * Send a 404 header, but not more than once per request
* *
@@ -1170,6 +1306,31 @@ class ProcessPageView extends Process {
return $parent && $parent->id ? $parent : null; return $parent && $parent->id ? $parent : null;
} }
/**
* Get page num URL prefixes
*
* @return array
*
*/
protected function pageNumUrlPrefixes() {
$config = $this->wire()->config;
$pageNumUrlPrefixes = $config->pageNumUrlPrefixes;
$returnValue = array();
if(!is_array($pageNumUrlPrefixes)) $pageNumUrlPrefixes = array();
if(count($pageNumUrlPrefixes)) {
foreach($pageNumUrlPrefixes as $prefix) {
$returnValue[$prefix] = $prefix;
}
} else {
$prefix = $config->pageNumUrlPrefix;
if(strlen($prefix)) $retrnValue[$prefix] = $prefix;
}
return $returnValue;
}
} }