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:
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user