mirror of
https://github.com/processwire/processwire.git
synced 2025-08-13 10:15:28 +02:00
Upgrade path hooks with 1) pagination support; and 2) the ability to handle requests triggered by 404s thrown from template files. To support pagination, just append "{pageNum}" to your URL to match and it will take care of setting the $input->pageNum() and calling your hook. Example: $wire->addHook('/foo/bar/{pageNum}', function($event) { ... }); To handle requests triggered by 404s a template file, you don't need to do anything extra, as it will take care of calling your path hook(s) automatically should the requested page URL match the one added by your hook.
This commit is contained in:
@@ -830,11 +830,12 @@ class WireHooks {
|
||||
$filterPath = trim(str_replace(array('-', '_', '.'), '/', $path), '/');
|
||||
foreach(explode('/', $filterPath) as $filter) {
|
||||
// identify any non-regex portions to use as pre-filters before using regexes
|
||||
// @todo see if this can be improved further to include slash positions
|
||||
if(ctype_alnum($filter) && strlen($filter) > 1) $filters[] = $filter;
|
||||
}
|
||||
$this->pathHooks[$id] = array(
|
||||
'match' => $path,
|
||||
'filters' => array(),
|
||||
'filters' => $filters,
|
||||
);
|
||||
return $id;
|
||||
}
|
||||
@@ -964,8 +965,9 @@ class WireHooks {
|
||||
}
|
||||
|
||||
if($this->allowPathHooks && isset($this->pathHooks[$hook['id']])) {
|
||||
if(!$this->allowRunPathHook($hook, $arguments)) continue;
|
||||
$allowRunPathHook = $this->allowRunPathHook($hook['id'], $arguments);
|
||||
$this->removeHook($object, $hook['id']); // once only
|
||||
if(!$allowRunPathHook) continue;
|
||||
$useHookReturnValue = true;
|
||||
}
|
||||
|
||||
@@ -1063,21 +1065,22 @@ class WireHooks {
|
||||
* regular and regex matches and populating parenthesized portions to arguments
|
||||
* that will appear in the HookEvent.
|
||||
*
|
||||
* @param array $hook
|
||||
* @param string $id Hook ID
|
||||
* @param array $arguments
|
||||
* @return bool
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
protected function allowRunPathHook(array $hook, array &$arguments) {
|
||||
protected function allowRunPathHook($id, array &$arguments) {
|
||||
|
||||
$id = $hook['id'];
|
||||
$pathHook = $this->pathHooks[$id];
|
||||
$matchPath = $pathHook['match'];
|
||||
$requestPath = $arguments[0];
|
||||
$slashed = substr($requestPath, -1) === '/' && strlen($requestPath) > 1;
|
||||
$filterFail = false;
|
||||
$regexDelim = ''; // populated only for user-specified regex
|
||||
$pageNum = $this->wire->wire()->input->pageNum();
|
||||
$pageNumArgument = 0;
|
||||
|
||||
// first pre-filter the requestPath against any words matchPath (filters)
|
||||
foreach($pathHook['filters'] as $filter) {
|
||||
@@ -1097,6 +1100,17 @@ class WireHooks {
|
||||
$matchPath = "#$matchPath$#";
|
||||
}
|
||||
|
||||
if(strpos($matchPath, '{pageNum}') !== false) {
|
||||
// the {pageNum} named argument maps to $input->pageNum. remove the {pageNum} argument
|
||||
// from the match path since it is handled differently from other named arguments
|
||||
$matchPath = str_replace(array('/{pageNum}/', '/{pageNum}'), '/', $matchPath);
|
||||
$pathHook['match'] = str_replace(array('/{pageNum}/', '/{pageNum}'), '/', $pathHook['match']);
|
||||
$pageNumArgument = $pageNum;
|
||||
} else if($pageNum > 1) {
|
||||
// hook does not handle pagination numbers above 1
|
||||
return false;
|
||||
}
|
||||
|
||||
if(strpos($matchPath, ':') && strpos($matchPath, '(') !== false) {
|
||||
// named arguments in format “(name: value)” converted to named PCRE capture groups
|
||||
$matchPath = preg_replace('#\(([-_a-z0-9]+):#i', '(?P<$1>', $matchPath);
|
||||
@@ -1136,6 +1150,7 @@ class WireHooks {
|
||||
|
||||
// success: at this point the requestPath has matched
|
||||
$arguments['path'] = $arguments[0];
|
||||
if($pageNumArgument) $arguments['pageNum'] = $pageNumArgument;
|
||||
|
||||
foreach($matches as $key => $value) {
|
||||
// populate requested arguments
|
||||
@@ -1270,14 +1285,42 @@ class WireHooks {
|
||||
/**
|
||||
* Return whether or not any path hooks are pending
|
||||
*
|
||||
* @return int
|
||||
* @param string $requestPath Optionally provide request path to determine if any might match (3.0.174+)
|
||||
* @return bool
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
public function hasPathHooks() {
|
||||
public function hasPathHooks($requestPath = '') {
|
||||
// first pre-filter the requestPath against any words matchPath (filters)
|
||||
if(strlen($requestPath)) return $this->filterPathHooks($requestPath, true);
|
||||
return count($this->pathHooks) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return path hooks that have potential to match given request path
|
||||
*
|
||||
* @param string $requestPath
|
||||
* @param bool $has Specify true to change return value to boolean as to whether any can match (default=false)
|
||||
* @return array|bool
|
||||
* @since 3.0.174
|
||||
*
|
||||
*/
|
||||
public function filterPathHooks($requestPath, $has = false) {
|
||||
$pathHooks = array();
|
||||
foreach($this->pathHooks as $id => $pathHook) {
|
||||
$fail = false;
|
||||
foreach($pathHook['filters'] as $filter) {
|
||||
$fail = strpos($requestPath, $filter) === false;
|
||||
if($fail) break;
|
||||
}
|
||||
if(!$fail) {
|
||||
$pathHooks[$id] = $pathHook;
|
||||
if($has) break;
|
||||
}
|
||||
}
|
||||
return $has ? count($pathHooks) > 0 : $pathHooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set whether path hooks are allowed
|
||||
*
|
||||
|
@@ -226,12 +226,19 @@ class ProcessPageView extends Process {
|
||||
}
|
||||
|
||||
} catch(Wire404Exception $e) {
|
||||
return $this->pageNotFound($page, $this->requestPath, false, '404 thrown during page render', $e);
|
||||
// 404 exception
|
||||
return $this->renderNoPage(array(
|
||||
'reason404' => '404 thrown during page render',
|
||||
'exception404' => $e,
|
||||
'page' => $page,
|
||||
'ready' => true, // let it know ready state already executed
|
||||
));
|
||||
|
||||
} catch(\Exception $e) {
|
||||
// other exception (re-throw non 404 exceptions)
|
||||
$this->responseType = self::responseTypeError;
|
||||
$this->failed($e, "Thrown during page render", $page);
|
||||
throw $e; // re-throw non-404 Exceptions
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -240,31 +247,71 @@ class ProcessPageView extends Process {
|
||||
/**
|
||||
* Render when no page mapped to request URL
|
||||
*
|
||||
* @param array $options
|
||||
* @return array|bool|false|string
|
||||
* @throws WireException
|
||||
* @since 3.0.173
|
||||
*
|
||||
*/
|
||||
protected function renderNoPage() {
|
||||
protected function renderNoPage(array $options = array()) {
|
||||
|
||||
$defaults = array(
|
||||
'allow404' => true, // allow 404 to be thrown?
|
||||
'reason404' => 'Requested URL did not resolve to a Page',
|
||||
'exception404' => null,
|
||||
'ready' => false, // are we executing from the API ready state?
|
||||
'page' => $this->http404Page(), // potential Page object (default is 404 page)
|
||||
);
|
||||
|
||||
$options = count($options) ? array_merge($defaults, $options) : $defaults;
|
||||
$config = $this->wire()->config;
|
||||
$hooks = $this->wire()->hooks;
|
||||
$input = $this->wire()->input;
|
||||
$requestPath = $this->requestPath;
|
||||
$setPageNum = 0;
|
||||
$pageNumSegment = '';
|
||||
$page = null;
|
||||
$out = false;
|
||||
$page404 = $this->http404Page();
|
||||
|
||||
$this->setResponseType(self::responseTypeNoPage);
|
||||
$this->wire('page', $page404);
|
||||
|
||||
if($this->pageNum > 0 && $this->pageNumPrefix !== null) {
|
||||
// there is a pagination segment present in the request path
|
||||
$slash = substr($requestPath, -1) === '/' ? '/' : '';
|
||||
$requestPath = rtrim($requestPath, '/');
|
||||
$pageNumSegment = $this->pageNumPrefix . $this->pageNum;
|
||||
if(substr($requestPath, -1 * strlen($pageNumSegment)) === $pageNumSegment) {
|
||||
// remove pagination segment from request path
|
||||
$requestPath = substr($requestPath, 0, -1 * strlen($pageNumSegment));
|
||||
$setPageNum = $this->pageNum;
|
||||
// disallow specific "/page1" in URL as it is implied by the lack of pagination segment
|
||||
if($setPageNum === 1) $this->redirect($config->urls->root . ltrim($requestPath, '/'));
|
||||
// enforce no trailing slashes for pagination numbers
|
||||
if($slash) $this->redirect($config->urls->root . ltrim($requestPath, '/') . $pageNumSegment);
|
||||
$input->setPageNum($this->pageNum);
|
||||
} else {
|
||||
// not a pagination segment
|
||||
// add the slash back to restore requestPath
|
||||
$requestPath .= $slash;
|
||||
$pageNumSegment = '';
|
||||
}
|
||||
}
|
||||
|
||||
if(!$options['ready']) $this->wire('page', $options['page']);
|
||||
|
||||
// run up to 2 times, once before ready state and once after
|
||||
for($n = 1; $n <= 2; $n++) {
|
||||
|
||||
// only run once if already in ready state
|
||||
if($options['ready']) $n = 2;
|
||||
|
||||
// call ready() on 2nd iteration only, allows for ready hooks to set $page
|
||||
if($n === 2) $this->ready();
|
||||
if($n === 2 && !$options['ready']) $this->ready();
|
||||
|
||||
if(!$hooks->hasPathHooks()) continue;
|
||||
|
||||
$this->setResponseType(self::responseTypePathHook);
|
||||
$out = $this->pathHooks($this->requestPath, $out);
|
||||
$out = $this->pathHooks($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
|
||||
@@ -281,7 +328,7 @@ class ProcessPageView extends Process {
|
||||
}
|
||||
|
||||
// first hook that determines the $page wins
|
||||
if($page && $page->id && $page->id !== $page404->id) break;
|
||||
if($page && $page->id && $page->id !== $options['page']->id) break;
|
||||
|
||||
$this->setResponseType(self::responseTypeNoPage);
|
||||
}
|
||||
@@ -289,13 +336,17 @@ class ProcessPageView extends Process {
|
||||
// did a path hook require a redirect for trailing slash (vs non-trailing slash)?
|
||||
$redirect = $hooks->getPathHookRedirect();
|
||||
if($redirect) {
|
||||
$this->redirect($config->urls->root . ltrim($redirect, '/'));
|
||||
// path hook suggests a redirect for proper URL format
|
||||
$url = $config->urls->root . ltrim($redirect, '/');
|
||||
// if present, add pagination segment back into URL
|
||||
if($pageNumSegment) $url = rtrim($url, '/') . "/$pageNumSegment";
|
||||
$this->redirect($url);
|
||||
}
|
||||
|
||||
$this->pathHooksReturnValue = false; // no longer applicable once this line reached
|
||||
$hooks->allowPathHooks(false); // no more path hooks allowed
|
||||
|
||||
if($page && $page->id && $page->id !== $page404->id && $page instanceof Page) {
|
||||
if($page && $page->id && $page instanceof Page && $page->id !== $options['page']->id) {
|
||||
// one of the path hooks set the page
|
||||
$this->wire('page', $page);
|
||||
return $this->renderPage($page);
|
||||
@@ -303,7 +354,15 @@ class ProcessPageView extends Process {
|
||||
|
||||
if($out === false) {
|
||||
// hook failed to handle request
|
||||
return $this->pageNotFound(new NullPage(), $this->requestPath, false, 'Requested URL did not resolve to a Page');
|
||||
if($setPageNum > 1) $input->setPageNum(1);
|
||||
if($options['allow404']) {
|
||||
$page = $options['page'];
|
||||
// hooks to pageNotFound() method may expect NullPage rather than 404 page
|
||||
if($page->id == $config->http404PageID) $page = $this->wire(new NullPage());
|
||||
$out = $this->pageNotFound($page, $this->requestPath, false, $options['reason404'], $options['exception404']);
|
||||
} else {
|
||||
$out = false;
|
||||
}
|
||||
|
||||
} else if($out === true) {
|
||||
// hook silently handled the request
|
||||
@@ -1350,7 +1409,7 @@ class ProcessPageView extends Process {
|
||||
}
|
||||
} else {
|
||||
$prefix = $config->pageNumUrlPrefix;
|
||||
if(strlen($prefix)) $retrnValue[$prefix] = $prefix;
|
||||
if(strlen($prefix)) $returnValue[$prefix] = $prefix;
|
||||
}
|
||||
|
||||
return $returnValue;
|
||||
|
Reference in New Issue
Block a user