request()`, i.e. * ~~~~~ * $page = $pages->request()->getPage(); * ~~~~~ * #pw-body * * ProcessWire 3.x, Copyright 2023 by Ryan Cramer * https://processwire.com * * @method Page|NullPage getPage() * @method Page|null getPageForUser(Page $page, User $user) * @method Page|NullPage getClosestPage() * @method Page|string getLoginPageOrUrl(Page $page) * */ class PagesRequest extends Wire { /** * @var Pages * */ protected $pages; /** * @var Config * */ protected $config; /** * @var Page|null|bool Page when found, NullPage when 404, null when not yet known * */ protected $page = null; /** * Closest page to one requested, when getPage() didn’t resolve * * @var null|Page * */ protected $closestPage = null; /** * Page that access was requested to and denied * * @var Page|null * */ protected $requestPage = null; /** * @var array * */ protected $pageInfo = array(); /** * Sanitized path that generated this request * * Set by the getPage() method and passed to the pageNotFound function. * */ protected $requestPath = ''; /** * Processed request path (for identifying if setRequestPath called manually) * * @var string * */ protected $processedPath = ''; /** * 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; /** * Response type codes to response type names * * @var array * */ protected $responseCodeNames = array( 0 => 'unknown', 200 => 'ok', 300 => 'maybeRedirect', 301 => 'permRedirect', 302 => 'tempRedirect', 307 => 'tempRedo', 308 => 'permRedo', 400 => 'badRequest', 401 => 'unauthorized', 403 => 'forbidden', 404 => 'pageNotFound', 405 => 'methodNotAllowed', 414 => 'pathTooLong', ); /** * Response http code * * @var int * */ protected $responseCode = 0; /** * 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 = ''; /** * @var int 301 or 302 * */ protected $redirectType = 0; /** * @var string * */ protected $languageName = ''; /** * Previous value of $_GET[it] when used for something non PW * * @var mixed * */ protected $prevGetIt = null; /** * Optional message provided to setResponseCode() with additional detail * * @var string * */ protected $responseMessage = ''; /*************************************************************************************/ /** * Construct * * @param Pages $pages * */ public function __construct(Pages $pages) { parent::__construct(); $this->pages = $pages; $this->config = $pages->wire()->config; $this->init(); } /** * Initialize * */ protected function init() { $dirtyUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''; if(!strlen($dirtyUrl) && !empty($_SERVER['QUERY_STRING'])) { if(strlen($_SERVER['QUERY_STRING']) < 4096) { $dirtyUrl = '?' . $_SERVER['QUERY_STRING']; } } $this->dirtyUrl = $dirtyUrl; if(!isset($_GET['it'])) return; // check if there is an 'it' var present in the request query string, which we don’t want if((strpos($dirtyUrl, '?it=') !== false || strpos($dirtyUrl, '&it='))) { // the request URL included a user-inserted 'it' variable in query string // force to use path in request url rather than contents of 'it' var list($it, /*query-string*/) = explode('?', $dirtyUrl, 2); $rootUrl = $this->config->urls->root; if(strlen($rootUrl) > 1) { // root url is a subdirectory, like /pwsite/ if(strpos($it, $rootUrl) === 0) { // url begins with that subdirectory, like /pwsite/ // convert '/pwsite/path/to/page/' to just '/path/to/page/' $it = substr($it, strlen($rootUrl) - 1); } else if(strpos(ltrim($it, '/'), ltrim($rootUrl, '/')) === 0) { $it = substr(ltrim($it, '/'), strlen(ltrim($rootUrl, '/'))); } } $it = str_ireplace('index.php', '', $it); $this->prevGetIt = $_GET['it']; $_GET['it'] = $it; } } /** * Set current request page * * @param Page|NullPage|null $page * @return Page|NullPage|null * */ public function setPage($page) { $this->page = $page; $this->dirtyUrl = ''; $input = $this->wire()->input; if($this->prevGetIt === null) { unset($_GET['it']); $input->get->offsetUnset('it'); } else { $_GET['it'] = $this->prevGetIt; $input->get->offsetSet('it', $this->prevGetIt); $this->prevGetIt = null; } return $page; } /** * Get the requested page * * - Populates identified urlSegments or page numbers to $input. * - Returns NullPage for error, call getResponseCode() and/or getResponseMessage() for details. * - Returned page should be validated with getPageForUser() method before rendering it. * - Call getFile() method afterwards to see if request resolved to file managed by returned page. * * @param array $options * @return Page|NullPage * */ public function ___getPage(array $options = array()) { $defaults = array( 'verbose' => false, 'useHistory' => false, // disabled because redundant with hook in PagePathHistory module 'useExcludeRoot' => false, ); $options = empty($options) ? $defaults : array_merge($defaults, $options); // perform this work only once unless reset by setPage or setRequestPath if($this->page && $this->requestPath === $this->processedPath) return $this->page; $input = $this->wire()->input; $languages = $this->wire()->languages; // get the requested path $path = $this->getRequestPath(); // request path is not one ProcessWire is allowed to handle if($path === false) return $this->setPage($this->pages->newNullPage()); // SECURE-PAGEFILES: check if request is for a secure pagefile if($this->pagefileSecurePossibleUrl($path)) { // get Page (success), NullPage (404), false (no file present), true (file present old method) $page = $this->checkRequestFile($path); // can modify $path directly if(is_object($page)) { // Page (success) or NullPage (404) $this->setResponseCode($page->id ? 200 : 404, 'Secure pagefile request'); return $this->setPage($page); } else if($page === false) { // $path is unrelated to /site/assets/files/ } else if($page === true) { // $path was to a file using config.pageFileUrlPrefix prefix method // $this->requestFile is populated and $path is now updated to be // the page path without the filename in it } } // populate request path to class as other methods will now use it $this->setRequestPath($path); // determine if original URL had anything filtered out of path that will suggest a redirect list($dirtyUrl,) = explode('?', "$this->dirtyUrl?", 2); // exclude query string if(stripos($dirtyUrl, 'index.php') !== false && stripos($path, 'index.php') === false) { // force pathFinder to detect a redirect condition without index.php $dirtyUrl = strtolower(rtrim($dirtyUrl, '/')); if(substr("/$dirtyUrl", -10) === '/index.php') $path = rtrim($path, '/') . '/index.php'; } else if(strpos($dirtyUrl, '//') !== false) { // force pathFinder to detect redirect sans double slashes, /page/path// => /page/path/ $path = rtrim($path, '/') . '//'; } // get info about requested path $info = $this->pages->pathFinder()->get($path, $options); $pageId = $info['page']['id']; $this->pageInfo = &$info; $this->languageName = $info['language']['name']; $this->setResponseCode($info['response']); // URL segments if(count($info['urlSegments'])) { $input->setUrlSegments($info['urlSegments']); } // pagination numbers if($info['pageNum'] > 1) { $input->setPageNum($info['pageNum']); $this->pageNum = $info['pageNum']; $this->pageNumPrefix = $info['pageNumPrefix']; } // check if we have matched a page if($pageId) { $page = $this->pages->getOneById($pageId, array( 'template' => $info['page']['templates_id'], 'parent_id' => $info['page']['parent_id'], )); } else { $page = $this->pages->newNullPage(); } $this->requestPage = $page; if($page->id) { if(!empty($info['urlSegments'])) { // the first version of PW populated first URL segment to $page, // this undocumented behavior retained for backwards compatibility $page->setQuietly('urlSegment', $input->urlSegment1); } if(!$this->checkRequestMethod($page)) { // request method not allowed $page = $this->pages->newNullPage(); } } else if($this->responseCode < 300) { // no page ID found but got success code (this should not be possible) $this->setResponseCode(404); } if($this->responseCode === 300) { // 300 “maybe” redirect: page not available in requested language if($languages && $languages->hasPageNames()) { $language = $languages->get($info['language']['name']); $result = $languages->pageNames()->pageNotAvailableInLanguage($page, $language); if(is_array($result)) { // array returned where index 0=301|302, 1=redirect URL $this->setResponseCode($result[0]); $this->setRedirectUrl($result[1], $result[0]); } else if(is_bool($result)) { // bool returned where true=200 (render anyway), false=404 (fail) $this->setResponseCode($result ? 200 : 404); } } else if(!empty($info['redirect'])) { $this->setResponseCode(301); } } // check for redirect if(empty($this->redirectUrl) && $this->responseCode >= 300 && $this->responseCode < 400) { // 301 permRedirect, 302 tempRedirect, 307 or 308 $this->setRedirectPath($info['redirect'], $info['response']); } // check for error if($this->responseCode >= 400) { // 400 badRequest, 401 unauthorized, 403 forbidden, // 404 pageNotFound, 405 methodNotallowed, 414 pathTooLong if(!empty($info['redirect'])) { // pathFinder suggests a redirect may still be possible // currently not implemented } if($page->id) { // if a page was found but with an error code then set the // closestPage property for optional later inspection $this->closestPage = $page; } $page = $this->pages->newNullPage(); } if($page->id) $this->setPage($page); return $page; } /** * Update/get page for given user * * Must be called once the current $user is known as it may change the $page. * Returns NullPage if user lacks access or page out of bounds. * Returns different page if it should be substituted due to lack of access (like login page). * * @param Page $page * @param User $user * @return Page|NullPage * */ public function ___getPageForUser(Page $page, User $user) { $config = $this->config; $isGuest = $user->isGuest(); // if no page found for guest user, check if path was in admin and map to admin root if(!$page->id && $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->pages->get($config->adminRootPageID); $this->redirectUrl = ''; } } $requestPage = $page; // enforce max pagination number when user is not logged in $pageNum = $this->wire()->input->pageNum(); if($pageNum > 1 && $page->id && $isGuest) { $maxPageNum = $config->maxPageNum; if(!$maxPageNum) $maxPageNum = 999; if($this->pageNum > $maxPageNum) { $page = $this->pages->newNullPage(); } } if($page->id) { $page = $this->checkAccess($page, $user); if(is_string($page)) { // redirect URL if(strlen($page)) $this->setRedirectUrl($page, 302); $page = $this->pages->newNullPage(); } else if(!$page || !$page->id) { // 404 $page = $this->pages->newNullPage(); } else { // login Page or Page to render } if($page && $page->id) { // access allowed } else if($user->isLoggedin()) { $this->setResponseCode(403, 'Authenticated user lacks access'); } else { $this->setResponseCode(401, 'User must login for access'); } } if($page->id) { $this->checkScheme($page); $this->setPage($page); $page->of(true); } // if $page was changed as a result of above remember the requested one if($requestPage->id != $page->id) { $this->requestPage = $requestPage; } return $page; } /** * Get closest matching page when getPage() returns an error/NullPage * * This is useful for a 404 page to suggest if maybe the user intended a different page * and give them a link to it. For instance, you might have the following code in the * template file used by your 404 page: * ~~~~~ * echo "
Are you looking for $p->title?
"; * } * ~~~~~ * * @return Page|NullPage * */ public function ___getClosestPage() { return $this->closestPage ? $this->closestPage : $this->pages->newNullPage(); } /** * Get page that was requested * * If this is different from the Page returned by getPageForUser() then it would * represent the page that the user lacked access to. * * @return NullPage|Page * */ public function getRequestPage() { if($this->requestPage) return $this->requestPage; $page = $this->getPage(); if($this->requestPage) return $this->requestPage; // duplication from above intentional return $page; } /** * Get the requested path * * @return bool|string Return false on fail, path on success * */ protected function getRequestPagePath() { $config = $this->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 $this->setResponseCode(404, 'Request URL outside of our web root'); 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 $this->setResponseCode(400, 'Request URL contains invalid/unsupported characters'); return false; } } $maxUrlDepth = $config->maxUrlDepth; if($maxUrlDepth > 0 && substr_count($it, '/') > $config->maxUrlDepth) { $this->setResponseCode(414, 'Request URL exceeds max depth set in $config->maxUrlDepth'); return false; } if(!isset($it[0]) || $it[0] != '/') $it = "/$it"; if(strpos($it, '//') !== false) { $this->setResponseCode(400, 'Request URL contains a blank segment “//”'); return false; } $this->requestPath = $it; $this->processedPath = $it; return $it; } /** * Check if the requested path 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 $path when pagefile was found using deprecated prefix method. * - Returns false when none found. * * @param string $path Request path * @return bool|Page|NullPage * */ protected function checkRequestFile(&$path) { $config = $this->config; $pages = $this->wire()->pages; // request with url to root (applies only if site runs from subdirectory) $url = rtrim($config->urls->root, '/') . $path; // check for secured filename, method 1: actual file URL, minus leading "." or "-" if(strpos($url, $config->urls->files) !== 0) { // if URL is not to files, check if it might be using legacy prefix if($config->pagefileUrlPrefix) return $this->checkRequestFilePrefix($path); // request is not for a file return false; } // request is for file in site/assets/files/... $idAndFile = substr($url, 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. $regex = '{^(\d[\d\/]*)/([-_a-zA-Z0-9][-_./a-zA-Z0-9]+)$}'; if(!preg_match($regex, $idAndFile, $matches) && strpos($matches[2], '.')) { // 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(); } // 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 } /** * Check for secured filename: method 2 (deprecated) * * Used only if $config->pagefileUrlPrefix is defined * * @param string $path * @return bool * */ protected function checkRequestFilePrefix(&$path) { $filePrefix = $this->wire()->config->pagefileUrlPrefix; if(empty($filePrefix)) return false; if(!strpos($path, '/' . $filePrefix)) return false; $regex = '{^(.*/)' . $filePrefix . '([-_.a-zA-Z0-9]+)$}'; if(!preg_match($regex, $path, $matches)) return false; if(!strpos($matches[2], '.')) return false; $path = $matches[1]; $this->requestFile = $matches[2]; return true; } /** * Get login Page object or URL to redirect to for login needed to access given $page * * - When a Page is returned, it is suggested the Page be rendered in this request. * - When a string/URL is returned, it is suggested you redirect to it. * - When null is returned no login page or URL could be identified and 404 should render. * * @param Page|null $page Page that access was requested to or omit to get admin login page * @return string|Page|null Login page object or string w/redirect URL, null if 404 * */ public function ___getLoginPageOrUrl(Page $page = null) { $config = $this->wire()->config; // if no $page given return default login page if($page === null) return $this->pages->get((int) $config->loginPageID); // if NullPage given return URL to default login page if(!$page->id) return $this->pages->get((int) $config->loginPageID)->httpUrl(); // if given page is one that cannot be accessed regardless of login return null if($page->id === $config->trashPageID) return null; // get redirectLogin setting from the template $accessTemplate = $page->getAccessTemplate(); $redirectLogin = $accessTemplate ? $accessTemplate->redirectLogin : false; if(empty($redirectLogin)) { // no setting for template.redirectLogin means 404 return null; } else if(ctype_digit("$redirectLogin")) { // Page ID provided in template.redirectLogin $loginID = (int) $redirectLogin; if($loginID < 2) $loginID = (int) $config->loginPageID; $loginPage = $this->pages->get($loginID); if(!$loginPage->id && $loginID != $config->loginPageID) { $loginPage = $this->pages->get($config->loginPageID); } if(!$loginPage->id) $loginPage = null; return $loginPage; } else if(strlen($redirectLogin)) { // redirect URL provided in template.redirectLogin $redirectUrl = str_replace('{id}', $page->id, $redirectLogin); list($path, $query) = array($redirectUrl, ''); if(strpos($redirectUrl, '?') !== false) list($path, $query) = explode('?', $redirectUrl, 2); if(strlen($path) && strpos($path, '/') === 0 && strpos($path, '//') === false) { // attempt to match to page so we can use URL with scheme and relative to installation url $p = $this->wire()->pages->get($path); if($p->id && $p->viewable()) { $redirectUrl = $p->httpUrl() . ($query ? "?$query" : ""); } } else if(strpos($path, '//') === 0 && strpos($path, '://') === false) { // double slash at beginning force path without checking if it maps to page $redirectUrl = '/' . ltrim($redirectUrl, '/'); } return $redirectUrl; } return null; } /** * 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 * @param User $user * @return Page|string|null Page to render, URL to redirect to, or null for 404 * * */ protected function checkAccess(Page $page, User $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; // false=viewable without template file check if($this->checkAccessDelegated($page)) return $page; // below seems to be redundant with the above $page->viewable(false) check // if($page->status < Page::statusUnpublished && $user->hasPermission('page-view', $page)) return $page; return null; } if($page->viewable()) { // regular page view return $page; } 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; } } // if we reach this point, page is not viewable // get login Page or URL to redirect to for login (Page, string or null) $result = $this->getLoginPageOrUrl($page); // if we won’t be presenting a login or redirect then return null (404) if(empty($result)) return null; return $result; } /** * 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) { return $this->checkAccessRepeater($page); } return null; } /** * Check access to a delegated repeater * * @param Page $page * @return Page|null|bool * */ protected function checkAccessRepeater(Page $page) { 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->pages->get((int) $forPageID); if(!$forPage->id) return null; // delegate viewable check to the page the repeater lives on if($forPage->viewable($field)) return $page; if(strpos($forPage->template->name, 'repeater_') === 0) { // go recursive for nested repeaters $forPage = $this->checkAccessRepeater($forPage); if($forPage && $forPage->id) return $forPage; } return null; } /** * If the template requires a different scheme/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 * */ public function checkScheme(Page $page) { $config = $this->config; $input = $this->wire()->input; $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"; } 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 = (int) $this->pageNum; if($pageNum > 1 && $page->template->allowPageNum) { $prefix = $this->pageNumPrefix ? $this->pageNumPrefix : $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->setRedirectUrl($url, 301); } /** * Check current request method * * @param Page $page * @return bool True if current request method allowed, false if not * */ private function checkRequestMethod(Page $page) { // @todo replace static allowMethods array with template setting like below // $allowMethods = $page->template->get('requestMethods'); // $allowMethods = array('GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'); $allowMethods = array(); // feature disabled until further development if(empty($allowMethods)) return true; // all allowed when none selected $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : ''; if(empty($method)) return true; if(in_array($method, $allowMethods, true)) return true; if($method === 'GET' || $method === 'POST') { if($page->template->name === 'admin') return true; if($page->id == $this->wire()->config->http404PageID) return true; } $this->setResponseCode(405, "Request method $method not allowed by $page->template"); return false; } /** * Are secure pagefiles possible on this system and url? * * @param string $url * @return bool * @since 3.0.166 * */ protected function pagefileSecurePossibleUrl($url) { $config = $this->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; } /** * Set response code and type * * @param int $code * @param string $message Optional message string * */ protected function setResponseCode($code, $message = '') { $this->responseCode = (int) $code; if($message) $this->responseMessage = $message; } /** * Get all possible response code names indexed by http response code * * @return array * */ public function getResponseCodeNames() { return $this->responseCodeNames; } /** * Get response type name for this request * * Returns string, one of: * * - unknown: request not yet analyzed (0) * - ok: successful request (200) * - fileOk: successful file request (200) * - fileNotFound: requested file not found (404) * - maybeRedirect: needs decision about whether to redirect (300) * - permRedirect: permanent redirect (301) * - tempRedirect: temporary redirect (302) * - tempRedo: temporary redirect and redo using same method (307) * - permRedo: permanent redirect and redo using same method (308) * - badRequest: bad request/page path error (400) * - unauthorized: login required (401) * - forbidden: authenticated user lacks access (403) * - pageNotFound: page not found (404) * - methodNotAllowed: request method is not allowed by template (405) * - pathTooLong: path too long or segment too long (414) * * @return string * */ public function getResponseCodeName() { return $this->responseCodeNames[$this->responseCode]; } /** * Get response http code for this request * * Returns integer, one of: * * - 0: unknown/request not yet analyzed * - 200: successful request * - 300: maybe redirect (needs decision) * - 301: permanent redirect * - 302: temporary redirect * - 307: temporary redirect and redo using same method * - 308: permanent redirect and redo using same method * - 400: bad request/page path error * - 401: unauthorized/login required * - 403: forbidden/authenticated user lacks access * - 404: page not found * - 405: method not allowed * - 414: request path too long or segment too long * * @return int * */ public function getResponseCode() { return $this->responseCode; } /** * Set request path * * @param string $requestPath * */ public function setRequestPath($requestPath) { $this->requestPath = $requestPath; } /** * Get request path * * @return string * */ public function getRequestPath() { if(empty($this->requestPath)) $this->requestPath = $this->getRequestPagePath(); return $this->requestPath; } /** * Get request language name * * @return string * */ public function getLanguageName() { return $this->languageName; } /** * Set the redirect path * * @param string $redirectPath * @param int $type 301 or 302 * */ public function setRedirectPath($redirectPath, $type = 301) { $this->redirectUrl = $this->wire()->config->urls->root . ltrim($redirectPath, '/'); $this->redirectType = (int) $type; } /** * Set the redirect URL * * @param string $redirectUrl * @param int $type * */ public function setRedirectUrl($redirectUrl, $type = 301) { $this->redirectUrl = $redirectUrl; $this->redirectType = (int) $type; } /** * Get the redirect URL * * @return string * */ public function getRedirectUrl() { return $this->redirectUrl; } /** * Get the redirect type (0, 301, 302, 307, 308) * * @return int * */ public function getRedirectType() { return $this->redirectType === 300 ? 301 : $this->redirectType; } /** * Get the requested pagination number * * @return null|int * */ public function getPageNum() { return $this->pageNum; } /** * Get the requested pagination number prefix * * @return null|string * */ public function getPageNumPrefix() { return $this->pageNumPrefix; } /** * Get the requested file * * @return string * */ public function getFile() { return $this->requestFile; } /** * Get the requested file (alias of getFile method) * * @return string * */ public function getRequestFile() { return $this->requestFile; } /** * Get message about response only if response was an error, blank otherwise * * @return string * */ public function getResponseError() { return ($this->responseCode >= 400 ? $this->getResponseMessage() : ''); } /** * Set response message * * @param string $message * @param bool $append Append to existing message? * */ public function setResponseMessage($message, $append = false) { if($append && $this->responseMessage) $message = "$this->responseMessage \n$message"; $this->responseMessage = $message; } /** * Get message string about response * * @return string * */ public function getResponseMessage() { $code = $this->getResponseCode(); $value = $this->getResponseCodeName(); if(empty($value)) $value = "unknown"; $value = "$code $value"; if($this->responseMessage) $value .= ": $this->responseMessage"; $attrs = array(); if(!empty($this->pageInfo['urlSegments'])) $attrs[] = 'urlSegments'; if($this->pageNum > 1) $attrs[] = 'pageNum'; if($this->requestFile) $attrs[] = 'file'; return $value; } }