From 808ba2ddb3846949e67476c783170be8a61094a6 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 1 Oct 2021 13:43:22 -0400 Subject: [PATCH] Rewrite/refactor ProcessPageView module so that it now uses the new core PagesRequest/PagesPathFinder classes. --- wire/modules/Process/ProcessPageView.module | 953 ++------------------ 1 file changed, 64 insertions(+), 889 deletions(-) diff --git a/wire/modules/Process/ProcessPageView.module b/wire/modules/Process/ProcessPageView.module index b777ad7c..2a81df04 100644 --- a/wire/modules/Process/ProcessPageView.module +++ b/wire/modules/Process/ProcessPageView.module @@ -27,7 +27,7 @@ class ProcessPageView extends Process { return array( 'title' => __('Page View', __FILE__), // getModuleInfo title 'summary' => __('All page views are routed through this Process', __FILE__), // getModuleInfo summary - 'version' => 104, + 'version' => 105, 'permanent' => true, 'permission' => 'page-view', ); @@ -52,54 +52,12 @@ class ProcessPageView extends Process { */ 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 * @@ -118,31 +76,13 @@ class ProcessPageView extends Process { * Construct * */ - public function __construct() { - // no parent call intentional - } + 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 - } + public function init() {} // no parent call intentional /** * Retrieve a page, check access, and render @@ -157,78 +97,81 @@ class ProcessPageView extends Process { $this->responseType = self::responseTypeNormal; $config = $this->wire()->config; + $pages = $this->wire()->pages; + $request = $pages->request(); $timerKey = $config->debug ? 'ProcessPageView.getPage()' : ''; + if($config->usePoweredBy !== null) header('X-Powered-By:' . ($config->usePoweredBy ? ' ProcessWire CMS' : '')); - $this->wire()->pages->setOutputFormatting(true); + $pages->setOutputFormatting(true); if($timerKey) Debug::timer($timerKey); - $page = $this->getPage(); + $page = $request->getPage(); if($timerKey) Debug::saveTimer($timerKey, ($page && $page->id ? $page->path : '')); - if(!$page || !$page->id) return $this->renderNoPage(); - - return $this->renderPage($page); + if($page && $page->id) { + return $this->renderPage($page, $request); + } else { + return $this->renderNoPage($request); + } } + /** * Render Page * * @param Page $page + * @param PagesRequest $request * @return bool|mixed|string * @throws WireException * @since 3.0.173 * */ - protected function renderPage(Page $page) { - + protected function renderPage(Page $page, PagesRequest $request) { + $config = $this->wire()->config; - $page->setOutputFormatting(true); - $_page = $page; + $user = $this->wire()->user; - $page = $this->checkAccess($page); - - if(!$page || $_page->id == $config->http404PageID) { + $page->of(true); + $originalPage = $page; + $page = $request->getPageForUser($page, $user); + + if(!$page || !$page->id || $originalPage->id == $config->http404PageID) { $s = 'access not allowed'; $e = new Wire404Exception($s, Wire404Exception::codePermission); - return $this->pageNotFound($_page, $this->requestPath, true, $s, $e); + return $this->pageNotFound($originalPage, $request->getRequestPath(), true, $s, $e); } - if(!$this->delayRedirects) { - $this->checkProtocol($page); - if($this->redirectURL) $this->redirect($this->redirectURL); - } + if(!$this->delayRedirects) $this->checkForRedirect($request); $this->wire('page', $page); $this->ready(); - $page = $this->wire('page'); // in case anything changed it + $page = $this->wire()->page; // in case anything changed it if($this->delayRedirects) { - $this->checkProtocol($page); - if($this->redirectURL) $this->redirect($this->redirectURL); + if($page !== $originalPage) $request->checkScheme($page); + $this->checkForRedirect($request); } try { + $requestFile = $request->getRequestFile(); - if($this->requestFile) { - + if($requestFile) { $this->responseType = self::responseTypeFile; - $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $this->requestFile)); - $this->sendFile($page, $this->requestFile); + $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $requestFile)); + $this->sendFile($page, $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 + // 404 exception thrown during page render TemplateFile::clearAll(); - return $this->renderNoPage(array( + return $this->renderNoPage($request, array( 'reason404' => '404 thrown during page render', 'exception404' => $e, 'page' => $page, @@ -236,7 +179,7 @@ class ProcessPageView extends Process { )); } catch(\Exception $e) { - // other exception (re-throw non 404 exceptions) + // other exception thrown during page render (re-throw non 404 exceptions) $this->responseType = self::responseTypeError; $this->failed($e, "Thrown during page render", $page); throw $e; @@ -248,13 +191,14 @@ class ProcessPageView extends Process { /** * Render when no page mapped to request URL * + * @param PagesRequest $request * @param array $options * @return array|bool|false|string * @throws WireException * @since 3.0.173 * */ - protected function renderNoPage(array $options = array()) { + protected function renderNoPage(PagesRequest $request, array $options = array()) { $defaults = array( 'allow404' => true, // allow 404 to be thrown? @@ -268,7 +212,10 @@ class ProcessPageView extends Process { $config = $this->wire()->config; $hooks = $this->wire()->hooks; $input = $this->wire()->input; - $requestPath = $this->requestPath; + $pages = $this->wire()->pages; + $requestPath = $request->getRequestPath(); + $pageNum = $request->getPageNum(); + $pageNumPrefix = $request->getPageNumPrefix(); $setPageNum = 0; $pageNumSegment = ''; $page = null; @@ -276,20 +223,20 @@ class ProcessPageView extends Process { $this->setResponseType(self::responseTypeNoPage); - if($this->pageNum > 0 && $this->pageNumPrefix !== null) { + if($pageNum > 0 && $pageNumPrefix !== null) { // there is a pagination segment present in the request path $slash = substr($requestPath, -1) === '/' ? '/' : ''; $requestPath = rtrim($requestPath, '/'); - $pageNumSegment = $this->pageNumPrefix . $this->pageNum; + $pageNumSegment = $pageNumPrefix . $pageNum; if(substr($requestPath, -1 * strlen($pageNumSegment)) === $pageNumSegment) { // remove pagination segment from request path $requestPath = substr($requestPath, 0, -1 * strlen($pageNumSegment)); - $setPageNum = $this->pageNum; + $setPageNum = $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); + $input->setPageNum($pageNum); } else { // not a pagination segment // add the slash back to restore requestPath @@ -355,7 +302,7 @@ class ProcessPageView extends Process { 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); + return $this->renderPage($page, $request); } if($out === false) { @@ -364,8 +311,8 @@ class ProcessPageView extends Process { 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']); + if($page->id == $config->http404PageID) $page = $pages->newNullPage(); + $out = $this->pageNotFound($page, $requestPath, false, $options['reason404'], $options['exception404']); } else { $out = false; } @@ -384,6 +331,17 @@ class ProcessPageView extends Process { return $out; } + + /** + * Check request for redirect and apply it when appropriate + * + * @param PagesRequest $request + * + */ + protected function checkForRedirect(PagesRequest $request) { + $redirectUrl = $request->getRedirectUrl(); + if($redirectUrl) $this->redirect($redirectUrl, $request->getRedirectType()); + } /** * Get and optionally send the content-type header @@ -421,12 +379,12 @@ class ProcessPageView extends Process { */ public function ___executeExternal() { $this->setResponseType(self::responseTypeExternal); - $config = $this->wire('config'); + $config = $this->wire()->config; $config->external = true; if($config->externalPageID) { - $page = $this->wire('pages')->get((int) $config->externalPageID); + $page = $this->wire()->pages->get((int) $config->externalPageID); } else { - $page = $this->wire('pages')->newNullPage(); + $page = $this->wire()->pages->newNullPage(); } $this->wire('page', $page); $this->ready(); @@ -469,688 +427,6 @@ class ProcessPageView extends Process { $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 * @@ -1282,7 +558,7 @@ class ProcessPageView extends Process { protected function redirect($url, $permanent = true) { $session = $this->wire()->session; $this->setResponseType(self::responseTypeRedirect); - if($permanent) { + if($permanent === true || $permanent === 301) { $session->redirect($url); } else { $session->location($url); @@ -1320,107 +596,6 @@ class ProcessPageView extends Process { 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; - } - } +