1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-10 16:54:44 +02:00

Continued updates to PagesRequest, PagesPathFinder, LanguageSupportPageNames, ProcessPageView and related modules

This commit is contained in:
Ryan Cramer
2021-10-08 15:52:07 -04:00
parent 5ed2e3047e
commit aa4fbd4dd9
6 changed files with 766 additions and 311 deletions

View File

@@ -2587,7 +2587,7 @@ class PageFinder extends Wire {
} }
if($langNames) { if($langNames) {
$module = $this->languages->pageNames(); $module = $this->languages->pageNames();
if($module) $selectorValue = $module->updatePath($selectorValue); if($module) $selectorValue = $module->removeLanguageSegment($selectorValue);
} }
$parts = explode('/', rtrim($selectorValue, '/')); $parts = explode('/', rtrim($selectorValue, '/'));
$part = $sanitizer->pageName(array_pop($parts), Sanitizer::toAscii); $part = $sanitizer->pageName(array_pop($parts), Sanitizer::toAscii);

View File

@@ -34,6 +34,8 @@ class PagesPathFinder extends Wire {
protected $defaults = array( protected $defaults = array(
'useLanguages' => true, 'useLanguages' => true,
'useShortcuts' => true, 'useShortcuts' => true,
'usePagePaths' => true,
'useGlobalUnique' => true,
'useHistory' => true, 'useHistory' => true,
'verbose' => true, 'verbose' => true,
); );
@@ -63,19 +65,16 @@ class PagesPathFinder extends Wire {
protected $result = array(); protected $result = array();
/** /**
* Response type codes to response type names * @var Template|null
*
* @var array
* *
*/ */
protected $responseTypes = array( protected $template = null;
200 => 'ok',
301 => 'permRedirect', /**
302 => 'tempRedirect', * @var bool|null
400 => 'pagePathError', *
404 => 'pageNotFound', */
414 => 'pathTooLong', protected $admin = null;
);
/** /**
* URL part types (for reference) * URL part types (for reference)
@@ -114,6 +113,8 @@ class PagesPathFinder extends Wire {
$this->verbose = $this->options['verbose']; $this->verbose = $this->options['verbose'];
$this->methods = array(); $this->methods = array();
$this->result = $this->getBlankResult(array('request' => $path)); $this->result = $this->getBlankResult(array('request' => $path));
$this->template = null;
$this->admin = null;
if(empty($this->pageNameCharset)) { if(empty($this->pageNameCharset)) {
$this->pageNameCharset = $this->wire()->config->pageNameCharset; $this->pageNameCharset = $this->wire()->config->pageNameCharset;
@@ -243,9 +244,8 @@ class PagesPathFinder extends Wire {
if($result['response'] >= 400) { if($result['response'] >= 400) {
$page = $this->pages->newNullPage(); $page = $this->pages->newNullPage();
} else { } else {
$template = $this->wire()->templates->get($result['page']['templates_id']);
$page = $this->pages->getOneById($result['page']['id'], array( $page = $this->pages->getOneById($result['page']['id'], array(
'template' => $template, 'template' => $this->template(),
'parent_id' => $result['page']['parent_id'], 'parent_id' => $result['page']['parent_id'],
)); ));
} }
@@ -398,7 +398,8 @@ class PagesPathFinder extends Wire {
/** @var Language $language */ /** @var Language $language */
if($language->isDefault()) continue; if($language->isDefault()) continue;
$nameLanguage = $this->pageNameToUTF8($row["{$key}_name$language->id"]); $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($nameLanguage != $nameDefault && $nameLanguage === $name) {
if($this->verbose) { if($this->verbose) {
$result['parts'][] = array( $result['parts'][] = array(
@@ -427,6 +428,12 @@ class PagesPathFinder extends Wire {
$path = '/' . implode('/', $namesByLanguage[$langName]); $path = '/' . implode('/', $namesByLanguage[$langName]);
if($langName === 'default') {
$result['page']['path'] = $path;
} else {
$result['page']['path'] = '/' . implode('/', $namesByLanguage['default']);
}
if(count($this->useLanguages)) { if(count($this->useLanguages)) {
if($langName === 'default') { if($langName === 'default') {
$result['language']['status'] = $result['page']['status']; $result['language']['status'] = $result['page']['status'];
@@ -516,11 +523,15 @@ class PagesPathFinder extends Wire {
if(!count($languages)) return null; if(!count($languages)) return null;
$firstPart = reset($parts); $firstPart = reset($parts);
$key = array_search($firstPart, $this->languageSegments()); $languageKey = array_search($firstPart, $this->languageSegments());
if($key === false) return $languages->getDefault();
$segment = array_shift($parts); if($languageKey === false) {
$language = $languages->get($key); $language = $languages->getDefault();
$segment = $this->languageSegment('default');
} else {
$segment = array_shift($parts);
$language = $languages->get($languageKey);
}
if(!$language || !$language->id) return null; if(!$language || !$language->id) return null;
@@ -528,7 +539,7 @@ class PagesPathFinder extends Wire {
$result['language']['segment'] = $segment; $result['language']['segment'] = $segment;
$result['language']['name'] = $language->name; $result['language']['name'] = $language->name;
if($this->verbose) { if($this->verbose && $languageKey !== false) {
$result['parts'][] = array( $result['parts'][] = array(
'type' => 'language', 'type' => 'language',
'value' => $segment, 'value' => $segment,
@@ -565,6 +576,7 @@ class PagesPathFinder extends Wire {
'parent_id' => 0, 'parent_id' => 0,
'templates_id' => 0, 'templates_id' => 0,
'status' => 0, 'status' => 0,
'path' => '',
), ),
'language' => array( 'language' => array(
'name' => '', // intentionally blank 'name' => '', // intentionally blank
@@ -576,6 +588,7 @@ class PagesPathFinder extends Wire {
'urlSegmentStr' => '', 'urlSegmentStr' => '',
'pageNum' => 1, 'pageNum' => 1,
'pageNumPrefix' => '', 'pageNumPrefix' => '',
'pathAdd' => '', // valid URL segments, page numbers, trailing slash, etc.
'scheme' => '', 'scheme' => '',
'method' => '', 'method' => '',
); );
@@ -596,7 +609,6 @@ class PagesPathFinder extends Wire {
*/ */
protected function applyResultTemplate($path) { protected function applyResultTemplate($path) {
$templates = $this->wire()->templates;
$config = $this->wire()->config; $config = $this->wire()->config;
$fail = false; $fail = false;
$result = &$this->result; $result = &$this->result;
@@ -605,10 +617,11 @@ class PagesPathFinder extends Wire {
$this->applyResultHome(); $this->applyResultHome();
} }
$template = $result['page']['templates_id'] ? $templates->get($result['page']['templates_id']) : null; $template = $this->template();
$slashUrls = $template ? (int) $template->slashUrls : 0; $slashUrls = $template ? (int) $template->slashUrls : 0;
$useTrailingSlash = $slashUrls ? 1 : -1; // 1=yes, 0=either, -1=no $useTrailingSlash = $slashUrls ? 1 : -1; // 1=yes, 0=either, -1=no
$https = $template ? (int) $template->https : 0; $https = $template ? (int) $template->https : 0;
$appendPath = '';
// populate urlSegmentStr property if applicable // populate urlSegmentStr property if applicable
if(empty($result['urlSegmentStr']) && !empty($result['urlSegments'])) { if(empty($result['urlSegmentStr']) && !empty($result['urlSegments'])) {
@@ -620,7 +633,7 @@ class PagesPathFinder extends Wire {
if(strlen($result['urlSegmentStr'])) { if(strlen($result['urlSegmentStr'])) {
if($template && ($template->urlSegments || $template->name === 'admin')) { if($template && ($template->urlSegments || $template->name === 'admin')) {
if($template->isValidUrlSegmentStr($result['urlSegmentStr'])) { if($template->isValidUrlSegmentStr($result['urlSegmentStr'])) {
$path = rtrim($path, '/') . "/$result[urlSegmentStr]"; $appendPath .= "/$result[urlSegmentStr]";
if($result['pageNum'] < 2) $useTrailingSlash = (int) $template->slashUrlSegments; if($result['pageNum'] < 2) $useTrailingSlash = (int) $template->slashUrlSegments;
} else { } else {
// ERROR: URL segments did not validate // ERROR: URL segments did not validate
@@ -643,7 +656,9 @@ class PagesPathFinder extends Wire {
$fail = true; $fail = true;
} }
$segment = $this->pageNumUrlSegment($result['pageNum'], $result['language']['name']); $segment = $this->pageNumUrlSegment($result['pageNum'], $result['language']['name']);
if(strlen($segment)) $path = rtrim($path, '/') . "/$segment"; if(strlen($segment)) {
$appendPath .= "/$segment";
}
$useTrailingSlash = (int) $template->slashPageNum; $useTrailingSlash = (int) $template->slashPageNum;
} else { } else {
// template does not allow page numbers // 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 // determine whether path should end with a trailing slash or not
$path = rtrim($path, '/');
if($useTrailingSlash > 0) { if($useTrailingSlash > 0) {
// trailing slash required // trailing slash required
$path .= '/'; $appendPath .= '/';
} else if($useTrailingSlash < 0) { } else if($useTrailingSlash < 0) {
// trailing slash disallowed // trailing slash disallowed
} else if(substr($result['request'], -1) === '/') { } else if(substr($result['request'], -1) === '/') {
// either acceptable, add slash if request had it // either acceptable, add slash if request had it
$path .= '/'; $appendPath .= '/';
} }
$path = rtrim($path, '/') . $appendPath;
$result['redirect'] = $path; $result['redirect'] = $path;
$result['pathAdd'] = $appendPath;
// determine if page requires specific https vs. http scheme // determine if page requires specific https vs. http scheme
if($https > 0 && !$config->noHTTPS) { if($https > 0 && !$config->noHTTPS) {
@@ -685,10 +702,10 @@ class PagesPathFinder extends Wire {
protected function applyResultHome() { protected function applyResultHome() {
$config = $this->wire()->config; $config = $this->wire()->config;
$home = $this->pages->get($config->rootPageID); $home = $this->pages->get($config->rootPageID);
$template = $home->template; $this->template = $home->template;
$this->result['page'] = array_merge($this->result['page'], array( $this->result['page'] = array_merge($this->result['page'], array(
'id' => $config->rootPageID, 'id' => $config->rootPageID,
'templates_id' => $template->id, 'templates_id' => $this->template->id,
'parent_id' => 0, 'parent_id' => 0,
'status' => $home->status 'status' => $home->status
)); ));
@@ -707,30 +724,28 @@ class PagesPathFinder extends Wire {
$result = &$this->result; $result = &$this->result;
if(!count($this->useLanguages)) return $path; 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 there were any non-default language segments, let that dictate the language
if(empty($result['language']['segment'])) { if(empty($result['language']['segment'])) {
$useLangName = ''; $useLangName = 'default';
foreach($result['parts'] as $key => $part) { foreach($result['parts'] as $key => $part) {
$langName = $part['language']; $langName = $part['language'];
if(empty($langName) || $langName === 'default') continue; if(empty($langName) || $langName === 'default') continue;
$useLangName = $langName; $useLangName = $langName;
break; break;
} }
if($useLangName) { $segment = $this->languageSegment($useLangName);
$segment = $this->languageSegment($useLangName); if($segment) $result['language']['segment'] = $segment;
if($segment) $result['language']['segment'] = $segment; $result['language']['name'] = $useLangName;
$result['language']['name'] = $useLangName;
}
} }
// prepend the path with the language segment // prepend the path with the language segment
if(!empty($result['language']['segment'])) { if(!empty($result['language']['segment'])) {
$segment = $result['language']['segment']; $path = $this->updatePathForLanguage($path);
if($path != "/$segment" && strpos($path, "/$segment/") !== 0) { $redirect = &$result['redirect'];
$path = "/$segment$path"; if(!empty($redirect)) $redirect = $this->updatePathForLanguage($redirect);
}
} }
return $path; return $path;
@@ -784,7 +799,7 @@ class PagesPathFinder extends Wire {
protected function finishResult($path) { protected function finishResult($path) {
$result = &$this->result; $result = &$this->result;
$types = &$this->responseTypes; $types = $this->pages->request()->getResponseCodeNames();
if($path !== false) $path = $this->applyResultLanguage($path); if($path !== false) $path = $this->applyResultLanguage($path);
if($path !== false) $path = $this->applyResultTemplate($path); if($path !== false) $path = $this->applyResultTemplate($path);
@@ -792,6 +807,7 @@ class PagesPathFinder extends Wire {
$response = &$result['response']; $response = &$result['response'];
$language = &$result['language']; $language = &$result['language'];
$errors = &$result['errors'];
if($response === 404) { if($response === 404) {
// page not found // page not found
@@ -813,25 +829,37 @@ class PagesPathFinder extends Wire {
$response = 301; $response = 301;
} }
if(empty($result['type']) && isset($types[$response])) { if(empty($language['name'])) {
if($result['response'] === 404 && !empty($result['redirect'])) { // set language property (likely for non-multi-language install)
// when page found but path not use the 400 response type name w/404 $language['name'] = 'default';
$result['type'] = 'pagePathError'; $language['status'] = 1;
} else {
$result['type'] = $types[$response]; } 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($language['name'])) { if(empty($result['type']) && isset($types[$response])) {
$language['name'] = 'default'; if($result['response'] === 404 && !empty($result['redirect'])) {
$language['status'] = 1; // when page found but path not use the 400 response type name w/404
$result['type'] = $types[400];
} else {
$result['type'] = $types[$response];
}
} }
$result['method'] = implode(',', $this->methods); $result['method'] = implode(',', $this->methods);
if(!$this->verbose) unset($result['parts']); if(!$this->verbose) unset($result['parts']);
if(empty($result['errors'])) { if(empty($errors)) {
// force errors placeholder to end if there arent any // force errors placeholder to end if there arent any
unset($result['errors']); unset($result['errors']);
$result['errors'] = array(); $result['errors'] = array();
@@ -881,6 +909,7 @@ class PagesPathFinder extends Wire {
*/ */
protected function getShortcutPagePaths(&$path) { protected function getShortcutPagePaths(&$path) {
if(!$this->options['usePagePaths']) return false;
$module = $this->pagePathsModule(); $module = $this->pagePathsModule();
if(!$module) return false; if(!$module) return false;
@@ -890,17 +919,23 @@ class PagesPathFinder extends Wire {
if(!$info) return false; if(!$info) return false;
$language = $this->language((int) $info['language_id']); $language = $this->language((int) $info['language_id']);
$path = "/$info[path]"; $path = "/$info[path]";
unset($info['language_id'], $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; $result['response'] = 200;
if($language && $language->id) { if($language && $language->id) {
$status = $language->isDefault() ? $info['status'] : $info["status$language->id"];
$result['language'] = array_merge($result['language'], array( $result['language'] = array_merge($result['language'], array(
'name' => $language->name, 'name' => $language->name,
'status' => $language->status, 'status' => ($language->status < Page::statusUnpublished ? $status : 0),
'segment' => $this->languageSegment($language) 'segment' => $this->languageSegment($language)
)); ));
} }
@@ -917,6 +952,8 @@ class PagesPathFinder extends Wire {
*/ */
protected function getShortcutGlobalUnique(&$path) { protected function getShortcutGlobalUnique(&$path) {
if(!$this->options['useGlobalUnique']) return false;
$database = $this->wire()->database; $database = $this->wire()->database;
$unique = Page::statusUnique; $unique = Page::statusUnique;
@@ -1135,6 +1172,37 @@ class PagesPathFinder extends Wire {
return $this->wire()->sanitizer->pageName($name, Sanitizer::toAscii); 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 * 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); 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 **********************************************************************************/ /*** MODULES **********************************************************************************/
/** /**
@@ -1268,19 +1326,24 @@ class PagesPathFinder extends Wire {
*/ */
protected function language($value) { protected function language($value) {
$language = null;
if($value instanceof Page) { if($value instanceof Page) {
if($value->className() === 'Language' || wireInstanceOf($value, 'Language')) { 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 */ if(!$language || !$language->id) return null;
$languages = $this->languages();
if(!count($languages)) return null;
$id = $this->languageId($value); return $language;
return $languages->get($id);
} }
/** /**
@@ -1311,7 +1374,7 @@ class PagesPathFinder extends Wire {
if($homepage) { if($homepage) {
foreach($columns as $name => $languageId) { foreach($columns as $name => $languageId) {
$value = $homepage->get($name); $value = $homepage->get($name);
if($name === 'name' && $value === 'home') $value = ''; if($name === 'name' && $value === Pages::defaultRootName) $value = '';
$this->languageSegments[$languageId] = $value; $this->languageSegments[$languageId] = $value;
} }
@@ -1333,7 +1396,7 @@ class PagesPathFinder extends Wire {
foreach($row as $name => $value) { foreach($row as $name => $value) {
$languageId = $columns[$name]; $languageId = $columns[$name];
$value = $this->pageNameToUTF8($value); $value = $this->pageNameToUTF8($value);
if($name === 'name' && $value === 'home') $value = ''; if($name === 'name' && $value === Pages::defaultRootName) $value = '';
$this->languageSegments[$languageId] = $value; $this->languageSegments[$languageId] = $value;
} }
} }
@@ -1435,5 +1498,34 @@ class PagesPathFinder extends Wire {
return (int) $language; 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;
}
} }

View File

@@ -8,8 +8,10 @@
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @method Page|null getPage() * @method Page|NullPage getPage()
* @method Page|null getPageForUser(Page $page, User $user) * @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; 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 * @var array
* *
@@ -81,6 +99,28 @@ class PagesRequest extends Wire {
*/ */
protected $pageNumPrefix = null; 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 * Response http code
* *
@@ -89,14 +129,6 @@ class PagesRequest extends Wire {
*/ */
protected $responseCode = 0; protected $responseCode = 0;
/**
* Response type name
*
* @var string
*
*/
protected $responseName = '';
/** /**
* URL that should be redirected to for this request * URL that should be redirected to for this request
* *
@@ -126,12 +158,14 @@ class PagesRequest extends Wire {
protected $prevGetIt = null; 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 * @var string
* *
*/ */
protected $error = ''; protected $responseMessage = '';
/*************************************************************************************/
/** /**
* Construct * 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 * @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 // perform this work only once unless reset by setPage or setRequestPath
if($this->page && $this->requestPath === $this->processedPath) return $this->page; if($this->page && $this->requestPath === $this->processedPath) return $this->page;
$input = $this->wire()->input; $input = $this->wire()->input;
$languages = $this->wire()->languages;
$page = null; $page = null;
// get the requested path // get the requested path
@@ -233,12 +286,13 @@ class PagesRequest extends Wire {
$page = $this->checkRequestFile($path); // can modify $path directly $page = $this->checkRequestFile($path); // can modify $path directly
if(is_object($page)) { if(is_object($page)) {
// Page (success) or NullPage (404) // Page (success) or NullPage (404)
if($page->id) { $this->setResponseCode($page->id ? 200 : 404, 'Secure pagefile request');
$this->setResponse(200, 'fileOk');
} else {
$this->setResponse(404, 'fileNotFound');
}
return $this->setPage($page); 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
} }
} }
@@ -252,10 +306,10 @@ class PagesRequest extends Wire {
} }
// get info about requested path // 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->pageInfo = &$info;
$this->languageName = $info['language']['name']; $this->languageName = $info['language']['name'];
$this->setResponse($info['response'], $info['type']); $this->setResponseCode($info['response']);
// URL segments // URL segments
if(count($info['urlSegments'])) { if(count($info['urlSegments'])) {
@@ -279,26 +333,53 @@ class PagesRequest extends Wire {
$page = $this->pages->newNullPage(); $page = $this->pages->newNullPage();
} }
// just in case (not likely) $this->requestPage = $page;
if(!$page->id && $this->responseCode < 300) $this->responseCode = 404;
// the first version of PW populated first URL segment to $page if($page->id) {
if($page->id && !empty($info['urlSegments'])) { if(!empty($info['urlSegments'])) {
// undocumented behavior retained for backwards compatibility // the first version of PW populated first URL segment to $page
$page->setQuietly('urlSegment', $input->urlSegment1); // 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) { if($this->responseCode === 300) {
// 200 ok // 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);
}
}
} else if($this->responseCode >= 300 && $this->responseCode < 400) { if($this->responseCode >= 300 && $this->responseCode < 400) {
// 301 permRedirect or 302 tempRedirect // 301 permRedirect or 302 tempRedirect
$this->setRedirectPath($info['redirect'], $info['response']); $this->setRedirectPath($info['redirect'], $info['response']);
}
} else if($this->responseCode >= 400) { if($this->responseCode >= 400) {
// 404 pageNotFound or 414 pathTooLong // 400 badRequest, 401 unauthorized, 403 forbidden,
if(!empty($info['redirect'])) {} // todo: pathFinder suggests a redirect may still be possible // 404 pageNotFound, 405 methodNotallowed, 414 pathTooLong
if($page->id) $this->wire('closestPage', $page); // set a $closestPage API in case 404 page wants it if(!empty($info['redirect'])) {
// pathFinder suggests a redirect may still be possible
}
if($page->id) {
$this->closestPage = $page;
}
$page = $this->pages->newNullPage(); $page = $this->pages->newNullPage();
} }
@@ -311,11 +392,12 @@ class PagesRequest extends Wire {
* Update/get page for given user * Update/get page for given user
* *
* Must be called once the current $user is known as it may change the $page. * 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 Page $page
* @param User $user * @param User $user
* @return Page|NullPage|null * @return Page|NullPage
* *
*/ */
public function ___getPageForUser(Page $page, User $user) { public function ___getPageForUser(Page $page, User $user) {
@@ -333,6 +415,8 @@ class PagesRequest extends Wire {
} }
} }
$requestPage = $page;
// enforce max pagination number when user is not logged in // enforce max pagination number when user is not logged in
$pageNum = $this->wire()->input->pageNum(); $pageNum = $this->wire()->input->pageNum();
if($pageNum > 1 && $page->id && $isGuest) { if($pageNum > 1 && $page->id && $isGuest) {
@@ -345,14 +429,73 @@ class PagesRequest extends Wire {
if($page->id) { if($page->id) {
$page = $this->checkAccess($page, $user); $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->checkScheme($page);
$this->setPage($page); $this->setPage($page);
$page->of(true); $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; return $page;
} }
@@ -362,7 +505,7 @@ class PagesRequest extends Wire {
* @return bool|string Return false on fail, path on success * @return bool|string Return false on fail, path on success
* *
*/ */
protected function getPageRequestPath() { protected function getRequestPagePath() {
$config = $this->config; $config = $this->config;
$sanitizer = $this->wire()->sanitizer; $sanitizer = $this->wire()->sanitizer;
@@ -385,7 +528,7 @@ class PagesRequest extends Wire {
$shit = substr($shit, strlen($rootUrl) - 1); $shit = substr($shit, strlen($rootUrl) - 1);
} else { } else {
// request URL outside of our root directory // 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; return false;
} }
} }
@@ -409,21 +552,21 @@ class PagesRequest extends Wire {
} }
if($shit !== $it) { if($shit !== $it) {
// if still does not match then fail // 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; return false;
} }
} }
$maxUrlDepth = $config->maxUrlDepth; $maxUrlDepth = $config->maxUrlDepth;
if($maxUrlDepth > 0 && substr_count($it, '/') > $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; return false;
} }
if(!isset($it[0]) || $it[0] != '/') $it = "/$it"; if(!isset($it[0]) || $it[0] != '/') $it = "/$it";
if(strpos($it, '//') !== false) { 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; return false;
} }
@@ -439,7 +582,7 @@ class PagesRequest extends Wire {
* - This function sets $this->requestFile when it finds one. * - This function sets $this->requestFile when it finds one.
* - Returns Page when a pagefile was found and matched to a page. * - Returns Page when a pagefile was found and matched to a page.
* - Returns NullPage when request should result in a 404. * - 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. * - Returns false when none found.
* *
* @param string $path Request path * @param string $path Request path
@@ -455,85 +598,151 @@ class PagesRequest extends Wire {
$url = rtrim($config->urls->root, '/') . $path; $url = rtrim($config->urls->root, '/') . $path;
// check for secured filename, method 1: actual file URL, minus leading "." or "-" // check for secured filename, method 1: actual file URL, minus leading "." or "-"
if(strpos($url, $config->urls->files) === 0) { if(strpos($url, $config->urls->files) !== 0) {
// request is for file in site/assets/files/... // if URL is not to files, check if it might be using legacy prefix
$idAndFile = substr($url, strlen($config->urls->files)); if($config->pagefileUrlPrefix) return $this->checkRequestFilePrefix($path);
// request is not for a file
return false;
}
// matching in $idAndFile: 1234/file.jpg, 1/2/3/4/file.jpg, 1234/subdir/file.jpg, 1/2/3/4/subdir/file.jpg, etc. // request is for file in site/assets/files/...
if(preg_match('{^(\d[\d\/]*)/([-_a-zA-Z0-9][-_./a-zA-Z0-9]+)$}', $idAndFile, $matches) && strpos($matches[2], '.')) { $idAndFile = substr($url, strlen($config->urls->files));
// 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(); // 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();
}
if(!ctype_digit("$idPath")) { // request is consistent with those that would match to a file
// extended paths where id separated by slashes, i.e. 1/2/3/4 $idPath = trim($matches[1], '/');
if($config->pagefileExtendedPaths) { $file = trim($matches[2], '.');
// 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) { if(!strpos($file, '.')) return $pages->newNullPage();
// 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(!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 { } else {
// request was to something in /site/assets/files/ but we don't recognize it // extended paths not allowed
// tell caller that this should be a 404
return $pages->newNullPage(); return $pages->newNullPage();
} }
} }
// check for secured filename: method 2 (deprecated), used only if $config->pagefileUrlPrefix is defined if(strpos($file, '/') !== false) {
$filePrefix = $config->pagefileUrlPrefix; // file in subdirectory (for instance ProDrafts uses subdirectories for draft files)
if($filePrefix && strpos($path, '/' . $filePrefix) !== false) { list($subdir, $file) = explode('/', $file, 2);
if(preg_match('{^(.*/)' . $filePrefix . '([-_.a-zA-Z0-9]+)$}', $path, $matches) && strpos($matches[2], '.')) {
$path = $matches[1]; if(strpos($file, '/') !== false) {
$this->requestFile = $matches[2]; // there is more than one subdirectory, which we do not allow
return true; 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 * 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 doesnt have access, then a login Page or NULL (for 404) is returned instead.
* *
* @param Page $page * @param Page $page
* @param User $user * @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($this->requestFile) {
// if a file was requested, we still allow view even if page doesn't have template file // 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($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($this->checkAccessDelegated($page)) return $page;
if($page->status < Page::statusUnpublished && $user->hasPermission('page-view', $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;
}
} else if($page->viewable()) { if($page->viewable()) {
// regular page view
return $page; 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 // 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() // example: a role named "edit" is created and collides with ProcessPageType::executeEdit()
$input = $this->wire()->input; $input = $this->wire()->input;
@@ -559,65 +773,14 @@ class PagesRequest extends Wire {
} }
} }
$accessTemplate = $page->getAccessTemplate(); // if we reach this point, page is not viewable
$redirectLogin = $accessTemplate ? $accessTemplate->redirectLogin : false; // 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 form then $page converts to null (404) // if we wont be presenting a login or redirect then return null (404)
if(!$redirectLogin) return null; if(empty($result)) return null;
$config = $this->config; return $result;
$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($page->id && in_array($page->id, $disallowIDs)) {
// don't allow login redirect when matching disallowIDs
$page = null;
} else if(ctype_digit("$redirectLogin")) {
// redirect login provided as a page ID
$redirectLogin = (int) $redirectLogin;
// if given ID 1 then this maps to the admin login page
if($redirectLogin === 1) $redirectLogin = $loginPageID;
$page = $this->pages->get($redirectLogin);
} else {
// redirect login provided as a URL, optionally with an {id} tag for requested page ID
$redirectLogin = str_replace('{id}', $page->id, $redirectLogin);
$this->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;
} }
/** /**
@@ -746,6 +909,30 @@ class PagesRequest extends Wire {
$this->setRedirectUrl($url, 301); $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? * Are secure pagefiles possible on this system and url?
* *
@@ -782,14 +969,22 @@ class PagesRequest extends Wire {
* Set response code and type * Set response code and type
* *
* @param int $code * @param int $code
* @param string $name * @param string $message Optional message string
* @param string $error Optional error string
* *
*/ */
protected function setResponse($code, $name , $error = '') { protected function setResponseCode($code, $message = '') {
$this->responseCode = (int) $code; $this->responseCode = (int) $code;
$this->responseName = $name; if($message) $this->responseMessage = $message;
if($error) $this->error = $error; }
/**
* 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) * - ok: successful request (200)
* - fileOk: successful file request (200) * - fileOk: successful file request (200)
* - fileNotFound: requested file not found (404) * - fileNotFound: requested file not found (404)
* - maybeRedirect: needs decision about whether to redirect (300)
* - permRedirect: permanent redirect (301) * - permRedirect: permanent redirect (301)
* - tempRedirect: temporary redirect (302) * - 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) * - 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 * @return string
* *
*/ */
public function getResponseName() { public function getResponseCodeName() {
if(!$this->responseName) return 'unknown'; return $this->responseCodeNames[$this->responseCode];
return $this->responseName;
} }
/** /**
@@ -820,12 +1020,18 @@ class PagesRequest extends Wire {
* *
* Returns integer, one of: * Returns integer, one of:
* *
* - 0: request not yet analyzed * - 0: unknown/request not yet analyzed
* - 200: successful request * - 200: successful request
* - 300: maybe redirect (needs decision)
* - 301: permanent redirect * - 301: permanent redirect
* - 302: temporary 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 * - 404: page not found
* - 405: method not allowed
* - 414: request path too long or segment too long * - 414: request path too long or segment too long
* *
* @return int * @return int
@@ -852,7 +1058,7 @@ class PagesRequest extends Wire {
* *
*/ */
public function getRequestPath() { public function getRequestPath() {
if(empty($this->requestPath)) $this->requestPath = $this->getPageRequestPath(); if(empty($this->requestPath)) $this->requestPath = $this->getRequestPagePath();
return $this->requestPath; 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 * @return int
* *
*/ */
public function getRedirectType() { 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 * Get the requested pagination number prefix
* *
* @return null * @return null|string
* *
*/ */
public function getPageNumPrefix() { 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 * @return string
* *
*/ */
public function getPageError() { public function getResponseError() {
return $this->error; 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;
}
} }

View File

@@ -24,7 +24,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
static public function getModuleInfo() { static public function getModuleInfo() {
return array( return array(
'title' => 'Languages Support - Page Names', 'title' => 'Languages Support - Page Names',
'version' => 12, 'version' => 13,
'summary' => 'Required to use multi-language page names.', 'summary' => 'Required to use multi-language page names.',
'author' => 'Ryan Cramer', 'author' => 'Ryan Cramer',
'autoload' => true, 'autoload' => true,
@@ -119,8 +119,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
$pageNumUrlPrefixes = array(); $pageNumUrlPrefixes = array();
$this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute'); $this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute');
$this->addHookBefore('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); $this->addHookAfter('PagesRequest::getPage', $this, 'hookAfterPagesRequestGetPage');
// $this->addHookAfter('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); // @todo
$this->addHookAfter('PageFinder::getQuery', $this, 'hookPageFinderGetQuery'); $this->addHookAfter('PageFinder::getQuery', $this, 'hookPageFinderGetQuery');
// identify the pageNum URL prefixes for each language // 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 // note that the hooks above are added before this so that 404s can still be handled properly
if($this->bypass) return; if($this->bypass) return;
$session = $this->wire()->session;
// verify that page path doesn't have mixed languages where it shouldn't // 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 // @todo this can be replaced since logic is now in PagesRequest/PagesPathFinder
/*
$session = $this->wire()->session;
$redirectUrl = $this->verifyPath($this->requestPath); $redirectUrl = $this->verifyPath($this->requestPath);
if($redirectUrl) { if($redirectUrl) {
@@ -174,6 +173,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
} }
return; return;
} }
*/
$language = $this->wire()->user->language; $language = $this->wire()->user->language;
$pages = $this->wire()->pages; $pages = $this->wire()->pages;
@@ -241,25 +241,34 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
* @return string * @return string
* *
*/ */
public function updatePath($path) { public function removeLanguageSegment($path) {
if($path === '/' || !strlen($path)) return $path; if($path === '/' || !strlen($path)) return $path;
$languages = $this->wire()->languages;
$trailingSlash = substr($path, -1) == '/'; $trailingSlash = substr($path, -1) == '/';
$testPath = trim($path, '/') . '/'; $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) { foreach($languages as $language) {
$name = $language->isDefault() ? $home->get("name") : $home->get("name$language"); $name = $language->isDefault() ? $home->get("name") : $home->get("name$language");
if($name == Pages::defaultRootName) continue; if($name == Pages::defaultRootName) continue;
if(!strlen($name)) continue; if(!strlen($name)) continue;
$name = "$name/"; $name = "$name/";
if(strpos($testPath, $name) === 0) { if(strpos($testPath, $name) === 0) {
$this->setLanguage = $language; // $this->setLanguage = $language;
$path = substr($testPath, strlen($name)); $path = substr($testPath, strlen($name));
} }
} }
*/
if(!$trailingSlash && $path != '/') { if(!$trailingSlash && $path != '/') {
$path = rtrim($path, '/'); $path = rtrim($path, '/');
@@ -268,6 +277,16 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
return '/' . ltrim($path, '/'); 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 * 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 * @todo this can be replaced/removed since logic is now in PagesRequest/PagesPathFinder
* *
*/
protected function verifyPath($requestPath) { protected function verifyPath($requestPath) {
$languages = $this->wire()->languages; $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 // determine if we should set the current language based on requested URL
if(!$setLanguage) { if(!$setLanguage) {
foreach($parentsAndPage as $p) { foreach($parentsAndPage as $p) {
/** @var Page $p */
$requestedPart = strtolower(array_shift($requestedParts)); $requestedPart = strtolower(array_shift($requestedParts));
if($requestedPart === $p->name) continue; if($requestedPart === $p->name) continue;
@@ -403,6 +420,21 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
return $redirectURL; 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 * 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 * @since 3.0.186
* *
*/ */
protected function ___pageNotAvailableInLanguage(Page $page, Language $language) { public function ___pageNotAvailableInLanguage(Page $page, Language $language) {
if($language) {} // ignore if($language) {} // ignore
if($page->editable()) return true; if($page->editable()) return true;
if($page->id == $this->wire()->config->http404PageID) 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 * @since 3.0.186
* @todo this can be replaced with an after() hook as PagesRequest can figure out language on its own now * @todo this can be replaced with an after() hook as PagesRequest can figure out language on its own now
* *
*/
public function hookBeforePagesRequestGetPage(HookEvent $event) { public function hookBeforePagesRequestGetPage(HookEvent $event) {
if($this->requestPath) return; // execute only once if($this->requestPath) return; // execute only once
/** @var PagesRequest $request */
$request = $event->object; $request = $event->object;
$requestPath = $request->getRequestPath(); $requestPath = $request->getRequestPath();
$this->requestPath = $requestPath; $this->requestPath = $requestPath;
if($this->isAssetPath($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 // that has no possibilty of language support
$this->bypass = true; $this->bypass = true;
} else { } else {
@@ -519,6 +549,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
$event->removeHook($event); $event->removeHook($event);
} }
*/
/** /**
* Hook in after PagesRequest::getPage * Hook in after PagesRequest::getPage
@@ -526,25 +557,25 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
* @param HookEvent $event * @param HookEvent $event
* @since 3.0.186 * @since 3.0.186
* *
*/
public function hookAfterPagesRequestGetPage(HookEvent $event) { public function hookAfterPagesRequestGetPage(HookEvent $event) {
/** @var PagesRequest $request */
$request = $event->object; $request = $event->object;
$requestPath = $request->getRequestPath(); $this->requestPath = $request->getRequestPath();
$this->requestPath = $requestPath;
$languageName = $request->getLanguageName(); $languageName = $request->getLanguageName();
if($this->isAssetPath($requestPath)) { if($this->isAssetPath($this->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 // that has no possibilty of language support
$this->bypass = true; $this->bypass = true;
} else if($languageName) { } else if($languageName) {
$language = $this->wire()->languages->get($languageName); $language = $this->wire()->languages->get($languageName);
if($language && $language->id) $this->setLanguage = $language; if($language && $language->id) $this->setLanguage($language);
} }
$event->removeHook($event); $event->removeHook($event);
} }
*/
/** /**
@@ -959,7 +990,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM
} }
if(!$language || !$language->id || !$language instanceof Language) { if(!$language || !$language->id || !$language instanceof Language) {
$language = $languages->get('default'); $language = $languages->getDefault();
} }
return $language; return $language;

View File

@@ -268,10 +268,14 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$sanitizer = $this->wire()->sanitizer; $sanitizer = $this->wire()->sanitizer;
$database = $this->wire()->database; $database = $this->wire()->database;
$languages = $this->wire()->languages;
$config = $this->wire()->config; $config = $this->wire()->config;
$table = self::dbTableName; $table = self::dbTableName;
$useUTF8 = $config->pageNameCharset === 'UTF8'; $useUTF8 = $config->pageNameCharset === 'UTF8';
if($languages && !$languages->hasPageNames()) $languages = null;
if($useUTF8) { if($useUTF8) {
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii); $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
} }
@@ -285,6 +289,13 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
'pages.status AS status' '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); $cols = implode(', ', $columns);
$sql = $sql =
@@ -303,7 +314,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
if(!$row) return false; if(!$row) return false;
foreach($row as $key => $value) { 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; $row[$key] = (int) $value;
} }
} }

View File

@@ -19,6 +19,7 @@
* @method sendFile($page, $basename) * @method sendFile($page, $basename)
* @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null) * @method string pageNotFound($page, $url, $triggerReady = false, $reason = '', \Exception $e = null)
* @method string|bool|array|Page pathHooks($path, $out) * @method string|bool|array|Page pathHooks($path, $out)
* @method void userNotAllowed(User $user, $page, PagesRequest $request)
* *
*/ */
class ProcessPageView extends Process { class ProcessPageView extends Process {
@@ -27,7 +28,7 @@ class ProcessPageView extends Process {
return array( return array(
'title' => __('Page View', __FILE__), // getModuleInfo title 'title' => __('Page View', __FILE__), // getModuleInfo title
'summary' => __('All page views are routed through this Process', __FILE__), // getModuleInfo summary 'summary' => __('All page views are routed through this Process', __FILE__), // getModuleInfo summary
'version' => 105, 'version' => 106,
'permanent' => true, 'permanent' => true,
'permission' => 'page-view', 'permission' => 'page-view',
); );
@@ -116,7 +117,6 @@ class ProcessPageView extends Process {
} }
} }
/** /**
* Render Page * Render Page
* *
@@ -135,6 +135,11 @@ class ProcessPageView extends Process {
$page->of(true); $page->of(true);
$originalPage = $page; $originalPage = $page;
$page = $request->getPageForUser($page, $user); $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) { if(!$page || !$page->id || $originalPage->id == $config->http404PageID) {
$s = 'access not allowed'; $s = 'access not allowed';
@@ -154,12 +159,11 @@ class ProcessPageView extends Process {
} }
try { try {
$requestFile = $request->getRequestFile(); $file = $request->getFile();
if($file) {
if($requestFile) {
$this->responseType = self::responseTypeFile; $this->responseType = self::responseTypeFile;
$this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $requestFile)); $this->wire()->setStatus(ProcessWire::statusDownload, array('downloadFile' => $file));
$this->sendFile($page, $requestFile); $this->sendFile($page, $file);
} else { } else {
$contentType = $this->contentTypeHeader($page, true); $contentType = $this->contentTypeHeader($page, true);
@@ -209,40 +213,25 @@ class ProcessPageView extends Process {
); );
$options = count($options) ? array_merge($defaults, $options) : $defaults; $options = count($options) ? array_merge($defaults, $options) : $defaults;
$config = $this->wire()->config; $config = $this->wire()->config;
$hooks = $this->wire()->hooks; $hooks = $this->wire()->hooks;
$input = $this->wire()->input; $input = $this->wire()->input;
$pages = $this->wire()->pages; $pages = $this->wire()->pages;
$requestPath = $request->getRequestPath(); $requestPath = $request->getRequestPath();
$pageNum = $request->getPageNum();
$pageNumPrefix = $request->getPageNumPrefix(); $pageNumPrefix = $request->getPageNumPrefix();
$setPageNum = 0;
$pageNumSegment = ''; $pageNumSegment = '';
$setPageNum = 0;
$page = null; $page = null;
$out = false; $out = false;
$this->setResponseType(self::responseTypeNoPage); $this->setResponseType(self::responseTypeNoPage);
if($pageNum > 0 && $pageNumPrefix !== null) { if($pageNumPrefix !== null) {
// there is a pagination segment present in the request path // request may have a pagination segment
$slash = substr($requestPath, -1) === '/' ? '/' : ''; $pageNumSegment = $this->renderNoPagePagination($requestPath, $pageNumPrefix, $request->getPageNum());
$requestPath = rtrim($requestPath, '/'); $setPageNum = $input->pageNum();
$pageNumSegment = $pageNumPrefix . $pageNum;
if(substr($requestPath, -1 * strlen($pageNumSegment)) === $pageNumSegment) {
// remove pagination segment from request path
$requestPath = substr($requestPath, 0, -1 * strlen($pageNumSegment));
$setPageNum = $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(!$options['ready']) $this->wire('page', $options['page']); if(!$options['ready']) $this->wire('page', $options['page']);
@@ -332,6 +321,102 @@ class ProcessPageView extends Process {
return $out; 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 * Check request for redirect and apply it when appropriate
* *