diff --git a/wire/core/PageFinder.php b/wire/core/PageFinder.php index 650c0401..bb692aee 100644 --- a/wire/core/PageFinder.php +++ b/wire/core/PageFinder.php @@ -2587,7 +2587,7 @@ class PageFinder extends Wire { } if($langNames) { $module = $this->languages->pageNames(); - if($module) $selectorValue = $module->updatePath($selectorValue); + if($module) $selectorValue = $module->removeLanguageSegment($selectorValue); } $parts = explode('/', rtrim($selectorValue, '/')); $part = $sanitizer->pageName(array_pop($parts), Sanitizer::toAscii); diff --git a/wire/core/PagesPathFinder.php b/wire/core/PagesPathFinder.php index 5a6f7450..2f8b9aa0 100644 --- a/wire/core/PagesPathFinder.php +++ b/wire/core/PagesPathFinder.php @@ -34,6 +34,8 @@ class PagesPathFinder extends Wire { protected $defaults = array( 'useLanguages' => true, 'useShortcuts' => true, + 'usePagePaths' => true, + 'useGlobalUnique' => true, 'useHistory' => true, 'verbose' => true, ); @@ -63,19 +65,16 @@ class PagesPathFinder extends Wire { protected $result = array(); /** - * Response type codes to response type names - * - * @var array + * @var Template|null * */ - protected $responseTypes = array( - 200 => 'ok', - 301 => 'permRedirect', - 302 => 'tempRedirect', - 400 => 'pagePathError', - 404 => 'pageNotFound', - 414 => 'pathTooLong', - ); + protected $template = null; + + /** + * @var bool|null + * + */ + protected $admin = null; /** * URL part types (for reference) @@ -114,6 +113,8 @@ class PagesPathFinder extends Wire { $this->verbose = $this->options['verbose']; $this->methods = array(); $this->result = $this->getBlankResult(array('request' => $path)); + $this->template = null; + $this->admin = null; if(empty($this->pageNameCharset)) { $this->pageNameCharset = $this->wire()->config->pageNameCharset; @@ -243,9 +244,8 @@ class PagesPathFinder extends Wire { if($result['response'] >= 400) { $page = $this->pages->newNullPage(); } else { - $template = $this->wire()->templates->get($result['page']['templates_id']); $page = $this->pages->getOneById($result['page']['id'], array( - 'template' => $template, + 'template' => $this->template(), 'parent_id' => $result['page']['parent_id'], )); } @@ -398,7 +398,8 @@ class PagesPathFinder extends Wire { /** @var Language $language */ if($language->isDefault()) continue; $nameLanguage = $this->pageNameToUTF8($row["{$key}_name$language->id"]); - $statusByLanguage[$language->name] = (int) $row["{$key}_status$language->id"]; + $statusLanguage = (int) $row["{$key}_status$language->id"]; + $statusByLanguage[$language->name] = $statusLanguage; if($nameLanguage != $nameDefault && $nameLanguage === $name) { if($this->verbose) { $result['parts'][] = array( @@ -427,6 +428,12 @@ class PagesPathFinder extends Wire { $path = '/' . implode('/', $namesByLanguage[$langName]); + if($langName === 'default') { + $result['page']['path'] = $path; + } else { + $result['page']['path'] = '/' . implode('/', $namesByLanguage['default']); + } + if(count($this->useLanguages)) { if($langName === 'default') { $result['language']['status'] = $result['page']['status']; @@ -516,11 +523,15 @@ class PagesPathFinder extends Wire { if(!count($languages)) return null; $firstPart = reset($parts); - $key = array_search($firstPart, $this->languageSegments()); - if($key === false) return $languages->getDefault(); + $languageKey = array_search($firstPart, $this->languageSegments()); - $segment = array_shift($parts); - $language = $languages->get($key); + if($languageKey === false) { + $language = $languages->getDefault(); + $segment = $this->languageSegment('default'); + } else { + $segment = array_shift($parts); + $language = $languages->get($languageKey); + } if(!$language || !$language->id) return null; @@ -528,7 +539,7 @@ class PagesPathFinder extends Wire { $result['language']['segment'] = $segment; $result['language']['name'] = $language->name; - if($this->verbose) { + if($this->verbose && $languageKey !== false) { $result['parts'][] = array( 'type' => 'language', 'value' => $segment, @@ -565,6 +576,7 @@ class PagesPathFinder extends Wire { 'parent_id' => 0, 'templates_id' => 0, 'status' => 0, + 'path' => '', ), 'language' => array( 'name' => '', // intentionally blank @@ -576,6 +588,7 @@ class PagesPathFinder extends Wire { 'urlSegmentStr' => '', 'pageNum' => 1, 'pageNumPrefix' => '', + 'pathAdd' => '', // valid URL segments, page numbers, trailing slash, etc. 'scheme' => '', 'method' => '', ); @@ -596,7 +609,6 @@ class PagesPathFinder extends Wire { */ protected function applyResultTemplate($path) { - $templates = $this->wire()->templates; $config = $this->wire()->config; $fail = false; $result = &$this->result; @@ -605,10 +617,11 @@ class PagesPathFinder extends Wire { $this->applyResultHome(); } - $template = $result['page']['templates_id'] ? $templates->get($result['page']['templates_id']) : null; + $template = $this->template(); $slashUrls = $template ? (int) $template->slashUrls : 0; $useTrailingSlash = $slashUrls ? 1 : -1; // 1=yes, 0=either, -1=no $https = $template ? (int) $template->https : 0; + $appendPath = ''; // populate urlSegmentStr property if applicable if(empty($result['urlSegmentStr']) && !empty($result['urlSegments'])) { @@ -620,7 +633,7 @@ class PagesPathFinder extends Wire { if(strlen($result['urlSegmentStr'])) { if($template && ($template->urlSegments || $template->name === 'admin')) { if($template->isValidUrlSegmentStr($result['urlSegmentStr'])) { - $path = rtrim($path, '/') . "/$result[urlSegmentStr]"; + $appendPath .= "/$result[urlSegmentStr]"; if($result['pageNum'] < 2) $useTrailingSlash = (int) $template->slashUrlSegments; } else { // ERROR: URL segments did not validate @@ -643,7 +656,9 @@ class PagesPathFinder extends Wire { $fail = true; } $segment = $this->pageNumUrlSegment($result['pageNum'], $result['language']['name']); - if(strlen($segment)) $path = rtrim($path, '/') . "/$segment"; + if(strlen($segment)) { + $appendPath .= "/$segment"; + } $useTrailingSlash = (int) $template->slashPageNum; } else { // template does not allow page numbers @@ -653,18 +668,20 @@ class PagesPathFinder extends Wire { } // determine whether path should end with a trailing slash or not - $path = rtrim($path, '/'); if($useTrailingSlash > 0) { // trailing slash required - $path .= '/'; + $appendPath .= '/'; } else if($useTrailingSlash < 0) { // trailing slash disallowed } else if(substr($result['request'], -1) === '/') { // either acceptable, add slash if request had it - $path .= '/'; + $appendPath .= '/'; } - + + $path = rtrim($path, '/') . $appendPath; + $result['redirect'] = $path; + $result['pathAdd'] = $appendPath; // determine if page requires specific https vs. http scheme if($https > 0 && !$config->noHTTPS) { @@ -685,10 +702,10 @@ class PagesPathFinder extends Wire { protected function applyResultHome() { $config = $this->wire()->config; $home = $this->pages->get($config->rootPageID); - $template = $home->template; + $this->template = $home->template; $this->result['page'] = array_merge($this->result['page'], array( 'id' => $config->rootPageID, - 'templates_id' => $template->id, + 'templates_id' => $this->template->id, 'parent_id' => 0, 'status' => $home->status )); @@ -707,30 +724,28 @@ class PagesPathFinder extends Wire { $result = &$this->result; if(!count($this->useLanguages)) return $path; - if(empty($result['language']['name'])) return $path; + if(empty($result['language']['name']) && $path != '/') return $path; + if($this->admin()) return $path; // if there were any non-default language segments, let that dictate the language if(empty($result['language']['segment'])) { - $useLangName = ''; + $useLangName = 'default'; foreach($result['parts'] as $key => $part) { $langName = $part['language']; if(empty($langName) || $langName === 'default') continue; $useLangName = $langName; break; } - if($useLangName) { - $segment = $this->languageSegment($useLangName); - if($segment) $result['language']['segment'] = $segment; - $result['language']['name'] = $useLangName; - } + $segment = $this->languageSegment($useLangName); + if($segment) $result['language']['segment'] = $segment; + $result['language']['name'] = $useLangName; } // prepend the path with the language segment if(!empty($result['language']['segment'])) { - $segment = $result['language']['segment']; - if($path != "/$segment" && strpos($path, "/$segment/") !== 0) { - $path = "/$segment$path"; - } + $path = $this->updatePathForLanguage($path); + $redirect = &$result['redirect']; + if(!empty($redirect)) $redirect = $this->updatePathForLanguage($redirect); } return $path; @@ -784,7 +799,7 @@ class PagesPathFinder extends Wire { protected function finishResult($path) { $result = &$this->result; - $types = &$this->responseTypes; + $types = $this->pages->request()->getResponseCodeNames(); if($path !== false) $path = $this->applyResultLanguage($path); if($path !== false) $path = $this->applyResultTemplate($path); @@ -792,7 +807,8 @@ class PagesPathFinder extends Wire { $response = &$result['response']; $language = &$result['language']; - + $errors = &$result['errors']; + if($response === 404) { // page not found if(empty($result['errors'])) $result['errors']['pageNotFound'] = "Page not found"; @@ -813,30 +829,42 @@ class PagesPathFinder extends Wire { $response = 301; } + if(empty($language['name'])) { + // set language property (likely for non-multi-language install) + $language['name'] = 'default'; + $language['status'] = 1; + + } else if($language['name'] != 'default' && !$language['status'] && $result['page']['id']) { + // page found but not published in language (needs later decision) + $response = 300; // 300 Multiple Choice + $errors['languageOFF'] = "Page not active in request language ($language[name])"; + if(!empty($result['page']['path'])) { + $result['redirect'] = $this->updatePathForLanguage( + rtrim($result['page']['path'], '/') . $result['pathAdd'], + $this->languageSegment('default') + ); + } + } + if(empty($result['type']) && isset($types[$response])) { if($result['response'] === 404 && !empty($result['redirect'])) { // when page found but path not use the 400 response type name w/404 - $result['type'] = 'pagePathError'; + $result['type'] = $types[400]; } else { $result['type'] = $types[$response]; } } - if(empty($language['name'])) { - $language['name'] = 'default'; - $language['status'] = 1; - } - $result['method'] = implode(',', $this->methods); if(!$this->verbose) unset($result['parts']); - if(empty($result['errors'])) { + if(empty($errors)) { // force errors placeholder to end if there aren’t any unset($result['errors']); $result['errors'] = array(); } - + return $result; } @@ -881,6 +909,7 @@ class PagesPathFinder extends Wire { */ protected function getShortcutPagePaths(&$path) { + if(!$this->options['usePagePaths']) return false; $module = $this->pagePathsModule(); if(!$module) return false; @@ -890,17 +919,23 @@ class PagesPathFinder extends Wire { if(!$info) return false; $language = $this->language((int) $info['language_id']); + $path = "/$info[path]"; unset($info['language_id'], $info['path']); - $result['page'] = array_merge($result['page'], $info); + $result['page']['id'] = $info['id']; + $result['page']['status'] = $info['status']; + $result['page']['templates_id'] = $info['templates_id']; + $result['page']['parent_id'] = $info['parent_id']; + $result['response'] = 200; if($language && $language->id) { + $status = $language->isDefault() ? $info['status'] : $info["status$language->id"]; $result['language'] = array_merge($result['language'], array( 'name' => $language->name, - 'status' => $language->status, + 'status' => ($language->status < Page::statusUnpublished ? $status : 0), 'segment' => $this->languageSegment($language) )); } @@ -916,6 +951,8 @@ class PagesPathFinder extends Wire { * */ protected function getShortcutGlobalUnique(&$path) { + + if(!$this->options['useGlobalUnique']) return false; $database = $this->wire()->database; $unique = Page::statusUnique; @@ -1135,6 +1172,37 @@ class PagesPathFinder extends Wire { return $this->wire()->sanitizer->pageName($name, Sanitizer::toAscii); } + /** + * @return null|Template + * + */ + protected function template() { + if(!$this->template && !empty($this->result['page']['templates_id'])) { + $this->template = $this->wire()->templates->get($this->result['page']['templates_id']); + } + return $this->template; + } + + /** + * Is matched result in admin? + * + * @return bool + * + */ + protected function admin() { + if($this->admin !== null) return $this->admin; + $config = $this->wire()->config; + if($this->result['page']['templates_id'] === 2) { + $this->admin = true; + } else if($this->result['page']['id'] === $config->adminRootPageID) { + $this->admin = true; + } else { + $template = $this->template(); + $this->admin = $template && in_array($template->name, $config->adminTemplates, true); + } + return $this->admin; + } + /** * Get string length, using mb_strlen() if available, strlen() if not * @@ -1146,16 +1214,6 @@ class PagesPathFinder extends Wire { return function_exists('mb_strlen') ? mb_strlen($str) : strlen($str); } - /** - * Get array of all possible response types indexed by http response code - * - * @return array - * - */ - public function getResponseTypes() { - return $this->responseTypes; - } - /*** MODULES **********************************************************************************/ /** @@ -1267,20 +1325,25 @@ class PagesPathFinder extends Wire { * */ protected function language($value) { + + $language = null; if($value instanceof Page) { if($value->className() === 'Language' || wireInstanceOf($value, 'Language')) { - return $value; + $language = $value; } + } else { + /** @var Languages|array $languages */ + $languages = $this->languages(); + if(!count($languages)) return null; + + $id = $this->languageId($value); + $language = $id ? $languages->get($id) : null; } - /** @var Languages|array $languages */ - $languages = $this->languages(); - if(!count($languages)) return null; + if(!$language || !$language->id) return null; - $id = $this->languageId($value); - - return $languages->get($id); + return $language; } /** @@ -1311,7 +1374,7 @@ class PagesPathFinder extends Wire { if($homepage) { foreach($columns as $name => $languageId) { $value = $homepage->get($name); - if($name === 'name' && $value === 'home') $value = ''; + if($name === 'name' && $value === Pages::defaultRootName) $value = ''; $this->languageSegments[$languageId] = $value; } @@ -1333,7 +1396,7 @@ class PagesPathFinder extends Wire { foreach($row as $name => $value) { $languageId = $columns[$name]; $value = $this->pageNameToUTF8($value); - if($name === 'name' && $value === 'home') $value = ''; + if($name === 'name' && $value === Pages::defaultRootName) $value = ''; $this->languageSegments[$languageId] = $value; } } @@ -1435,5 +1498,34 @@ class PagesPathFinder extends Wire { return (int) $language; } + /** + * Update given path for result language and return it + * + * @param string $path + * @param string $segment + * @return string + * + */ + protected function updatePathForLanguage($path, $segment = '') { + $result = &$this->result; + $config = $this->wire()->config; + $template = $this->template; + if($template && in_array($template->name, $config->adminTemplates)) return $path; + if(!strlen($segment)) { + $segment = $result['language']['segment']; + } + if(!strlen($segment) || $segment === Pages::defaultRootName) { + return $path; + } + if($result['page']['id'] === 1 || $path === '/') { + $pageNames = $this->wire()->languages->pageNames(); + if(!$pageNames || !$pageNames->useHomeSegment) return $path; + } + if($path != "/$segment" && strpos($path, "/$segment/") !== 0) { + $path = "/$segment$path"; + } + return $path; + } + } \ No newline at end of file diff --git a/wire/core/PagesRequest.php b/wire/core/PagesRequest.php index 2d66888e..abaddb52 100644 --- a/wire/core/PagesRequest.php +++ b/wire/core/PagesRequest.php @@ -8,8 +8,10 @@ * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * - * @method Page|null getPage() + * @method Page|NullPage getPage() * @method Page|null getPageForUser(Page $page, User $user) + * @method Page|NullPage getClosestPage() + * @method Page|string getLoginPageOrUrl(Page $page) * */ @@ -33,6 +35,22 @@ class PagesRequest extends Wire { */ 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 * @@ -80,6 +98,28 @@ class PagesRequest extends Wire { * */ 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 @@ -89,14 +129,6 @@ class PagesRequest extends Wire { */ protected $responseCode = 0; - /** - * Response type name - * - * @var string - * - */ - protected $responseName = ''; - /** * URL that should be redirected to for this request * @@ -126,12 +158,14 @@ class PagesRequest extends Wire { protected $prevGetIt = null; /** - * Error from getPage() method, if it could not identify a valid Page + * Optional message provided to setResponseCode() with additional detail * * @var string * */ - protected $error = ''; + protected $responseMessage = ''; + + /*************************************************************************************/ /** * Construct @@ -208,17 +242,36 @@ class PagesRequest extends Wire { } /** - * Get the requested page and populate identified urlSegments or page numbers - * + * 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 + * - `useShortcuts` (bool): Allow use PagePaths module and global-unique shortcuts? (default=true) + * - `useHistory` (bool): Allow use historical path names via PagePathHistory? (default=true) * @return Page|NullPage * */ - public function ___getPage() { + public function ___getPage(array $options = array()) { + + $defaults = array( + 'verbose' => false, + 'useShortcuts' => true, + 'useHistory' => true, + 'usePagePaths' => false, // needs more testing before setting true + 'useGlobalUnique' => true + ); + + $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; $page = null; // get the requested path @@ -233,12 +286,13 @@ class PagesRequest extends Wire { $page = $this->checkRequestFile($path); // can modify $path directly if(is_object($page)) { // Page (success) or NullPage (404) - if($page->id) { - $this->setResponse(200, 'fileOk'); - } else { - $this->setResponse(404, 'fileNotFound'); - } + $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 } } @@ -250,12 +304,12 @@ class PagesRequest extends Wire { // this will force pathFinder to detect a redirect condition $path = rtrim($path, '/') . '/index.php'; } - + // get info about requested path - $info = $this->pages->pathFinder()->get($path, array('verbose' => false)); + $info = $this->pages->pathFinder()->get($path, $options); $this->pageInfo = &$info; $this->languageName = $info['language']['name']; - $this->setResponse($info['response'], $info['type']); + $this->setResponseCode($info['response']); // URL segments if(count($info['urlSegments'])) { @@ -278,27 +332,54 @@ class PagesRequest extends Wire { } else { $page = $this->pages->newNullPage(); } - - // just in case (not likely) - if(!$page->id && $this->responseCode < 300) $this->responseCode = 404; - // the first version of PW populated first URL segment to $page - if($page->id && !empty($info['urlSegments'])) { - // undocumented behavior retained for backwards compatibility - $page->setQuietly('urlSegment', $input->urlSegment1); + $this->requestPage = $page; + + if($page->id) { + if(!empty($info['urlSegments'])) { + // the first version of PW populated first URL segment to $page + // 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) { + // just in case (not likely) + $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)) { + $this->setResponseCode($result[0]); + $this->setRedirectUrl($result[1], $result[0]); + } else if(is_bool($result)) { + $this->setResponseCode($result ? 200 : 404); + } + } else if(!empty($info['redirect'])) { + $this->setResponseCode(301); + } } - if($this->responseCode < 300) { - // 200 ok - - } else if($this->responseCode >= 300 && $this->responseCode < 400) { + if($this->responseCode >= 300 && $this->responseCode < 400) { // 301 permRedirect or 302 tempRedirect $this->setRedirectPath($info['redirect'], $info['response']); + } - } else if($this->responseCode >= 400) { - // 404 pageNotFound or 414 pathTooLong - if(!empty($info['redirect'])) {} // todo: pathFinder suggests a redirect may still be possible - if($page->id) $this->wire('closestPage', $page); // set a $closestPage API in case 404 page wants it + 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 + } + if($page->id) { + $this->closestPage = $page; + } $page = $this->pages->newNullPage(); } @@ -311,11 +392,12 @@ class PagesRequest extends Wire { * Update/get page for given user * * Must be called once the current $user is known as it may change the $page. - * Returns NullPage or login page if user lacks access. + * 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|null + * @return Page|NullPage * */ public function ___getPageForUser(Page $page, User $user) { @@ -332,6 +414,8 @@ class PagesRequest extends Wire { $this->redirectUrl = ''; } } + + $requestPage = $page; // enforce max pagination number when user is not logged in $pageNum = $this->wire()->input->pageNum(); @@ -345,24 +429,83 @@ class PagesRequest extends Wire { if($page->id) { $page = $this->checkAccess($page, $user); + if(!$page || !$page->id) { + // 404 + $page = $this->pages->newNullPage(); + } if(is_string($page)) { + // redirect URL + $this->setRedirectUrl($page); + $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 && $page->id) { + 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 "

404 Page Not Found

"; + * $p = $pages->request()->getClosestPage(); + * if($p->id) { + * 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 getPageRequestPath() { + protected function getRequestPagePath() { $config = $this->config; $sanitizer = $this->wire()->sanitizer; @@ -385,7 +528,7 @@ class PagesRequest extends Wire { $shit = substr($shit, strlen($rootUrl) - 1); } else { // request URL outside of our root directory - $this->setResponse(404, 'pageNotFound', 'Request URL outside of our web root'); + $this->setResponseCode(404, 'Request URL outside of our web root'); return false; } } @@ -409,21 +552,21 @@ class PagesRequest extends Wire { } if($shit !== $it) { // if still does not match then fail - $this->setResponse(400, 'pagePathError', 'Request URL contains invalid/unsupported characters'); + $this->setResponseCode(400, 'Request URL contains invalid/unsupported characters'); return false; } } $maxUrlDepth = $config->maxUrlDepth; if($maxUrlDepth > 0 && substr_count($it, '/') > $config->maxUrlDepth) { - $this->setResponse(414, 'pathTooLong', 'Request URL exceeds max depth set in $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->setResponse(400, 'pagePathError', 'Request URL contains a blank segment “//”'); + $this->setResponseCode(400, 'Request URL contains a blank segment “//”'); return false; } @@ -439,7 +582,7 @@ class PagesRequest extends Wire { * - 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 true and updates $path, when pagefile was found using deprecated prefix method. * - Returns false when none found. * * @param string $path Request path @@ -455,85 +598,151 @@ class PagesRequest extends Wire { $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) { - // request is for file in site/assets/files/... - $idAndFile = substr($url, strlen($config->urls->files)); + 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. - 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], '.'); + // 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 + 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 { - // request was to something in /site/assets/files/ but we don't recognize it - // tell caller that this should be a 404 + // extended paths not allowed return $pages->newNullPage(); } } - // check for secured filename: method 2 (deprecated), used only if $config->pagefileUrlPrefix is defined - $filePrefix = $config->pagefileUrlPrefix; - if($filePrefix && strpos($path, '/' . $filePrefix) !== false) { - if(preg_match('{^(.*/)' . $filePrefix . '([-_.a-zA-Z0-9]+)$}', $path, $matches) && strpos($matches[2], '.')) { - $path = $matches[1]; - $this->requestFile = $matches[2]; - return true; + 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 false; + 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); + 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. + * 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|null + * @return Page|string|null Page to render, URL to redirect to, or null for 404 * * */ @@ -542,14 +751,19 @@ class PagesRequest extends Wire { 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->viewable(false)) return $page; // false=viewable without template file check if($this->checkAccessDelegated($page)) return $page; - if($page->status < Page::statusUnpublished && $user->hasPermission('page-view', $page)) return $page; - - } else if($page->viewable()) { + // 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; + } - } else if($page->parent_id && $page->parent->template->name === 'admin' && $page->parent->viewable()) { + 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; @@ -558,66 +772,15 @@ class PagesRequest extends Wire { 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->config; - $session = $this->wire()->session; - $input = $this->wire()->input; - $disallowIDs = array($config->trashPageID); // don't allow login redirect for these pages - $loginRequestURL = $this->redirectUrl; - $loginPageID = $config->loginPageID; - $requestPage = $page; - $ns = 'ProcessPageView'; + // 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($page->id && in_array($page->id, $disallowIDs)) { - // don't allow login redirect when matching disallowIDs - $page = null; + // if we won’t be presenting a login or redirect then return null (404) + if(empty($result)) return 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->setRedirectUrl($redirectLogin); - } - - if(empty($loginRequestURL) && $session) { - $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 && $session) { - 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; + return $result; } /** @@ -746,6 +909,30 @@ class PagesRequest extends Wire { $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? * @@ -782,14 +969,22 @@ class PagesRequest extends Wire { * Set response code and type * * @param int $code - * @param string $name - * @param string $error Optional error string + * @param string $message Optional message string * */ - protected function setResponse($code, $name , $error = '') { + protected function setResponseCode($code, $message = '') { $this->responseCode = (int) $code; - $this->responseName = $name; - if($error) $this->error = $error; + if($message) $this->responseMessage = $message; + } + + /** + * Get all possible response code names indexed by http response code + * + * @return array + * + */ + public function getResponseCodeNames() { + return $this->responseCodeNames; } /** @@ -801,18 +996,23 @@ class PagesRequest extends Wire { * - 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) - * - pagePathError: page path error (400) + * - 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) - * - pathTooLong: path too long or segment too long + * - methodNotAllowed: request method is not allowed by template (405) + * - pathTooLong: path too long or segment too long (414) * * @return string * */ - public function getResponseName() { - if(!$this->responseName) return 'unknown'; - return $this->responseName; + public function getResponseCodeName() { + return $this->responseCodeNames[$this->responseCode]; } /** @@ -820,12 +1020,18 @@ class PagesRequest extends Wire { * * Returns integer, one of: * - * - 0: request not yet analyzed + * - 0: unknown/request not yet analyzed * - 200: successful request + * - 300: maybe redirect (needs decision) * - 301: permanent redirect * - 302: temporary redirect - * - 400: page path error + * - 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 @@ -852,7 +1058,7 @@ class PagesRequest extends Wire { * */ public function getRequestPath() { - if(empty($this->requestPath)) $this->requestPath = $this->getPageRequestPath(); + if(empty($this->requestPath)) $this->requestPath = $this->getRequestPagePath(); return $this->requestPath; } @@ -901,13 +1107,13 @@ class PagesRequest extends Wire { } /** - * Get the redirect type (0, 301 or 302) + * Get the redirect type (0, 301, 302, 307, 308) * * @return int * */ public function getRedirectType() { - return $this->redirectType; + return $this->redirectType === 300 ? 301 : $this->redirectType; } /** @@ -923,7 +1129,7 @@ class PagesRequest extends Wire { /** * Get the requested pagination number prefix * - * @return null + * @return null|string * */ public function getPageNumPrefix() { @@ -951,13 +1157,43 @@ class PagesRequest extends Wire { } /** - * Get error message + * Get message about response only if response was an error, blank otherwise * * @return string * */ - public function getPageError() { - return $this->error; + 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; + } } diff --git a/wire/modules/LanguageSupport/LanguageSupportPageNames.module b/wire/modules/LanguageSupport/LanguageSupportPageNames.module index f7f4836c..ab2d37a7 100644 --- a/wire/modules/LanguageSupport/LanguageSupportPageNames.module +++ b/wire/modules/LanguageSupport/LanguageSupportPageNames.module @@ -24,7 +24,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM static public function getModuleInfo() { return array( 'title' => 'Languages Support - Page Names', - 'version' => 12, + 'version' => 13, 'summary' => 'Required to use multi-language page names.', 'author' => 'Ryan Cramer', 'autoload' => true, @@ -119,8 +119,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $pageNumUrlPrefixes = array(); $this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute'); - $this->addHookBefore('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); - // $this->addHookAfter('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); // @todo + $this->addHookAfter('PagesRequest::getPage', $this, 'hookAfterPagesRequestGetPage'); $this->addHookAfter('PageFinder::getQuery', $this, 'hookPageFinderGetQuery'); // identify the pageNum URL prefixes for each language @@ -158,10 +157,10 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // note that the hooks above are added before this so that 404s can still be handled properly if($this->bypass) return; - $session = $this->wire()->session; - // verify that page path doesn't have mixed languages where it shouldn't // @todo this can be replaced since logic is now in PagesRequest/PagesPathFinder + /* + $session = $this->wire()->session; $redirectUrl = $this->verifyPath($this->requestPath); if($redirectUrl) { @@ -174,6 +173,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM } return; } + */ $language = $this->wire()->user->language; $pages = $this->wire()->pages; @@ -241,25 +241,34 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * @return string * */ - public function updatePath($path) { + public function removeLanguageSegment($path) { if($path === '/' || !strlen($path)) return $path; - $languages = $this->wire()->languages; $trailingSlash = substr($path, -1) == '/'; $testPath = trim($path, '/') . '/'; - $home = $this->wire()->pages->get(1); + $segments = $this->wire()->pages->pathFinder()->languageSegments(); + foreach($segments as $languageId => $segment) { + if(!strlen($segment)) continue; + $name = "$segment/"; + if(strpos($testPath, $name) !== 0) continue; + $path = substr($testPath, strlen($name)); + break; + } + + /* foreach($languages as $language) { $name = $language->isDefault() ? $home->get("name") : $home->get("name$language"); if($name == Pages::defaultRootName) continue; if(!strlen($name)) continue; $name = "$name/"; if(strpos($testPath, $name) === 0) { - $this->setLanguage = $language; + // $this->setLanguage = $language; $path = substr($testPath, strlen($name)); } } + */ if(!$trailingSlash && $path != '/') { $path = rtrim($path, '/'); @@ -268,6 +277,16 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM return '/' . ltrim($path, '/'); } + /** + * @param string $path + * @return string + * @deprecated use removeLanguageSegment instead + * + */ + public function updatePath($path) { + return $this->removeLanguageSegment($path); + } + /** * Determine language from requested path, and if a redirect needs to be performed * @@ -281,7 +300,6 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * * @todo this can be replaced/removed since logic is now in PagesRequest/PagesPathFinder * - */ protected function verifyPath($requestPath) { $languages = $this->wire()->languages; @@ -303,7 +321,6 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // determine if we should set the current language based on requested URL if(!$setLanguage) { foreach($parentsAndPage as $p) { - /** @var Page $p */ $requestedPart = strtolower(array_shift($requestedParts)); if($requestedPart === $p->name) continue; @@ -403,6 +420,21 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM return $redirectURL; } + */ + + /** + * Set the request language + * + * @param Language|null $language + * + */ + public function setLanguage(Language $language = null) { + $languages = $this->wire()->languages; + if(!$language) $language = $languages->getDefault(); + $this->setLanguage = $language; + $this->wire()->user->setLanguage($language); + $languages->setLocale(); + } /** * Called when page is not available in a given language @@ -424,7 +456,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * @since 3.0.186 * */ - protected function ___pageNotAvailableInLanguage(Page $page, Language $language) { + public function ___pageNotAvailableInLanguage(Page $page, Language $language) { if($language) {} // ignore if($page->editable()) return true; if($page->id == $this->wire()->config->http404PageID) return true; @@ -492,18 +524,16 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * @since 3.0.186 * @todo this can be replaced with an after() hook as PagesRequest can figure out language on its own now * - */ public function hookBeforePagesRequestGetPage(HookEvent $event) { if($this->requestPath) return; // execute only once - /** @var PagesRequest $request */ $request = $event->object; $requestPath = $request->getRequestPath(); $this->requestPath = $requestPath; if($this->isAssetPath($requestPath)) { - // bypass means the request was to something in /site/*/ + // bypass means the request was to something in /site/ // that has no possibilty of language support $this->bypass = true; } else { @@ -519,6 +549,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $event->removeHook($event); } + */ /** * Hook in after PagesRequest::getPage @@ -526,25 +557,25 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * @param HookEvent $event * @since 3.0.186 * + */ public function hookAfterPagesRequestGetPage(HookEvent $event) { + /** @var PagesRequest $request */ $request = $event->object; - $requestPath = $request->getRequestPath(); - $this->requestPath = $requestPath; + $this->requestPath = $request->getRequestPath(); $languageName = $request->getLanguageName(); - if($this->isAssetPath($requestPath)) { + if($this->isAssetPath($this->requestPath)) { // bypass means the request was to something in /site/... // that has no possibilty of language support $this->bypass = true; } else if($languageName) { $language = $this->wire()->languages->get($languageName); - if($language && $language->id) $this->setLanguage = $language; + if($language && $language->id) $this->setLanguage($language); } $event->removeHook($event); } - */ /** @@ -959,7 +990,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM } if(!$language || !$language->id || !$language instanceof Language) { - $language = $languages->get('default'); + $language = $languages->getDefault(); } return $language; diff --git a/wire/modules/PagePaths.module b/wire/modules/PagePaths.module index c9e307ab..79091bad 100644 --- a/wire/modules/PagePaths.module +++ b/wire/modules/PagePaths.module @@ -268,10 +268,14 @@ class PagePaths extends WireData implements Module, ConfigurableModule { $sanitizer = $this->wire()->sanitizer; $database = $this->wire()->database; + $languages = $this->wire()->languages; $config = $this->wire()->config; + $table = self::dbTableName; $useUTF8 = $config->pageNameCharset === 'UTF8'; + if($languages && !$languages->hasPageNames()) $languages = null; + if($useUTF8) { $path = $sanitizer->pagePathName($path, Sanitizer::toAscii); } @@ -285,6 +289,13 @@ class PagePaths extends WireData implements Module, ConfigurableModule { 'pages.status AS status' ); + if($languages) { + foreach($languages as $language) { + if($language->isDefault()) continue; + $columns[] = "pages.status$language->id AS status$language->id"; + } + } + $cols = implode(', ', $columns); $sql = @@ -303,7 +314,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule { if(!$row) return false; foreach($row as $key => $value) { - if($key === 'id' || $key === 'status' || strpos($key, '_id')) { + if($key === 'id' || strpos($key, 'status') === 0 || strpos($key, '_id')) { $row[$key] = (int) $value; } } diff --git a/wire/modules/Process/ProcessPageView.module b/wire/modules/Process/ProcessPageView.module index 2a81df04..549797dd 100644 --- a/wire/modules/Process/ProcessPageView.module +++ b/wire/modules/Process/ProcessPageView.module @@ -19,6 +19,7 @@ * @method sendFile($page, $basename) * @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null) * @method string|bool|array|Page pathHooks($path, $out) + * @method void userNotAllowed(User $user, $page, PagesRequest $request) * */ class ProcessPageView extends Process { @@ -27,7 +28,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' => 105, + 'version' => 106, 'permanent' => true, 'permission' => 'page-view', ); @@ -116,7 +117,6 @@ class ProcessPageView extends Process { } } - /** * Render Page * @@ -135,6 +135,11 @@ class ProcessPageView extends Process { $page->of(true); $originalPage = $page; $page = $request->getPageForUser($page, $user); + $code = $request->getResponseCode(); + + if($code == 401 || $code == 403) { + $this->userNotAllowed($user, $page, $request); + } if(!$page || !$page->id || $originalPage->id == $config->http404PageID) { $s = 'access not allowed'; @@ -154,12 +159,11 @@ class ProcessPageView extends Process { } try { - $requestFile = $request->getRequestFile(); - - if($requestFile) { + $file = $request->getFile(); + if($file) { $this->responseType = self::responseTypeFile; - $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $requestFile)); - $this->sendFile($page, $requestFile); + $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $file)); + $this->sendFile($page, $file); } else { $contentType = $this->contentTypeHeader($page, true); @@ -209,42 +213,27 @@ class ProcessPageView extends Process { ); $options = count($options) ? array_merge($defaults, $options) : $defaults; + $config = $this->wire()->config; $hooks = $this->wire()->hooks; $input = $this->wire()->input; $pages = $this->wire()->pages; + $requestPath = $request->getRequestPath(); - $pageNum = $request->getPageNum(); $pageNumPrefix = $request->getPageNumPrefix(); - $setPageNum = 0; $pageNumSegment = ''; + $setPageNum = 0; $page = null; $out = false; $this->setResponseType(self::responseTypeNoPage); - if($pageNum > 0 && $pageNumPrefix !== null) { - // there is a pagination segment present in the request path - $slash = substr($requestPath, -1) === '/' ? '/' : ''; - $requestPath = rtrim($requestPath, '/'); - $pageNumSegment = $pageNumPrefix . $pageNum; - if(substr($requestPath, -1 * strlen($pageNumSegment)) === $pageNumSegment) { - // remove pagination segment from request path - $requestPath = substr($requestPath, 0, -1 * strlen($pageNumSegment)); - $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($pageNum); - } else { - // not a pagination segment - // add the slash back to restore requestPath - $requestPath .= $slash; - $pageNumSegment = ''; - } + if($pageNumPrefix !== null) { + // request may have a pagination segment + $pageNumSegment = $this->renderNoPagePagination($requestPath, $pageNumPrefix, $request->getPageNum()); + $setPageNum = $input->pageNum(); } - + if(!$options['ready']) $this->wire('page', $options['page']); // run up to 2 times, once before ready state and once after @@ -331,6 +320,102 @@ class ProcessPageView extends Process { return $out; } + + /** + * Check for pagination in a no-page request (helper to renderNoPage method) + * + * - Updates given request path to remove pagination segment. + * - Returns found pagination segment or blank if none. + * - Redirects to non-slash version if pagination segment found with trailing slash. + * + * @param string $requestPath + * @param string|null $pageNumPrefix + * @param int $pageNum + * @return string Return found pageNum segment or blank if none + * + */ + protected function renderNoPagePagination(&$requestPath, $pageNumPrefix, $pageNum) { + + $config = $this->wire()->config; + + if($pageNum < 1 || $pageNumPrefix === null) return ''; + + // there is a pagination segment present in the request path + $slash = substr($requestPath, -1) === '/' ? '/' : ''; + $requestPath = rtrim($requestPath, '/'); + $pageNumSegment = $pageNumPrefix . $pageNum; + + if(substr($requestPath, -1 * strlen($pageNumSegment)) === $pageNumSegment) { + // remove pagination segment from request path + $requestPath = substr($requestPath, 0, -1 * strlen($pageNumSegment)); + $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); + } + $this->wire()->input->setPageNum($pageNum); + + } else { + // not a pagination segment + // add the slash back to restore requestPath + $requestPath .= $slash; + $pageNumSegment = ''; + } + + return $pageNumSegment; + } + + /** + * Called when a 401 unauthorized or 403 forbidden request + * + * #pw-hooker + * + * @param User $user + * @param Page|NullPage|null $page + * @param PagesRequest $request + * @since 3.0.186 + * + */ + protected function ___userNotAllowed(User $user, $page, PagesRequest $request) { + + $input = $this->wire()->input; + $config = $this->wire()->config; + $session = $this->wire()->session; + + if(!$session || !$page || !$page->id) return; + if($user->isLoggedin()) return; + + $loginRequestURL = $request->getRedirectUrl(); + $ns = 'ProcessPageView'; // session namespace + + if(empty($loginRequestURL) && $session) { + $loginRequestURL = $session->getFor($ns, 'loginRequestURL'); + } + + if(!empty($loginRequestURL)) return; + if($page->id == $config->loginPageID) return; + if($input->get('loggedout')) return; + + $loginRequestURL = $input->url(array('page' => $page)); + + 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', $page->id); + $session->setFor($ns, 'loginRequestURL', $loginRequestURL); + } /** * Check request for redirect and apply it when appropriate