1427 lines
43 KiB
Text
1427 lines
43 KiB
Text
|
<?php namespace ProcessWire;
|
|||
|
|
|||
|
/**
|
|||
|
* ProcessWire Page View Process
|
|||
|
*
|
|||
|
* Enables viewing or Processes, one of the core components in connecting ProcessWire to HTTP.
|
|||
|
*
|
|||
|
* For more details about how Process modules work, please see:
|
|||
|
* /wire/core/Process.php
|
|||
|
*
|
|||
|
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
|||
|
* https://processwire.com
|
|||
|
*
|
|||
|
* @method string execute($internal = true)
|
|||
|
* @method string executeExternal()
|
|||
|
* @method ready(array $data = array())
|
|||
|
* @method finished(array $data = array())
|
|||
|
* @method failed(\Exception $e, $reason = '', $page = null, $url = '')
|
|||
|
* @method sendFile($page, $basename)
|
|||
|
* @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null)
|
|||
|
* @method string|bool|array|Page pathHooks($path, $out)
|
|||
|
*
|
|||
|
*/
|
|||
|
class ProcessPageView extends Process {
|
|||
|
|
|||
|
public static function getModuleInfo() {
|
|||
|
return array(
|
|||
|
'title' => __('Page View', __FILE__), // getModuleInfo title
|
|||
|
'summary' => __('All page views are routed through this Process', __FILE__), // getModuleInfo summary
|
|||
|
'version' => 104,
|
|||
|
'permanent' => true,
|
|||
|
'permission' => 'page-view',
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Response types
|
|||
|
*
|
|||
|
*/
|
|||
|
const responseTypeError = 0;
|
|||
|
const responseTypeNormal = 1;
|
|||
|
const responseTypeAjax = 2;
|
|||
|
const responseTypeFile = 4;
|
|||
|
const responseTypeRedirect = 8;
|
|||
|
const responseTypeExternal = 16;
|
|||
|
const responseTypeNoPage = 32;
|
|||
|
const responseTypePathHook = 64;
|
|||
|
|
|||
|
/**
|
|||
|
* Response type (see response type codes above)
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $responseType = 1;
|
|||
|
|
|||
|
/**
|
|||
|
* URL that should be redirected to for this request
|
|||
|
*
|
|||
|
* Set by other methods in this class, and checked by the execute method before rendering.
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $redirectURL = '';
|
|||
|
|
|||
|
/**
|
|||
|
* True if any redirects should be delayed until after API ready() has been issued
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $delayRedirects = false;
|
|||
|
|
|||
|
/**
|
|||
|
* Sanitized path that generated this request
|
|||
|
*
|
|||
|
* Set by the getPage() method and passed to the pageNotFound function.
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $requestPath = '';
|
|||
|
|
|||
|
/**
|
|||
|
* Unsanitized URL from $_SERVER['REQUEST_URI']
|
|||
|
*
|
|||
|
* @var string
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $dirtyURL = '';
|
|||
|
|
|||
|
/**
|
|||
|
* Requested filename, if URL in /path/to/page/-/filename.ext format
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $requestFile = '';
|
|||
|
|
|||
|
/**
|
|||
|
* Page number found in the URL or null if not found
|
|||
|
*
|
|||
|
*/
|
|||
|
protected $pageNum = null;
|
|||
|
|
|||
|
/**
|
|||
|
* Page number prefix found in the URL or null if not found
|
|||
|
*
|
|||
|
*/
|
|||
|
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
|
|||
|
*
|
|||
|
*/
|
|||
|
public function __construct() {
|
|||
|
// no parent call intentional
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Init
|
|||
|
*
|
|||
|
*/
|
|||
|
public function init() {
|
|||
|
|
|||
|
$this->dirtyURL = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
|
|||
|
if(empty($this->dirtyURL) && !empty($_SERVER['QUERY_STRING'])) $this->dirtyURL = '?' . $_SERVER['QUERY_STRING'];
|
|||
|
|
|||
|
// check if there is an 'it' GET variable present in the request URL query string, which we don't want here
|
|||
|
if(isset($_GET['it']) && (strpos($this->dirtyURL, '?it=') !== false || strpos($this->dirtyURL, '&it='))) {
|
|||
|
// force to use path in request url rather than contents of 'it' var
|
|||
|
list($it,) = explode('?', $this->dirtyURL);
|
|||
|
$rootURL = $this->wire('config')->urls->root;
|
|||
|
if(strlen($rootURL) > 1 && strpos($it, $rootURL) === 0) $it = substr($it, strlen($rootURL)-1);
|
|||
|
$it = str_replace('index.php', '', $it);
|
|||
|
$_GET['it'] = $it;
|
|||
|
}
|
|||
|
|
|||
|
// no parent call intentional
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Retrieve a page, check access, and render
|
|||
|
*
|
|||
|
* @param bool $internal True if request should be internally processed. False if PW is bootstrapped externally.
|
|||
|
* @return string Output of request
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___execute($internal = true) {
|
|||
|
|
|||
|
if(!$internal) return $this->executeExternal();
|
|||
|
|
|||
|
$this->responseType = self::responseTypeNormal;
|
|||
|
$config = $this->wire()->config;
|
|||
|
$timerKey = $config->debug ? 'ProcessPageView.getPage()' : '';
|
|||
|
if($config->usePoweredBy !== null) header('X-Powered-By:' . ($config->usePoweredBy ? ' ProcessWire CMS' : ''));
|
|||
|
|
|||
|
$this->wire()->pages->setOutputFormatting(true);
|
|||
|
|
|||
|
if($timerKey) Debug::timer($timerKey);
|
|||
|
$page = $this->getPage();
|
|||
|
if($timerKey) Debug::saveTimer($timerKey, ($page && $page->id ? $page->path : ''));
|
|||
|
|
|||
|
if(!$page || !$page->id) return $this->renderNoPage();
|
|||
|
|
|||
|
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);
|
|||
|
}
|
|||
|
|
|||
|
if(!$this->delayRedirects) {
|
|||
|
$this->checkProtocol($page);
|
|||
|
if($this->redirectURL) $this->redirect($this->redirectURL);
|
|||
|
}
|
|||
|
|
|||
|
$this->wire('page', $page);
|
|||
|
$this->ready();
|
|||
|
$page = $this->wire('page'); // in case anything changed it
|
|||
|
|
|||
|
if($this->delayRedirects) {
|
|||
|
$this->checkProtocol($page);
|
|||
|
if($this->redirectURL) $this->redirect($this->redirectURL);
|
|||
|
}
|
|||
|
|
|||
|
try {
|
|||
|
|
|||
|
if($this->requestFile) {
|
|||
|
|
|||
|
$this->responseType = self::responseTypeFile;
|
|||
|
$this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $this->requestFile));
|
|||
|
$this->sendFile($page, $this->requestFile);
|
|||
|
|
|||
|
} else {
|
|||
|
|
|||
|
$contentType = $this->contentTypeHeader($page, true);
|
|||
|
$this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => $contentType));
|
|||
|
if($config->ajax) $this->responseType = self::responseTypeAjax;
|
|||
|
|
|||
|
return $page->render();
|
|||
|
}
|
|||
|
|
|||
|
} catch(Wire404Exception $e) {
|
|||
|
// 404 exception
|
|||
|
TemplateFile::clearAll();
|
|||
|
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;
|
|||
|
}
|
|||
|
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 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(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;
|
|||
|
|
|||
|
$this->setResponseType(self::responseTypeNoPage);
|
|||
|
|
|||
|
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 && !$options['ready']) $this->ready();
|
|||
|
|
|||
|
if(!$hooks->hasPathHooks()) continue;
|
|||
|
|
|||
|
$this->setResponseType(self::responseTypePathHook);
|
|||
|
|
|||
|
try {
|
|||
|
$out = $this->pathHooks($requestPath, $out);
|
|||
|
} catch(Wire404Exception $e) {
|
|||
|
$out = false;
|
|||
|
}
|
|||
|
|
|||
|
// 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 !== $options['page']->id) break;
|
|||
|
|
|||
|
$this->setResponseType(self::responseTypeNoPage);
|
|||
|
}
|
|||
|
|
|||
|
// did a path hook require a redirect for trailing slash (vs non-trailing slash)?
|
|||
|
$redirect = $hooks->getPathHookRedirect();
|
|||
|
if($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 instanceof Page && $page->id !== $options['page']->id) {
|
|||
|
// one of the path hooks set the page
|
|||
|
$this->wire('page', $page);
|
|||
|
return $this->renderPage($page);
|
|||
|
}
|
|||
|
|
|||
|
if($out === false) {
|
|||
|
// hook failed to handle request
|
|||
|
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
|
|||
|
$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
|
|||
|
*
|
|||
|
* @return string blank string
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___executeExternal() {
|
|||
|
$this->setResponseType(self::responseTypeExternal);
|
|||
|
$config = $this->wire('config');
|
|||
|
$config->external = true;
|
|||
|
if($config->externalPageID) {
|
|||
|
$page = $this->wire('pages')->get((int) $config->externalPageID);
|
|||
|
} else {
|
|||
|
$page = $this->wire('pages')->newNullPage();
|
|||
|
}
|
|||
|
$this->wire('page', $page);
|
|||
|
$this->ready();
|
|||
|
$this->wire()->setStatus(ProcessWire::statusRender, array('contentType' => 'external'));
|
|||
|
return '';
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called when the $page API var is ready, and before the $page is rendered.
|
|||
|
*
|
|||
|
* @param array $data
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___ready(array $data = array()) {
|
|||
|
$this->wire()->setStatus(ProcessWire::statusReady, $data);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called with the pageview has been finished and output has been sent. Note this is called in /index.php.
|
|||
|
*
|
|||
|
* @param array $data
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___finished(array $data = array()) {
|
|||
|
$this->wire()->setStatus(ProcessWire::statusFinished, $data);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hook called when the pageview failed to finish due to an Exception or Error.
|
|||
|
*
|
|||
|
* Sends a copy of the throwable that occurred.
|
|||
|
*
|
|||
|
* @param \Throwable $e Exception or Error
|
|||
|
* @param string $reason
|
|||
|
* @param Page|null $page
|
|||
|
* @param string $url
|
|||
|
*
|
|||
|
*/
|
|||
|
public function ___failed($e, $reason = '', $page = null, $url = '') {
|
|||
|
$this->wire()->setStatusFailed($e, $reason, $page, $url);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get the requested page and populate it with identified urlSegments or page numbers
|
|||
|
*
|
|||
|
* @return Page|null
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function getPage() {
|
|||
|
|
|||
|
$config = $this->wire()->config;
|
|||
|
$sanitizer = $this->wire()->sanitizer;
|
|||
|
|
|||
|
// force redirect to actual page URL? (if different from request URL)
|
|||
|
$forceRedirect = false;
|
|||
|
|
|||
|
// did URL end with index.php|htm|html? If so we might redirect if a page matches without it.
|
|||
|
$hasIndexFile = false;
|
|||
|
|
|||
|
// get the requested path
|
|||
|
$it = $this->getPageRequestPath();
|
|||
|
if($it === false) return null;
|
|||
|
|
|||
|
// check if there are index files in the request
|
|||
|
if(strpos($it, '/index.') !== false && preg_match('{/index\.(php|html?)$}', $it, $matches)) {
|
|||
|
// if request is to index.php|htm|html, make note of it to determine if we can redirect later
|
|||
|
$hasIndexFile = true;
|
|||
|
} else if(strpos($this->dirtyURL, 'index.php') !== false && strpos($it, 'index.php') === false) {
|
|||
|
// if request contains index.php and the request path ($it) does not, force redirect to correct version
|
|||
|
if(preg_match('!/index\.php$!', parse_url($this->dirtyURL, PHP_URL_PATH))) $forceRedirect = true;
|
|||
|
}
|
|||
|
|
|||
|
// check if request is for a secure pagefile
|
|||
|
if($this->pagefileSecurePossible($it)) {
|
|||
|
$page = $this->checkRequestFile($it);
|
|||
|
if(is_object($page)) {
|
|||
|
$this->responseType = self::responseTypeFile;
|
|||
|
return $page; // Page or NullPage
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// check for pagination segment
|
|||
|
if($this->checkPageNumPath($it)) {
|
|||
|
// path has a pagination prefix and number in it,
|
|||
|
// populated to $this->pageNumPrefix and $this->pageNumPath
|
|||
|
$page = null;
|
|||
|
} else {
|
|||
|
// no pagination number, see if it already resolves to a Page
|
|||
|
$page = $this->pagesGet($it);
|
|||
|
}
|
|||
|
|
|||
|
if($page && $page->id) {
|
|||
|
// path resolves to page with NO pageNum or urlSegments present
|
|||
|
if($forceRedirect) {
|
|||
|
// index.php in URL redirect to actual page URL
|
|||
|
$this->redirectURL = $page->url;
|
|||
|
} else if($page->id > 1) {
|
|||
|
// trailing slash vs. non trailing slash, enforced if not homepage
|
|||
|
// redirect to proper trailed slash version if incorrect version is present.
|
|||
|
// note: this section only executed if no URL segments or page numbers were present
|
|||
|
$hasTrailingSlash = substr($it, -1) == '/';
|
|||
|
$slashUrls = $page->template->slashUrls;
|
|||
|
if((!$hasTrailingSlash && $slashUrls !== 0) || ($hasTrailingSlash && $slashUrls === 0)) {
|
|||
|
$this->redirectURL = $page->url;
|
|||
|
}
|
|||
|
}
|
|||
|
return $page;
|
|||
|
}
|
|||
|
|
|||
|
// check for globally unique page which can redirect
|
|||
|
$name = trim($it, '/');
|
|||
|
if($name && strpos($name, '/') === false && $sanitizer->pageNameUTF8($name) === $name) {
|
|||
|
$page = $this->pagesGet($name, 'name', 'status=' . Page::statusUnique);
|
|||
|
// if found, redirect to globally unique page
|
|||
|
if($page && $page->viewable()) $this->redirectURL = $page->url;
|
|||
|
}
|
|||
|
|
|||
|
// populate request path to class as other methods will now use it
|
|||
|
$this->requestPath = $it;
|
|||
|
|
|||
|
// check if path with URL segments can resolve to a page
|
|||
|
$urlSegments = array();
|
|||
|
if(!$page) $page = $this->getPageUrlSegments($it, $urlSegments);
|
|||
|
|
|||
|
// if URL segments and/or page numbers are present and not allowed then abort
|
|||
|
if($page && count($urlSegments) && !$this->checkUrlSegments($urlSegments, $page)) {
|
|||
|
// found URL segments were checked and found not to be allowed here
|
|||
|
if($hasIndexFile && count($urlSegments) === 1) {
|
|||
|
// index.php|htm|html segments if not used by page can redirect to URL without it
|
|||
|
$forceRedirect = true;
|
|||
|
} else {
|
|||
|
// page with invalid URL segments becomes a 404
|
|||
|
$page = null;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// if no page found for guest user, check if path was in admin and map to admin root
|
|||
|
if(!$page && $this->wire()->user->isGuest()) {
|
|||
|
// this ensures that no admin requests resolve to a 404 and instead show login form
|
|||
|
$adminPath = substr($config->urls->admin, strlen($config->urls->root)-1);
|
|||
|
if(strpos($this->requestPath, $adminPath) === 0) {
|
|||
|
$page = $this->wire()->pages->get($config->adminRootPageID);
|
|||
|
}
|
|||
|
$forceRedirect = false;
|
|||
|
}
|
|||
|
|
|||
|
if($forceRedirect && $page && !$this->redirectURL) {
|
|||
|
$this->redirectURL = $page->url;
|
|||
|
}
|
|||
|
|
|||
|
return $page;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given a path with URL segments, get matching Page and populate given $urlSegments array
|
|||
|
*
|
|||
|
* @param string $path
|
|||
|
* @param array $urlSegments
|
|||
|
* @return null|Page
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function getPageUrlSegments($path, array &$urlSegments) {
|
|||
|
|
|||
|
$numSegments = 0;
|
|||
|
$maxSegments = $this->wire()->config->maxUrlSegments;
|
|||
|
$maxSegments = $maxSegments === null ? 4 : (int) $maxSegments;
|
|||
|
$page = null;
|
|||
|
|
|||
|
// if the page isn't found, then check if a page one path level before exists
|
|||
|
// this loop allows for us to have both a urlSegment and a pageNum
|
|||
|
while(!$page && $numSegments < $maxSegments) {
|
|||
|
$path = rtrim($path, '/');
|
|||
|
$pos = strrpos($path, '/') + 1;
|
|||
|
$urlSegment = substr($path, $pos);
|
|||
|
$urlSegments[$numSegments] = $urlSegment;
|
|||
|
$path = substr($path, 0, $pos); // $path no longer includes the urlSegment
|
|||
|
$page = $this->pagesGet($path);
|
|||
|
$numSegments++;
|
|||
|
}
|
|||
|
|
|||
|
return $page;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get the requested path
|
|||
|
*
|
|||
|
* @return bool|string Return false on fail or path on success
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function getPageRequestPath() {
|
|||
|
|
|||
|
$config = $this->wire()->config;
|
|||
|
$sanitizer = $this->wire()->sanitizer;
|
|||
|
|
|||
|
/** @var string $shit Dirty URL */
|
|||
|
/** @var string $it Clean URL */
|
|||
|
|
|||
|
if(isset($_GET['it'])) {
|
|||
|
// normal request
|
|||
|
$shit = trim($_GET['it']);
|
|||
|
|
|||
|
} else if(isset($_SERVER['REQUEST_URI'])) {
|
|||
|
// abnormal request, something about request URL made .htaccess skip it, or index.php called directly
|
|||
|
$rootUrl = $config->urls->root;
|
|||
|
$shit = trim($_SERVER['REQUEST_URI']);
|
|||
|
if(strpos($shit, '?') !== false) list($shit,) = explode('?', $shit, 2);
|
|||
|
if($rootUrl != '/') {
|
|||
|
if(strpos($shit, $rootUrl) === 0) {
|
|||
|
// remove root URL from request
|
|||
|
$shit = substr($shit, strlen($rootUrl) - 1);
|
|||
|
} else {
|
|||
|
// request URL outside of our root directory
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
$shit = '/';
|
|||
|
}
|
|||
|
|
|||
|
if($shit === '/') {
|
|||
|
$it = '/';
|
|||
|
} else {
|
|||
|
$it = preg_replace('{[^-_./a-zA-Z0-9]}', '', $shit); // clean
|
|||
|
}
|
|||
|
|
|||
|
unset($_GET['it']);
|
|||
|
|
|||
|
if($shit !== $it) {
|
|||
|
// sanitized URL does not match requested URL
|
|||
|
if($config->pageNameCharset == 'UTF8') {
|
|||
|
// test for extended page name URL
|
|||
|
$it = $sanitizer->pagePathNameUTF8($shit);
|
|||
|
}
|
|||
|
if($shit !== $it) {
|
|||
|
// if still does not match then fail
|
|||
|
return false;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$maxUrlDepth = $config->maxUrlDepth;
|
|||
|
if($maxUrlDepth > 0 && substr_count($it, '/') > $config->maxUrlDepth) return false;
|
|||
|
|
|||
|
if(!isset($it[0]) || $it[0] != '/') $it = "/$it";
|
|||
|
if(strpos($it, '//') !== false) return false;
|
|||
|
|
|||
|
return $it;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check if given path has a page/pagination number and return it if so (return 0 if not)
|
|||
|
*
|
|||
|
* @param string $path
|
|||
|
* @return int
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkPageNumPath($path) {
|
|||
|
|
|||
|
$hasPrefix = false;
|
|||
|
$pageNumUrlPrefixes = $this->pageNumUrlPrefixes();
|
|||
|
|
|||
|
foreach($pageNumUrlPrefixes as $prefix) {
|
|||
|
if(strpos($path, '/' . $prefix) !== false) {
|
|||
|
$hasPrefix = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
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
|
|||
|
$this->pageNumPrefix = $matches[1];
|
|||
|
$this->pageNum = (int) $matches[2];
|
|||
|
return $this->pageNum;
|
|||
|
}
|
|||
|
|
|||
|
return 0;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check if the requested URL is to a secured page file
|
|||
|
*
|
|||
|
* This function sets $this->requestFile when it finds one.
|
|||
|
* Returns Page when a pagefile was found and matched to a page.
|
|||
|
* Returns NullPage when request should result in a 404.
|
|||
|
* Returns true, and updates $it, when pagefile was found using old/deprecated method.
|
|||
|
* Returns false when none found.
|
|||
|
*
|
|||
|
* @param string $it Request URL
|
|||
|
* @return bool|Page|NullPage
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkRequestFile(&$it) {
|
|||
|
|
|||
|
$config = $this->wire()->config;
|
|||
|
$pages = $this->wire()->pages;
|
|||
|
|
|||
|
// request with url to root (applies only if site runs from subdirectory)
|
|||
|
$itRoot = rtrim($config->urls->root, '/') . $it;
|
|||
|
|
|||
|
// check for secured filename, method 1: actual file URL, minus leading "." or "-"
|
|||
|
if(strpos($itRoot, $config->urls->files) === 0) {
|
|||
|
// request is for file in site/assets/files/...
|
|||
|
$idAndFile = substr($itRoot, strlen($config->urls->files));
|
|||
|
|
|||
|
// matching in $idAndFile: 1234/file.jpg, 1/2/3/4/file.jpg, 1234/subdir/file.jpg, 1/2/3/4/subdir/file.jpg, etc.
|
|||
|
if(preg_match('{^(\d[\d\/]*)/([-_a-zA-Z0-9][-_./a-zA-Z0-9]+)$}', $idAndFile, $matches) && strpos($matches[2], '.')) {
|
|||
|
// request is consistent with those that would match to a file
|
|||
|
$idPath = trim($matches[1], '/');
|
|||
|
$file = trim($matches[2], '.');
|
|||
|
|
|||
|
if(!strpos($file, '.')) return $pages->newNullPage();
|
|||
|
|
|||
|
if(!ctype_digit("$idPath")) {
|
|||
|
// extended paths where id separated by slashes, i.e. 1/2/3/4
|
|||
|
if($config->pagefileExtendedPaths) {
|
|||
|
// allow extended paths
|
|||
|
$idPath = str_replace('/', '', $matches[1]);
|
|||
|
if(!ctype_digit("$idPath")) return $pages->newNullPage();
|
|||
|
} else {
|
|||
|
// extended paths not allowed
|
|||
|
return $pages->newNullPage();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(strpos($file, '/') !== false) {
|
|||
|
// file in subdirectory (for instance ProDrafts uses subdirectories for draft files)
|
|||
|
list($subdir, $file) = explode('/', $file, 2);
|
|||
|
|
|||
|
if(strpos($file, '/') !== false) {
|
|||
|
// there is more than one subdirectory, which we do not allow
|
|||
|
return $pages->newNullPage();
|
|||
|
|
|||
|
} else if(strpos($subdir, '.') !== false || strlen($subdir) > 128) {
|
|||
|
// subdirectory has a "." in it or subdir length is too long
|
|||
|
return $pages->newNullPage();
|
|||
|
|
|||
|
} else if(!preg_match('/^[a-zA-Z0-9][-_a-zA-Z0-9]+$/', $subdir)) {
|
|||
|
// subdirectory not in expected format
|
|||
|
return $pages->newNullPage();
|
|||
|
}
|
|||
|
|
|||
|
$file = trim($file, '.');
|
|||
|
$this->requestFile = "$subdir/$file";
|
|||
|
|
|||
|
} else {
|
|||
|
// file without subdirectory
|
|||
|
$this->requestFile = $file;
|
|||
|
}
|
|||
|
|
|||
|
return $pages->get((int) $idPath); // Page or NullPage
|
|||
|
|
|||
|
} else {
|
|||
|
// request was to something in /site/assets/files/ but we don't recognize it
|
|||
|
// tell caller that this should be a 404
|
|||
|
return $pages->newNullPage();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// check for secured filename: method 2 (deprecated), used only if $config->pagefileUrlPrefix is defined
|
|||
|
$filePrefix = $config->pagefileUrlPrefix;
|
|||
|
if($filePrefix && strpos($it, '/' . $filePrefix) !== false) {
|
|||
|
if(preg_match('{^(.*/)' . $filePrefix . '([-_.a-zA-Z0-9]+)$}', $it, $matches) && strpos($matches[2], '.')) {
|
|||
|
$it = $matches[1];
|
|||
|
$this->requestFile = $matches[2];
|
|||
|
return true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Identify and populate URL segments and page numbers
|
|||
|
*
|
|||
|
* @param array $urlSegments URL segments as found in getPage()
|
|||
|
* @param Page $page
|
|||
|
* @return bool Returns false if URL segments found and aren't allowed
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkUrlSegments(array $urlSegments, Page $page) {
|
|||
|
|
|||
|
if(!count($urlSegments)) return true;
|
|||
|
|
|||
|
$input = $this->wire()->input;
|
|||
|
$lastSegment = reset($urlSegments);
|
|||
|
$urlSegments = array_reverse($urlSegments);
|
|||
|
$pageNum = 1;
|
|||
|
$template = $page->template;
|
|||
|
|
|||
|
// check if the last urlSegment is setting a page number and that page numbers are allowed
|
|||
|
if(!is_null($this->pageNum) && $lastSegment === "$this->pageNumPrefix$this->pageNum" && $template->allowPageNum) {
|
|||
|
// meets the requirements for a page number: last portion of URL and starts with 'page'
|
|||
|
$pageNum = (int) $this->pageNum;
|
|||
|
if($pageNum < 1) $pageNum = 1;
|
|||
|
if($pageNum > 1 && !$this->wire()->user->isLoggedin()) {
|
|||
|
$maxPageNum = $this->wire()->config->maxPageNum;
|
|||
|
if(!$maxPageNum) $maxPageNum = 999;
|
|||
|
if($pageNum > $maxPageNum) return false;
|
|||
|
}
|
|||
|
$page->setQuietly('pageNum', $pageNum); // backwards compatibility
|
|||
|
$input->setPageNum($pageNum);
|
|||
|
array_pop($urlSegments);
|
|||
|
}
|
|||
|
|
|||
|
// return false if URL segments aren't allowed with this page template
|
|||
|
if($template->name !== 'admin' && count($urlSegments)) {
|
|||
|
if(!$this->isAllowedUrlSegment($page, $urlSegments)) return false;
|
|||
|
}
|
|||
|
|
|||
|
// now set the URL segments to the $input API variable
|
|||
|
$cnt = 1;
|
|||
|
foreach($urlSegments as $urlSegment) {
|
|||
|
if($cnt == 1) $page->setQuietly('urlSegment', $urlSegment); // backwards compatibility
|
|||
|
$input->setUrlSegment($cnt, $urlSegment);
|
|||
|
$cnt++;
|
|||
|
}
|
|||
|
|
|||
|
if($pageNum > 1 || count($urlSegments)) {
|
|||
|
$hasTrailingSlash = substr($this->requestPath, -1) == '/';
|
|||
|
// $url=URL with urlSegments and no trailing slash
|
|||
|
// $url = rtrim(rtrim($page->url, '/') . '/' . $this->input->urlSegmentStr, '/');
|
|||
|
$redirectPath = null;
|
|||
|
if($pageNum > 1 && $template->slashPageNum) {
|
|||
|
if($template->slashPageNum == 1 && !$hasTrailingSlash) {
|
|||
|
// enforce trailing slash on page numbers
|
|||
|
//$this->redirectURL = "$url/$this->pageNumPrefix$pageNum/";
|
|||
|
$redirectPath = "/$this->pageNumPrefix$pageNum/";
|
|||
|
} else if($template->slashPageNum == -1 && $hasTrailingSlash) {
|
|||
|
// enforce NO trailing slash on page numbers
|
|||
|
// $this->redirectURL = "$url/$this->pageNumPrefix$pageNum";
|
|||
|
$redirectPath = "/$this->pageNumPrefix$pageNum";
|
|||
|
}
|
|||
|
|
|||
|
} else if(count($urlSegments) && $template->slashUrlSegments) {
|
|||
|
if($template->slashUrlSegments == 1 && !$hasTrailingSlash) {
|
|||
|
// enforce trailing slash with URL segments
|
|||
|
// $this->redirectURL = "$url/";
|
|||
|
$redirectPath = "/";
|
|||
|
|
|||
|
} else if($template->slashUrlSegments == -1 && $hasTrailingSlash) {
|
|||
|
// enforce no trailing slash with URL segments
|
|||
|
// $this->redirectURL = $url;
|
|||
|
$redirectPath = "";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if($redirectPath !== null) {
|
|||
|
// redirect will occur to a proper slash format
|
|||
|
$modules = $this->wire()->modules;
|
|||
|
if($modules->isInstalled('LanguageSupportPageNames')) {
|
|||
|
// ensure that LanguageSupportPageNames reaches a ready() state, since
|
|||
|
// it can modify the output of $page->url (if installed)
|
|||
|
$this->wire('page', $page);
|
|||
|
$modules->get('LanguageSupportPageNames')->ready();
|
|||
|
}
|
|||
|
$this->redirectURL = rtrim(rtrim($page->url, '/') . '/' . $input->urlSegmentStr, '/') . $redirectPath;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Is the given URL segment allowed according to the page template's settings?
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @param string|array $segment May be a single segment or path of segments
|
|||
|
* @return bool
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function isAllowedUrlSegment(Page $page, $segment) {
|
|||
|
|
|||
|
$urlSegments = $page->template->urlSegments();
|
|||
|
|
|||
|
if(is_array($urlSegments)) {
|
|||
|
// only specific URL segments are allowed
|
|||
|
if(is_array($segment)) $segment = implode('/', $segment);
|
|||
|
$segment = trim($segment, '/');
|
|||
|
$allowed = false;
|
|||
|
foreach($urlSegments as $allowedSegment) {
|
|||
|
if(strpos($allowedSegment, 'regex:') === 0) {
|
|||
|
$regex = '{' . trim(substr($allowedSegment, 6)) . '}';
|
|||
|
$allowed = preg_match($regex, $segment);
|
|||
|
|
|||
|
} else if($segment === $allowedSegment) {
|
|||
|
$allowed = true;
|
|||
|
}
|
|||
|
if($allowed) break;
|
|||
|
}
|
|||
|
return $allowed;
|
|||
|
|
|||
|
} else if($urlSegments > 0) {
|
|||
|
// all URL segments are allowed
|
|||
|
return true;
|
|||
|
|
|||
|
} else {
|
|||
|
// no URL segments are allowed
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check that the current user has access to the page and return it
|
|||
|
*
|
|||
|
* If the user doesn't have access, then a login Page or NULL (for 404) is returned instead.
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @return Page|null
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkAccess($page) {
|
|||
|
|
|||
|
$user = $this->wire()->user;
|
|||
|
|
|||
|
if($this->requestFile) {
|
|||
|
// if a file was requested, we still allow view even if page doesn't have template file
|
|||
|
if($page->viewable($this->requestFile) === false) return null;
|
|||
|
if($page->viewable(false)) return $page;
|
|||
|
// if($page->editable()) return $page;
|
|||
|
if($this->checkAccessDelegated($page)) return $page;
|
|||
|
if($page->status < Page::statusUnpublished && $user->hasPermission('page-view', $page)) return $page;
|
|||
|
|
|||
|
} else if($page->viewable()) {
|
|||
|
return $page;
|
|||
|
|
|||
|
} else if($page->parent_id && $page->parent->template->name === 'admin' && $page->parent->viewable()) {
|
|||
|
// check for special case in admin when Process::executeSegment() collides with page name underneath
|
|||
|
// example: a role named "edit" is created and collides with ProcessPageType::executeEdit()
|
|||
|
$input = $this->wire()->input;
|
|||
|
if($user->isLoggedin() && $page->editable() && !strlen($input->urlSegmentStr())) {
|
|||
|
$input->setUrlSegment(1, $page->name);
|
|||
|
return $page->parent;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$accessTemplate = $page->getAccessTemplate();
|
|||
|
$redirectLogin = $accessTemplate ? $accessTemplate->redirectLogin : false;
|
|||
|
|
|||
|
// if we won’t be presenting a login form then $page converts to null (404)
|
|||
|
if(!$redirectLogin) return null;
|
|||
|
|
|||
|
$config = $this->wire()->config;
|
|||
|
$disallowIDs = array($config->trashPageID); // don't allow login redirect for these pages
|
|||
|
$loginRequestURL = $this->redirectURL;
|
|||
|
$loginPageID = $config->loginPageID;
|
|||
|
$requestPage = $page;
|
|||
|
$session = $this->wire()->session;
|
|||
|
$input = $this->wire()->input;
|
|||
|
$ns = 'ProcessPageView';
|
|||
|
|
|||
|
if($page->id && in_array($page->id, $disallowIDs)) {
|
|||
|
// don't allow login redirect when matching disallowIDs
|
|||
|
$page = null;
|
|||
|
|
|||
|
} else if(ctype_digit("$redirectLogin")) {
|
|||
|
// redirect login provided as a page ID
|
|||
|
$redirectLogin = (int) $redirectLogin;
|
|||
|
// if given ID 1 then this maps to the admin login page
|
|||
|
if($redirectLogin === 1) $redirectLogin = $loginPageID;
|
|||
|
$page = $this->pages->get($redirectLogin);
|
|||
|
|
|||
|
} else {
|
|||
|
// redirect login provided as a URL, optionally with an {id} tag for requested page ID
|
|||
|
$redirectLogin = str_replace('{id}', $page->id, $redirectLogin);
|
|||
|
$this->redirectURL = $redirectLogin;
|
|||
|
}
|
|||
|
|
|||
|
if(empty($loginRequestURL)) {
|
|||
|
$loginRequestURL = $session->getFor($ns, 'loginRequestURL');
|
|||
|
}
|
|||
|
|
|||
|
// in case anything after login needs to know the originally requested page/URL
|
|||
|
if(empty($loginRequestURL) && $page && $requestPage && $requestPage->id) {
|
|||
|
if($requestPage->id != $loginPageID && !$input->get('loggedout')) {
|
|||
|
$loginRequestURL = $input->url(array('page' => $requestPage));
|
|||
|
if(!empty($_GET)) {
|
|||
|
$queryString = $input->queryStringClean(array(
|
|||
|
'maxItems' => 10,
|
|||
|
'maxLength' => 500,
|
|||
|
'maxNameLength' => 20,
|
|||
|
'maxValueLength' => 200,
|
|||
|
'sanitizeName' => 'fieldName',
|
|||
|
'sanitizeValue' => 'name',
|
|||
|
'entityEncode' => false,
|
|||
|
));
|
|||
|
if(strlen($queryString)) $loginRequestURL .= "?$queryString";
|
|||
|
}
|
|||
|
$session->setFor($ns, 'loginRequestPageID', $requestPage->id);
|
|||
|
$session->setFor($ns, 'loginRequestURL', $loginRequestURL);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return $page;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Check access to a delegated page (like a repeater)
|
|||
|
*
|
|||
|
* Note: this should move to PagePermissions.module or FieldtypeRepeater.module
|
|||
|
* if a similar check is needed somewhere else in the core.
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @return Page|null|bool
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkAccessDelegated(Page $page) {
|
|||
|
if(strpos($page->template->name, 'repeater_') == 0) {
|
|||
|
if(!$this->wire('modules')->isInstalled('FieldtypeRepeater')) return false;
|
|||
|
$fieldName = substr($page->template->name, strpos($page->template->name, '_') + 1); // repeater_(fieldName)
|
|||
|
if(!$fieldName) return false;
|
|||
|
$field = $this->wire('fields')->get($fieldName);
|
|||
|
if(!$field) return false;
|
|||
|
$forPageID = substr($page->parent->name, strrpos($page->parent->name, '-') + 1); // for-page-(id)
|
|||
|
$forPage = $this->wire('pages')->get((int) $forPageID);
|
|||
|
// delegate viewable check to the page the repeater lives on
|
|||
|
if($forPage->id) {
|
|||
|
if($forPage->viewable($field)) return $page;
|
|||
|
if(strpos($forPage->template->name, 'repeater_') === 0) {
|
|||
|
// go recursive for nested repeaters
|
|||
|
$forPage = $this->checkAccessDelegated($forPage);
|
|||
|
if($forPage && $forPage->id) return $forPage;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* If the template requires a different protocol than what is here, then redirect to it.
|
|||
|
*
|
|||
|
* This method just silently sets the $this->redirectURL var if a redirect is needed.
|
|||
|
* Note this does not work if GET vars are present in the URL -- they will be lost in the redirect.
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function checkProtocol($page) {
|
|||
|
|
|||
|
/** @var Config $config */
|
|||
|
$config = $this->wire('config');
|
|||
|
$requireHTTPS = $page->template->https;
|
|||
|
if($requireHTTPS == 0 || $config->noHTTPS) return; // neither HTTP or HTTPS required
|
|||
|
|
|||
|
$isHTTPS = $config->https;
|
|||
|
$scheme = '';
|
|||
|
|
|||
|
if($requireHTTPS == -1 && $isHTTPS) {
|
|||
|
// HTTP required: redirect to HTTP non-secure version
|
|||
|
$scheme = "http";
|
|||
|
|
|||
|
} else if($requireHTTPS == 1 && !$isHTTPS) {
|
|||
|
// HTTPS required: redirect to HTTPS secure version
|
|||
|
$scheme = "https";
|
|||
|
}
|
|||
|
|
|||
|
if(!$scheme) return;
|
|||
|
|
|||
|
if($this->redirectURL) {
|
|||
|
if(strpos($this->redirectURL, '://') !== false) {
|
|||
|
$url = str_replace(array('http://', 'https://'), "$scheme://", $this->redirectURL);
|
|||
|
} else {
|
|||
|
$url = "$scheme://$config->httpHost$this->redirectURL";
|
|||
|
}
|
|||
|
} else {
|
|||
|
$url = "$scheme://$config->httpHost$page->url";
|
|||
|
}
|
|||
|
$input = $this->wire('input');
|
|||
|
|
|||
|
if($this->redirectURL) {
|
|||
|
// existing redirectURL will already have segments/page numbers as needed
|
|||
|
|
|||
|
} else {
|
|||
|
$urlSegmentStr = $input->urlSegmentStr;
|
|||
|
if(strlen($urlSegmentStr) && $page->template->urlSegments) {
|
|||
|
$url = rtrim($url, '/') . '/' . $urlSegmentStr;
|
|||
|
if($page->template->slashUrlSegments) {
|
|||
|
// use defined setting for trailing slash
|
|||
|
if($page->template->slashUrlSegments == 1) $url .= '/';
|
|||
|
} else {
|
|||
|
// use whatever the request came with
|
|||
|
if(substr($this->requestPath, -1) == '/') $url .= '/';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$pageNum = $input->pageNum;
|
|||
|
if($pageNum > 1 && $page->template->allowPageNum) {
|
|||
|
$prefix = $this->pageNumPrefix ? $this->pageNumPrefix : $this->wire('config')->pageNumUrlPrefix;
|
|||
|
if(!$prefix) $prefix = 'page';
|
|||
|
$url = rtrim($url, '/') . "/$prefix$pageNum";
|
|||
|
if($page->template->slashPageNum) {
|
|||
|
// defined setting for trailing slash
|
|||
|
if($page->template->slashPageNum == 1) $url .= '/';
|
|||
|
} else {
|
|||
|
// use whatever setting the URL came with
|
|||
|
if(substr($this->requestPath, '-1') == '/') $url .= '/';
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
$this->redirectURL = $url;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Get Page from $pages via path (or other property) or return null if it does not exist
|
|||
|
*
|
|||
|
* @param string $value Value to match
|
|||
|
* @param string $property Property being matched (default='path')
|
|||
|
* @param string $selector Additional selector to apply (default='status<9999999');
|
|||
|
* @return null|Page
|
|||
|
* @since 3.0.168
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function pagesGet($value, $property = 'path', $selector = 'status<9999999') {
|
|||
|
if(!is_int($value)) $value = $this->wire()->sanitizer->selectorValue($value, array(
|
|||
|
'maxLength' => 2048,
|
|||
|
'maxBytes' => 6144,
|
|||
|
'allowArray' => false,
|
|||
|
'allowSpace' => false,
|
|||
|
'blacklist' => array(',', "'"),
|
|||
|
));
|
|||
|
$page = $this->wire()->pages->get("$property=$value, $selector");
|
|||
|
return $page->id ? $page : null;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Passthru a file for a non-public page
|
|||
|
*
|
|||
|
* If the page is public, then it just does a 301 redirect to the file.
|
|||
|
*
|
|||
|
* @param Page $page
|
|||
|
* @param string $basename
|
|||
|
* @param array $options
|
|||
|
* @throws Wire404Exception
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function ___sendFile($page, $basename, array $options = array()) {
|
|||
|
|
|||
|
$err = 'File not found';
|
|||
|
|
|||
|
if(!$page->hasFilesPath()) {
|
|||
|
throw new Wire404Exception($err, Wire404Exception::codeFile);
|
|||
|
}
|
|||
|
|
|||
|
$filename = $page->filesPath() . $basename;
|
|||
|
|
|||
|
if(!file_exists($filename)) {
|
|||
|
throw new Wire404Exception($err, Wire404Exception::codeFile);
|
|||
|
}
|
|||
|
|
|||
|
if(!$page->secureFiles()) {
|
|||
|
// if file is not secured, redirect to it
|
|||
|
// (potentially deprecated, only necessary for method 2 in checkRequestFile)
|
|||
|
$this->redirect($page->filesManager->url() . $basename);
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// options for WireHttp::sendFile
|
|||
|
$defaults = array('exit' => false, 'limitPath' => $page->filesPath());
|
|||
|
$options = array_merge($defaults, $options);
|
|||
|
|
|||
|
$this->wire()->files->send($filename, $options);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Called when a page is not found, sends 404 header, and displays the configured 404 page instead.
|
|||
|
*
|
|||
|
* Method is hookable, for instance if you wanted to log 404s. When hooking this method note that it
|
|||
|
* must be hooked sometime before the ready state.
|
|||
|
*
|
|||
|
* @param Page|null $page Page that was found if applicable (like if user didn't have permission or $page's template threw the 404). If not applicable then NULL will be given instead.
|
|||
|
* @param string $url The URL that the request originated from (like $_SERVER['REQUEST_URI'] but already sanitized)
|
|||
|
* @param bool $triggerReady Whether or not the ready() hook should be triggered (default=false)
|
|||
|
* @param string $reason Reason why 404 occurred, for debugging purposes (en text)
|
|||
|
* @param WireException|Wire404Exception $exception Exception that was thrown or that indicates details of error
|
|||
|
* @throws WireException
|
|||
|
* @return string
|
|||
|
*/
|
|||
|
protected function ___pageNotFound($page, $url, $triggerReady = false, $reason = '', $exception = null) {
|
|||
|
|
|||
|
if(!$exception) {
|
|||
|
// create exception but do not throw
|
|||
|
$exception = new Wire404Exception($reason, Wire404Exception::codeNonexist);
|
|||
|
}
|
|||
|
|
|||
|
$this->failed($exception, $reason, $page, $url);
|
|||
|
$this->responseType = self::responseTypeError;
|
|||
|
$this->header404();
|
|||
|
|
|||
|
$page = $this->http404Page();
|
|||
|
if($page->id) {
|
|||
|
$this->wire('page', $page);
|
|||
|
if($triggerReady) $this->ready();
|
|||
|
return $page->render();
|
|||
|
} else {
|
|||
|
return "404 page not found";
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 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
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function header404() {
|
|||
|
static $n = 0;
|
|||
|
if($n) return;
|
|||
|
$http = new WireHttp();
|
|||
|
$this->wire($http);
|
|||
|
$http->sendStatusHeader(404);
|
|||
|
$n++;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Perform redirect
|
|||
|
*
|
|||
|
* @param string $url
|
|||
|
* @param bool $permanent
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function redirect($url, $permanent = true) {
|
|||
|
$session = $this->wire()->session;
|
|||
|
$this->setResponseType(self::responseTypeRedirect);
|
|||
|
if($permanent) {
|
|||
|
$session->redirect($url);
|
|||
|
} else {
|
|||
|
$session->location($url);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Return the response type for this request, as one of the responseType constants
|
|||
|
*
|
|||
|
* @return int
|
|||
|
*
|
|||
|
*/
|
|||
|
public function getResponseType() {
|
|||
|
return $this->responseType;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set the response type for this request, see responseType constants in this class
|
|||
|
*
|
|||
|
* @param int $responseType
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setResponseType($responseType) {
|
|||
|
$this->responseType = (int) $responseType;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set whether any redirects should be performed after the API ready() call
|
|||
|
*
|
|||
|
* This is used by LanguageSupportPageNames to delay redirects until after http/https schema is determined.
|
|||
|
*
|
|||
|
* @param bool $delayRedirects
|
|||
|
*
|
|||
|
*/
|
|||
|
public function setDelayRedirects($delayRedirects) {
|
|||
|
$this->delayRedirects = $delayRedirects ? true : false;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Are secure pagefiles possible on this system and url?
|
|||
|
*
|
|||
|
* @param string $url
|
|||
|
* @return bool
|
|||
|
* @since 3.0.166
|
|||
|
*
|
|||
|
*/
|
|||
|
protected function pagefileSecurePossible($url) {
|
|||
|
$config = $this->wire()->config;
|
|||
|
|
|||
|
// if URL does not start from root, prepend root
|
|||
|
if(strpos($url, $config->urls->root) !== 0) $url = $config->urls->root . ltrim($url, '/');
|
|||
|
|
|||
|
// if URL is not pointing to the files structure, then this is not a files URL
|
|||
|
if(strpos($url, $config->urls->files) !== 0) return false;
|
|||
|
|
|||
|
// pagefileSecure option is enabled and URL pointing to files
|
|||
|
if($config->pagefileSecure) return true;
|
|||
|
|
|||
|
// check if any templates allow pagefileSecure option
|
|||
|
$allow = false;
|
|||
|
foreach($this->wire()->templates as $template) {
|
|||
|
if(!$template->pagefileSecure) continue;
|
|||
|
$allow = true;
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
// if at least one template supports pagefileSecure option we will return true here
|
|||
|
return $allow;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Given a request path (that does not exist) return the closest parent that does exist
|
|||
|
*
|
|||
|
* CURRENTLY NOT USED (future use)
|
|||
|
*
|
|||
|
* @param string $requestPath Request path
|
|||
|
* @param string $parentPath Optional minimum required parent path
|
|||
|
* @return null|Page Returns found parent on success or null if none found
|
|||
|
* @since 3.0.168
|
|||
|
*
|
|||
|
*/
|
|||
|
private function getClosestParentPage($requestPath, $parentPath = '') {
|
|||
|
|
|||
|
$requestPath = trim($requestPath, '/');
|
|||
|
$parentPath = trim($parentPath, '/');
|
|||
|
|
|||
|
// if request path is not in the required start path then exit early
|
|||
|
if($parentPath !== '') {
|
|||
|
if(stripos("/$requestPath/", "/$parentPath/") !== 0) return null;
|
|||
|
}
|
|||
|
|
|||
|
$sanitizer = $this->wire()->sanitizer;
|
|||
|
$parent = null;
|
|||
|
$path = '';
|
|||
|
|
|||
|
// attempt to match page from beginning of path to find closest parent
|
|||
|
$segments = explode('/', $requestPath);
|
|||
|
|
|||
|
foreach($segments as $segment) {
|
|||
|
$seg = $sanitizer->pageName($segment);
|
|||
|
if($seg !== $segment) break;
|
|||
|
$path .= "/$seg";
|
|||
|
if($parentPath !== '' && $path === "/$parentPath") continue;
|
|||
|
$page = $this->pagesGet($path);
|
|||
|
if(!$page->id) break;
|
|||
|
$parent = $page;
|
|||
|
}
|
|||
|
|
|||
|
if($parentPath !== '' && !$parent) $parent = $this->pagesGet("/$parentPath");
|
|||
|
|
|||
|
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)) $returnValue[$prefix] = $prefix;
|
|||
|
}
|
|||
|
|
|||
|
return $returnValue;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|