artabro/wire/core/PagesRequest.php
2024-08-27 11:35:37 +02:00

1227 lines
34 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Pages Request
*
* #pw-summary Methods for identifying and loading page from current request URL.
* #pw-body =
* Methods in this class should be accessed from `$pages->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() didnt 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 dont 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 "<h1>404 Page Not Found</h1>";
* $p = $pages->request()->getClosestPage();
* if($p->id) {
* echo "<p>Are you looking for <a href='$p->url'>$p->title</a>?</p>";
* }
* ~~~~~
*
* @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 doesnt 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 wont 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;
}
}