From 38b06b36f71bbbff2187f7e536a42e916b200a49 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 1 Oct 2021 13:35:40 -0400 Subject: [PATCH] Several upgrades to the LanguageSupportPageNames module, with more on the way. Also adds a new module config option (and hook) for letting you specify what should happen when a page is accessed in a language it is not available in (redirect or 404). Note that this update drops existing pages table indexes used by this module and re-creates them in a manner that significantly improves performance. For a large site, there may be a significant delay following upgrade as it creates the new table indexes. Remember to do a Modules > Refresh so that PW sees the version change. --- .../LanguageSupportPageNames.module | 608 ++++++++++++------ 1 file changed, 414 insertions(+), 194 deletions(-) diff --git a/wire/modules/LanguageSupport/LanguageSupportPageNames.module b/wire/modules/LanguageSupport/LanguageSupportPageNames.module index 7cabff9c..40939cfb 100644 --- a/wire/modules/LanguageSupport/LanguageSupportPageNames.module +++ b/wire/modules/LanguageSupport/LanguageSupportPageNames.module @@ -3,12 +3,15 @@ /** * Multi-language support page names module * - * ProcessWire 3.x, Copyright 2017 by Ryan Cramer + * ProcessWire 3.x, Copyright 2021 by Ryan Cramer * https://processwire.com * * @property int $moduleVersion * @property int $inheritInactive * @property int $useHomeSegment + * @property int $redirect404 + * + * @method bool|string|array pageNotAvailableInLanguage(Page $page, Language $language) * */ @@ -19,18 +22,18 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ static public function getModuleInfo() { - return array( - 'title' => 'Languages Support - Page Names', - 'version' => 10, - 'summary' => 'Required to use multi-language page names.', - 'author' => 'Ryan Cramer', - 'autoload' => true, - 'singular' => true, - 'requires' => array( - 'LanguageSupport', - 'LanguageSupportFields' - ) - ); + return array( + 'title' => 'Languages Support - Page Names', + 'version' => 12, + 'summary' => 'Required to use multi-language page names.', + 'author' => 'Ryan Cramer', + 'autoload' => true, + 'singular' => true, + 'requires' => array( + 'LanguageSupport', + 'LanguageSupportFields' + ) + ); } /** @@ -83,42 +86,61 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM */ 'useHomeSegment' => 0, - ); + /** + * Redirect rather than throwing 404 when page not available in particular language? + * + * - 200 to allow it to be rendered anyway. + * - 301 when it should do a permanent redirect. + * - 302 when it should do a temporary redirect. + * - 404 (or 0) if it should proceed with throwing 404. + * + */ + 'redirect404' => 0, + ); /** * Populate default config data * */ public function __construct() { - foreach(self::$defaultConfigData as $key => $value) $this->set($key, $value); + $this->setArray(self::$defaultConfigData); + parent::__construct(); } /** - * Initialize the module, save the requested path + * Initialize the module and init hooks * */ - public function init() { - $this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute'); + public function init() { + + $languages = $this->wire()->languages; + $config = $this->wire()->config; + $fields = $this->wire()->fields; + $pageNumUrlPrefixes = array(); + + $this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute'); + $this->addHookBefore('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); + // $this->addHookAfter('PagesRequest::getPage', $this, 'hookBeforePagesRequestGetPage'); // @todo $this->addHookAfter('PageFinder::getQuery', $this, 'hookPageFinderGetQuery'); - // tell ProcessPageView which segments are allowed for pagination - $config = $this->wire('config'); - $pageNumUrlPrefixes = array(); - $fields = $this->wire('fields'); - foreach($this->wire('languages') as $language) { + // identify the pageNum URL prefixes for each language + foreach($languages as $language) { $pageNumUrlPrefix = $this->get("pageNumUrlPrefix$language"); - if($pageNumUrlPrefix) $pageNumUrlPrefixes[] = $pageNumUrlPrefix; + if($pageNumUrlPrefix) $pageNumUrlPrefixes[$language->name] = $pageNumUrlPrefix; + // prevent user from creating fields with these names: $fields->setNative("name$language"); $fields->setNative("status$language"); } + + // tell ProcessPageView which segments are allowed for pagination if(count($pageNumUrlPrefixes)) { - $pageNumUrlPrefixes[] = $config->pageNumUrlPrefix; // original/fallback prefix + $pageNumUrlPrefixes['default'] = $config->pageNumUrlPrefix; // original/fallback prefix $config->set('pageNumUrlPrefixes', $pageNumUrlPrefixes); } } /** - * Attach hooks + * API ready: attach hooks * */ public function ready() { @@ -135,19 +157,33 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // bypass means the request was to something in /site/*/ that has no possibilty of language support // 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 - $redirectURL = $this->verifyPath($this->requestPath); - if($redirectURL) { - $this->wire('session')->redirect($redirectURL); + // @todo this can be replaced since logic is now in PagesRequest/PagesPathFinder + $redirectUrl = $this->verifyPath($this->requestPath); + + if($redirectUrl) { + // verifyPath says we should redirect to a different URL + if(is_array($redirectUrl)) { + list($code, $redirectUrl) = $redirectUrl; + $session->redirect($redirectUrl, (int) $code); + } else { + $session->redirect($redirectUrl); + } return; } - $page = $this->wire('page'); + $language = $this->wire()->user->language; + $pages = $this->wire()->pages; + $page = $this->wire()->page; + $process = $page->process; + $pageNumUrlPrefix = $this->get("pageNumUrlPrefix$language"); - if($page->template == 'admin' && $page->process && in_array('WirePageEditor', wireClassImplements($page->process))) { + if($process && $page->template->name === 'admin' && in_array('WirePageEditor', wireClassImplements($process))) { // when in admin, add inputs for each language's page name - if(!in_array('ProcessPageType', wireClassParents($page->process))) { + if(!in_array('ProcessPageType', wireClassParents($process))) { $page->addHookBefore('WirePageEditor::execute', $this, 'hookWirePageEditorExecute'); $this->addHookAfter('InputfieldPageName::render', $this, 'hookInputfieldPageNameRenderAfter'); $this->addHookAfter('InputfieldPageName::processInput', $this, 'hookInputfieldPageNameProcess'); @@ -157,16 +193,15 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $this->addHookBefore('LanguageSupportFields::languageDeleted', $this, 'hookLanguageDeleted'); $this->addHookBefore('LanguageSupportFields::languageAdded', $this, 'hookLanguageAdded'); - $this->wire('pages')->addHookAfter('saveReady', $this, 'hookPageSaveReady'); - $this->wire('pages')->addHookAfter('saved', $this, 'hookPageSaved'); - $this->wire('pages')->addHookAfter('setupNew', $this, 'hookPageSetupNew'); + $pages->addHookAfter('saveReady', $this, 'hookPageSaveReady'); + $pages->addHookAfter('saved', $this, 'hookPageSaved'); + $pages->addHookAfter('setupNew', $this, 'hookPageSetupNew'); - $language = $this->wire('user')->language; - $prefix = $this->get("pageNumUrlPrefix$language"); - if(strlen($prefix)) { - $config = $this->wire('config'); - $config->set('_pageNumUrlPrefix', $config->pageNumUrlPrefix); // origial/backup url prefix - $config->pageNumUrlPrefix = $prefix; + + if(strlen($pageNumUrlPrefix)) { + $config = $this->wire()->config; + $config->set('_pageNumUrlPrefix', $config->pageNumUrlPrefix); // original/backup url prefix + $config->pageNumUrlPrefix = $pageNumUrlPrefix; } } @@ -180,15 +215,20 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ protected function isAssetPath($path) { - $config = $this->wire('config'); + + $config = $this->wire()->config; + // determine if this is a asset request, for compatibility with pagefileSecure $segments = explode('/', trim($config->urls->assets, '/')); // start with [subdir]/site/assets array_pop($segments); // pop off /assets, reduce to [subdir]/site + $sitePath = '/' . implode('/', $segments) . '/'; // combine to [/subdir]/site/ $sitePath = str_replace($config->urls->root, '', $sitePath); // remove possible subdir, reduce to: site/ + // if it is a request to assets, then don't attempt to modify it $sitePath = rtrim($sitePath, '/') . '/'; $path = rtrim($path, '/') . '/'; + return strpos($path, $sitePath) === 0; } @@ -202,11 +242,15 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ public function updatePath($path) { + if($path === '/' || !strlen($path)) return $path; + + $languages = $this->wire()->languages; $trailingSlash = substr($path, -1) == '/'; $testPath = trim($path, '/') . '/'; - $home = $this->wire('pages')->get(1); - foreach($this->wire('languages') as $language) { + $home = $this->wire()->pages->get(1); + + foreach($languages as $language) { $name = $language->isDefault() ? $home->get("name") : $home->get("name$language"); if($name == Pages::defaultRootName) continue; if(!strlen($name)) continue; @@ -216,8 +260,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $path = substr($testPath, strlen($name)); } } - if(!$trailingSlash && $path != '/') $path = rtrim($path, '/'); - return $path; + + if(!$trailingSlash && $path != '/') { + $path = rtrim($path, '/'); + } + + return '/' . ltrim($path, '/'); } /** @@ -226,20 +274,25 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * Sets the user's language to that determined from the URL. * * @param string $requestPath - * @return string $redirectURL Returns URL to be redirected to, when applicable. Blank when not. + * @return string|array $redirectURL Returns one of hte following: + * - String with URL to be redirected to. + * - Array for redirect URL with redirect type, i.e. [ 302, '/path/to/redirect/to/' ] + * - Blank string when no redirect should occur. + * + * @todo this can be replaced/removed since logic is now in PagesRequest/PagesPathFinder * */ protected function verifyPath($requestPath) { - $languages = $this->wire('languages'); + $languages = $this->wire()->languages; + $page = $this->wire()->page; + $user = $this->wire()->user; + $config = $this->wire()->config; + $input = $this->wire()->input; + if(!count($languages)) return ''; + if($page->template->name === 'admin') return ''; - $page = $this->wire('page'); - if($page->template == 'admin') return ''; - - $user = $this->wire('user'); - $config = $this->wire('config'); - $requestedParts = explode('/', $requestPath); $parentsAndPage = $page->parents()->getArray(); $parentsAndPage[] = $page; @@ -248,20 +301,21 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $setLanguage = $this->setLanguage; // determine if we should set the current language based on requested URL - if(!$setLanguage) foreach($parentsAndPage as $p) { - /** @var Page $p */ + if(!$setLanguage) { + foreach($parentsAndPage as $p) { + /** @var Page $p */ + $requestedPart = strtolower(array_shift($requestedParts)); + if($requestedPart === $p->name) continue; - $requestedPart = strtolower(array_shift($requestedParts)); - if($requestedPart === $p->name) continue; - - foreach($languages as $language) { - if($language->isDefault()) { - $name = $p->get("name"); - } else { - $name = $p->get("name$language"); - } - if($name === $requestedPart) { - $setLanguage = $language; + foreach($languages as $language) { + if($language->isDefault()) { + $name = $p->get("name"); + } else { + $name = $p->get("name$language"); + } + if($name === $requestedPart) { + $setLanguage = $language; + } } } } @@ -283,17 +337,25 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // $active = $page->get("status$setLanguage") > 0 || $page->template->noLang; } // if page is inactive for a language, and it's not editable, send a 404 - if(!$active && !$page->editable() && $page->id != $config->http404PageID) { - // 404 or redirect to default language version - $this->force404 = true; - return ''; + if(!$active) { + $response = $this->pageNotAvailableInLanguage($page, $setLanguage); + if($response === false) { + // throw a 404 + $this->force404 = true; + return ''; + } else if($response === true) { + // render it + } else if($response && (is_string($response) || is_array($response))) { + // response contains redirect URL string or [ 302, 'url' ] + return $response; + } } } // set the language - if(!$setLanguage) $setLanguage = $languages->get('default'); - $user->language = $setLanguage; + if(!$setLanguage) $setLanguage = $languages->getDefault(); + $user->setLanguage($setLanguage); $this->setLanguage = $setLanguage; $languages->setLocale(); @@ -305,8 +367,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $useSlashURL = (bool) $page->template->slashUrls; $expectedPath = trim($this->getPagePath($page, $user->language), '/'); $requestPath = trim($requestPath, '/'); - $pageNum = $this->wire('input')->pageNum; - $urlSegmentStr = $this->wire('input')->urlSegmentStr; + $pageNum = $input->pageNum(); + $urlSegmentStr = $input->urlSegmentStr(); // URL segments if(strlen($urlSegmentStr)) { @@ -317,7 +379,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // page numbers if($pageNum > 1) { $prefix = $this->get("pageNumUrlPrefix$user->language"); - if(empty($prefix)) $prefix = $this->wire('config')->pageNumUrlPrefix; + if(empty($prefix)) $prefix = $config->pageNumUrlPrefix; $expectedPath .= (strlen($expectedPath) ? "/" : "") . "$prefix$pageNum"; $useSlashURL = false; } @@ -330,18 +392,53 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if(trim($expectedPath, '/') != trim($requestPath, '/')) { if($expectedPathLength && $useSlashURL) $expectedPath .= '/'; - $redirectURL = $this->wire('config')->urls->root . ltrim($expectedPath, '/'); + $redirectURL = $config->urls->root . ltrim($expectedPath, '/'); } else if($useSlashURL && !$hasSlashURL && strlen($expectedPath)) { - $redirectURL = $this->wire('config')->urls->root . $expectedPath . '/'; + $redirectURL = $config->urls->root . $expectedPath . '/'; } else if(!$useSlashURL && $hasSlashURL && $pageNum == 1) { - $redirectURL = $this->wire('config')->urls->root . $expectedPath; + $redirectURL = $config->urls->root . $expectedPath; } return $redirectURL; } + /** + * Called when page is not available in a given language + * + * Hook this method to change the behavior of what happens when a Page is requested in + * a language that it is not marked as active in. + * + * - Return boolean `true` if it should render the page anyway (like for editing user). + * - Return boolean `false` if it should throw a “404 Page Not Found”. + * - Return string containing URL like `/some/url/` if it should redirect to given URL. + * - Return array `[ 302, '/some/url/' ]` if it should do a 302 “temporary” redirect to URL. + * - Return array `[ 301, '/some/url/' ]` if it should do a 301 “permanent” redirect to URL. + * + * #pw-hooker + * + * @param Page $page + * @param Language $language + * @return bool|string|array + * @since 3.0.186 + * + */ + protected function ___pageNotAvailableInLanguage(Page $page, Language $language) { + if($language) {} // ignore + if($page->editable()) return true; + if($page->id == $this->wire()->config->http404PageID) return true; + $redirect404 = (int) $this->redirect404; + if(!$redirect404 || $redirect404 === 404) return false; + $default = $this->wire()->languages->getDefault(); + if(!$page->viewable($default)) return false; + if($redirect404 === 200) return true; + $url = $page->localUrl($default); + if($redirect404 === 301) return array(301, $url); + if($redirect404 === 302) return array(302, $url); + return false; + } + /** * Given a page and language, return the path to the page in that language * @@ -388,9 +485,70 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM return $path; } + /** + * Hook in before PagesRequest::getPage to capture and modify request path as needed + * + * @param HookEvent $event + * @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/*/ + // that has no possibilty of language support + $this->bypass = true; + } else { + // update path to remove language prefix + $requestPath = $this->updatePath($requestPath); + // determine if the update changed the request path + if($requestPath != $this->requestPath) { + // update /es/path/to/page to /path/to/page + // so that is recognized by PagesRequest + $request->setRequestPath($requestPath); + } + } + + $event->removeHook($event); + } + + /** + * Hook in after PagesRequest::getPage + * + * @param HookEvent $event + * @since 3.0.186 + * + public function hookAfterPagesRequestGetPage(HookEvent $event) { + + $request = $event->object; + $requestPath = $request->getRequestPath(); + $this->requestPath = $requestPath; + $languageName = $request->getLanguageName(); + + if($this->isAssetPath($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; + } + + $event->removeHook($event); + } + */ + /** - * Hook in before ProcesssPageView::execute to capture and modify $_GET[it] as needed + * Hook in before ProcesssPageView::execute * * @param HookEvent $event * @@ -398,16 +556,9 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM public function hookProcessPageViewExecute(HookEvent $event) { /** @var ProcessPageView $process */ $process = $event->object; + // tell it to delay redirects until after the $page API var is known/populated + // this ensures our hook before PagesRequest::getPage() will always be called $process->setDelayRedirects(true); - // save now, since ProcessPageView removes $_GET['it'] when it executes - $it = isset($_GET['it']) ? $_GET['it'] : ''; - $this->requestPath = $it; - if($this->isAssetPath($it)) { - $this->bypass = true; - } else { - $it = $this->updatePath($it); - if($it != $this->requestPath) $_GET['it'] = $it; - } } /** @@ -422,10 +573,6 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if($this->force404) { $this->force404 = false; // prevent another 404 on the 404 page throw new Wire404Exception('Not available in requested language', Wire404Exception::codeLanguage); - // $page = $event->wire('page'); - // if(!$page || ($page->id != $event->wire('config')->http404PageID)) { - // throw new Wire404Exception(); - // } } } @@ -438,17 +585,29 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ public function hookPageViewable(HookEvent $event) { + + // if page was already determined not viewable then do nothing further if(!$event->return) return; - /** @var Page $page */ - $page = $event->object; - // if(wire('user')->isSuperuser() || $page->editable()) return; - /** @var Language $language */ - $language = $event->arguments(0); + + $page = $event->object; /** @var Page $page */ + $language = $event->arguments(0); /** @var Language|Field|Pagefile|string|bool $language */ + if(!$language) return; - if(is_string($language)) $language = $this->wire('languages')->get($this->wire('sanitizer')->pageNameUTF8($language)); - if(!$language instanceof Language) return; // some other non-language argument - if($language->isDefault()) return; // we accept the result of the original viewable() call - $status = $page->get("status$language"); + + if(is_string($language)) { + // can be a language name or a field name (we only want language name) + $language = $this->wire()->sanitizer->pageNameUTF8($language); + $language = strlen($language) ? $this->wire()->languages->get($language) : null; + } + + // some other non-language argument was sent to Page::viewable() + if(!$language instanceof Language) return; + + // we accept the result of the original viewable() call for default language + if($language->isDefault()) return; + + $status = (int) $page->get("status$language"); + $event->return = $status > 0 && $status < Page::statusUnpublished; } @@ -462,9 +621,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** @var WirePageEditor $editor */ $editor = $event->object; $page = $editor->getPage(); - if($page && $page->id == 1) { - if($page->name == Pages::defaultRootName) $page->name = ''; - } + + // filter out everything but homepage (id=1) + if(!$page || !$page->id || $page->id > 1) return; + + // if homepage has the defaultRootName then make the name blank + if($page->name == Pages::defaultRootName) $page->name = ''; } /** @@ -479,16 +641,17 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** @var InputfieldPageName $inputfield */ $inputfield = $event->object; + if($inputfield->languageSupportLabel) return; // prevent recursion - $page = $this->process instanceof WirePageEditor ? $this->process->getPage() : $this->wire('pages')->newNullPage(); + $process = $this->process; + $page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage(); if(!$page->id && $inputfield->editPage) $page = $inputfield->editPage; $template = $page->template ? $page->template : null; if($template && $template->noLang) return; - /** @var Languages $languages */ - $user = $this->wire('user'); - $languages = $this->wire('languages'); + $user = $this->wire()->user; + $languages = $this->wire()->languages; $savedLanguage = $user->language; $savedValue = $inputfield->attr('value'); $savedName = $inputfield->attr('name'); @@ -499,7 +662,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $out = ''; $language = $languages->getDefault(); - $user->language = $language; + $user->setLanguage($language); $inputfield->languageSupportLabel = $language->get('title|name'); $out .= $inputfield->render(); $editable = true; @@ -508,7 +671,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // add labels and inputs for other languages foreach($languages as $language) { if($language->isDefault()) continue; - $user->language = $language; + $user->setLanguage($language); $value = $page->get("name$language"); if(is_null($value)) $value = $savedValue; $id = "$savedID$language"; @@ -531,7 +694,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM } // restore language that was saved in the 'before' hook - $user->language = $savedLanguage; + $user->setLanguage($savedLanguage); // restore Inputfield values back to what they were $inputfield->attr('name', $savedName); @@ -552,18 +715,15 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM */ public function hookInputfieldPageNameProcess(HookEvent $event) { - /** @var InputfieldPageName $inputfield */ - $inputfield = $event->object; - //$page = $this->process == 'ProcessPageEdit' ? $this->process->getPage() : new NullPage(); - /** @var WirePageEditor $process */ - $process = $this->process; - /** @var Page $page */ - $page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage(); + $inputfield = $event->object; /** @var InputfieldPageName $inputfield */ + $process = $this->process; /** @var WirePageEditor $process */ + $page = $process instanceof WirePageEditor ? $process->getPage() : new NullPage(); /** @var Page $page */ + if($page->id && !$page->editable('name', false)) return; // name is not editable - $input = $event->arguments[0]; - /** @var Languages $languages */ - $languages = $this->wire('languages'); - $sanitizer = $this->wire('sanitizer'); + + $input = $event->arguments[0]; /** @var WireInputData $input */ + $languages = $this->wire()->languages; + $sanitizer = $this->wire()->sanitizer; foreach($languages as $language) { @@ -576,8 +736,11 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if($page->get($key) != $value) { $inputfield->trackChange($key); $inputfield->trackChange('value'); - if($page->id) $page->set($key, $value); - else $page->setQuietly($key, $value); + if($page->id) { + $page->set($key, $value); + } else { + $page->setQuietly($key, $value); + } } // set language page name @@ -592,7 +755,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if($value == $page->get($key)) continue; $parentID = $page->parent_id; - if(!$parentID) $parentID = (int) $this->wire('input')->post('parent_id'); + if(!$parentID) $parentID = (int) $this->wire()->input->post('parent_id'); if(!$this->checkLanguagePageName($language, $page, $parentID, $value, $inputfield)) continue; if($page->id) { @@ -621,8 +784,8 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if(!strlen($value)) return true; - if($this->wire('config')->pageNameCharset == 'UTF8') { - $value = $this->wire('sanitizer')->pageName($value, Sanitizer::toAscii); + if($this->wire()->config->pageNameCharset == 'UTF8') { + $value = $this->wire()->sanitizer->pageName($value, Sanitizer::toAscii); } $sql = @@ -634,7 +797,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM "OR ($nameKey=:newName2)" . // or lang name is same as requested one ")"; - $query = $this->wire('database')->prepare($sql); + $query = $this->wire()->database->prepare($sql); $query->bindValue(':parent_id', $parentID, \PDO::PARAM_INT); $query->bindValue(':newName', $value); $query->bindValue(':newName2', $value); @@ -676,9 +839,9 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if(!empty($options['findAll'])) return; // don't apply exclusions when output formatting is off - if(!$this->wire('pages')->outputFormatting) return; + if(!$this->wire()->pages->outputFormatting) return; - $language = $this->wire('user')->language; + $language = $this->wire()->user->language; if(!$language || $language->isDefault()) return; $status = "status" . (int) $language->id; @@ -752,7 +915,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** @var Page $page */ $page = $event->object; $language = $this->getLanguage($event->arguments(0)); - $event->return = $this->wire('config')->urls->root . ltrim($this->getPagePath($page, $language), '/'); + $event->return = $this->wire()->config->urls->root . ltrim($this->getPagePath($page, $language), '/'); } /** @@ -767,7 +930,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM public function hookPageLocalHttpUrl(HookEvent $event) { $this->hookPageLocalUrl($event); $url = $event->return; - $event->return = $this->wire('input')->scheme() . "://" . $this->wire('config')->httpHost . $url; + $event->return = $this->wire()->input->scheme() . "://" . $this->wire()->config->httpHost . $url; } /** @@ -783,15 +946,20 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if($language instanceof Language) return $language; $language = ''; } + + $languages = $this->wire()->languages; if($language && (is_string($language) || is_int($language))) { - if(ctype_digit("$language")) $language = (int) $language; - else $language = $this->wire('sanitizer')->pageNameUTF8($language); - $language = $this->wire("languages")->get($language); + if(ctype_digit("$language")) { + $language = (int) $language; + } else { + $language = $this->wire()->sanitizer->pageNameUTF8($language); + } + $language = $languages->get($language); } if(!$language || !$language->id || !$language instanceof Language) { - $language = $this->wire('languages')->get('default'); + $language = $languages->get('default'); } return $language; @@ -813,11 +981,11 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $name = "name" . (int) $language->id; $status = "status" . (int) $language->id; - $database = $this->wire('database'); + $database = $this->wire()->database; $errors = 0; $sqls = array( "Add column $name" => "ALTER TABLE pages ADD $name VARCHAR(" . Pages::nameMaxLength . ") CHARACTER SET ascii", - "Add index for $name" => "ALTER TABLE pages ADD UNIQUE {$name}_parent_id ($name, parent_id)", + "Add index for $name" => "ALTER TABLE pages ADD INDEX parent_{$name} (parent_id, $name)", "Add column $status" => "ALTER TABLE pages ADD $status INT UNSIGNED NOT NULL DEFAULT " . Page::statusOn, ); @@ -854,9 +1022,9 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if(!$language->id || $language->name == 'default') return; $name = "name" . (int) $language->id; $status = "status" . (int) $language->id; - $database = $this->wire('database'); + $database = $this->wire()->database; try { - $database->exec("ALTER TABLE pages DROP INDEX {$name}_parent_id"); + $database->exec("ALTER TABLE pages DROP INDEX parent_$name"); $database->exec("ALTER TABLE pages DROP $name"); $database->exec("ALTER TABLE pages DROP $status"); } catch(\Exception $e) { @@ -891,9 +1059,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** @var Pages $pages */ $pages = $event->object; - - /** @var Sanitizer $sanitizer */ - $sanitizer = $this->wire('sanitizer'); + $sanitizer = $this->wire()->sanitizer; /** @var array $extraData */ $extraData = $event->return; @@ -904,7 +1070,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM 'Permission', 'PermissionPage', 'Language', 'LanguagePage', ); - $pageNameCharset = $this->wire('config')->pageNameCharset; + $pageNameCharset = $this->wire()->config->pageNameCharset; $isCloning = $pages->editor()->isCloning(); if(!is_array($extraData)) $extraData = array(); @@ -973,24 +1139,27 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // account for possibility that a new page with non-default language name/title exists // this prevents an exception from being thrown by Pages::save - $user = $this->wire('user'); + $user = $this->wire()->user; + $config = $this->wire()->config; + $sanitizer = $this->wire()->sanitizer; $userTrackChanges = $user->trackChanges(); $userLanguage = $user->language; + if($userTrackChanges) $user->setTrackChanges(false); - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { if($language->isDefault()) continue; - $user->language = $language; + $user->setLanguage($language); $name = $page->get("name$language"); if(strlen($name)) $page->name = $name; $title = $page->title; if(strlen($title)) { $page->title = $title; if(!$page->name) { - if($this->wire('config')->pageNameCharset === 'UTF8') { - $page->name = $this->wire('sanitizer')->pageNameUTF8($title); + if($config->pageNameCharset === 'UTF8') { + $page->name = $sanitizer->pageNameUTF8($title); } else { - $page->name = $this->wire('sanitizer')->pageName($title, Sanitizer::translate); + $page->name = $sanitizer->pageName($title, Sanitizer::translate); } } } @@ -998,7 +1167,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM } // restore user to previous state - $user->language = $userLanguage; + $user->setLanguage($userLanguage); if($userTrackChanges) $user->setTrackChanges(true); } @@ -1011,12 +1180,12 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM public function hookPageSaved(HookEvent $event) { // The setLanguage may get lost upon some page save events, so this restores that // $this->user->language = $this->setLanguage; - $page = $event->arguments(0); - $sanitizer = $this->wire('sanitizer'); + $page = $event->arguments(0); /** @var Page $page */ + $sanitizer = $this->wire()->sanitizer; if(!$page->namePrevious) { // go into this only if we know the renamed hook hasn't already been called $renamed = false; - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { if($language->isDefault()) continue; $namePrevious = $page->get("-name$language"); if(!$namePrevious) continue; @@ -1027,7 +1196,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM } } // trigger renamed hook if one of the language names changed - if($renamed) $this->wire('pages')->renamed($page); + if($renamed) $this->wire()->pages->renamed($page); } } @@ -1051,9 +1220,10 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM */ public function getPagePathLanguage($path, Page $page = null) { - $languages = $this->wire('languages'); + $languages = $this->wire()->languages; + $pages = $this->wire()->pages; - if(!$page || !$page->id) $page = $this->wire('pages')->getByPath($path, array( + if(!$page || !$page->id) $page = $pages->getByPath($path, array( 'useLanguages' => true, 'useHistory' => true )); @@ -1065,19 +1235,21 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if(!strlen($path)) return $languages->getDefault(); // first check entire path for a match - if($page->id) foreach($languages as $language) { - $languages->setLanguage($language); - if($path === trim($page->path(), '/')) $foundLanguage = $language; - $languages->unsetLanguage(); - if($foundLanguage) break; + if($page->id) { + foreach($languages as $language) { + $languages->setLanguage($language); + if($path === trim($page->path(), '/')) $foundLanguage = $language; + $languages->unsetLanguage(); + if($foundLanguage) break; + } } if($foundLanguage) return $foundLanguage; // if we get to this point, then we'll be checking the first segment and last segment $parts = explode('/', $path); - $homepageID = $this->wire('config')->rootPageID; - $homepage = $this->wire('pages')->get($homepageID); + $homepageID = $this->wire()->config->rootPageID; + $homepage = $pages->get($homepageID); $firstPart = reset($parts); $lastPart = end($parts); @@ -1123,14 +1295,14 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM if($info['version'] == $this->moduleVersion) return; } - $database = $this->wire('database'); + $database = $this->wire()->database; // version 3 to 4 check: addition of language-specific status columns $query = $database->prepare("SHOW COLUMNS FROM pages WHERE Field LIKE 'status%'"); $query->execute(); if($query->rowCount() < 2) { - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { if($language->isDefault()) continue; $status = "status" . (int) $language->id; $database->exec("ALTER TABLE pages ADD $status INT UNSIGNED NOT NULL DEFAULT " . Page::statusOn); @@ -1140,9 +1312,10 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM // save module version in config data if($info['version'] != $this->moduleVersion) { - $data = $this->wire('modules')->getModuleConfigData($this); + $modules = $this->wire()->modules; + $data = $modules->getModuleConfigData($this); $data['moduleVersion'] = $info['version']; - $this->wire('modules')->saveModuleConfigData($this, $data); + $modules->saveModuleConfigData($this, $data); } } @@ -1150,24 +1323,27 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM /** * Module interactive configuration fields * - * @param array $data - * @return InputfieldWrapper + * @param InputfieldWrapper $inputfields * */ - public function getModuleConfigInputfields(array $data) { + public function getModuleConfigInputfields(InputfieldWrapper $inputfields) { - $module = $this->wire('modules')->get('LanguageSupportPageNames'); + $modules = $this->wire()->modules; + $config = $this->wire()->config; + + /** @var LanguageSupportPageNames $module */ + $module = $modules->get('LanguageSupportPageNames'); $module->checkModuleVersion(true); - $inputfields = $this->wire(new InputfieldWrapper()); - $config = $this->wire('config'); + $defaultUrlPrefix = $config->get('_pageNumUrlPrefix|pageNumUrlPrefix'); - foreach($this->wire('languages') as $language) { - $f = $this->wire('modules')->get('InputfieldName'); + foreach($this->wire()->languages as $language) { + /** @var InputfieldName $f */ + $f = $modules->get('InputfieldName'); $name = "pageNumUrlPrefix$language"; - if($language->isDefault() && empty($data[$name])) $data[$name] = $defaultUrlPrefix; + if($language->isDefault() && !$this->get($name)) $this->set($name, $defaultUrlPrefix); $f->attr('name', $name); - $f->attr('value', isset($data[$name]) ? $data[$name] : ''); + $f->attr('value', $this->get($name)); $f->label = "$language->title ($language->name) - " . $this->_('Page number prefix for pagination'); $f->description = sprintf( $this->_('The page number is appended to this word in paginated URLs for this language. If omitted, "%s" will be used.'), @@ -1177,18 +1353,30 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM $inputfields->add($f); } - $input = $this->wire('modules')->get('InputfieldRadios'); - $input->attr('name', 'useHomeSegment'); - $input->label = $this->_('Default language homepage URL is same as root URL?'); // label for the home segment option - $input->description = $this->_('Choose **Yes** if you want the homepage of your default language to be served by the root URL **/** (recommended). Choose **No** if you want your root URL to perform a redirect to **/name/** (where /name/ is the default language name of your homepage).'); // description for the home segment option - $input->notes = $this->_('This setting only affects the homepage behavior. If you select No, you must also make sure your homepage has a name defined for the default language.'); // notes for the home segment option - $input->addOption(0, $this->_('Yes - Root URL serves default language homepage (recommended)')); - $input->addOption(1, $this->_('No - Root URL performs a redirect to: /name/')); - $input->attr('value', empty($data['useHomeSegment']) ? 0 : 1); - $input->collapsed = Inputfield::collapsedYes; - $inputfields->add($input); - - return $inputfields; + /** @var InputfieldRadios $f */ + $f = $modules->get('InputfieldRadios'); + $f->attr('name', 'useHomeSegment'); + $f->label = $this->_('Default language homepage URL is same as root URL?'); // label for the home segment option + $f->description = $this->_('Choose **Yes** if you want the homepage of your default language to be served by the root URL **/** (recommended). Choose **No** if you want your root URL to perform a redirect to **/name/** (where /name/ is the default language name of your homepage).'); // description for the home segment option + $f->notes = $this->_('This setting only affects the homepage behavior. If you select No, you must also make sure your homepage has a name defined for the default language.'); // notes for the home segment option + $f->addOption(0, $this->_('Yes - Root URL serves default language homepage (recommended)')); + $f->addOption(1, $this->_('No - Root URL performs a redirect to: /name/')); + $f->attr('value', (int) $this->useHomeSegment); + $inputfields->add($f); + + /** @var InputfieldRadios $f */ + $f = $modules->get('InputfieldRadios'); + $f->attr('name', 'redirect404'); + $f->label = $this->_('Behavior when page not available in requested language (but is available in default language)'); + $f->notes = $this->_('This setting does not apply if the page is editable to the user as it will always be available for preview purposes.'); + $f->addOption(0, $this->_('Throw a 404 (page not found) error - default behavior')); + $f->addOption(200, $this->_('Allow it to be rendered for language anyway (if accessed directly by URL)')); + $f->addOption(301, $this->_('Perform a 301 (permanent) redirect to the page in default language')); + $f->addOption(302, $this->_('Perform a 302 (temporary) redirect to the page in default language')); + $val = (int) $this->redirect404; + if($val === 404) $val = 0; + $f->val($val); + $inputfields->add($f); } /** @@ -1196,8 +1384,7 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ public function ___install() { - - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { $this->languageAdded($language); } @@ -1208,9 +1395,42 @@ class LanguageSupportPageNames extends WireData implements Module, ConfigurableM * */ public function ___uninstall() { - foreach($this->wire('languages') as $language) { + foreach($this->wire()->languages as $language) { $this->languageDeleted($language); } } + /** + * Upgrade the module + * + * @param $fromVersion + * @param $toVersion + * + */ + public function ___upgrade($fromVersion, $toVersion) { + if($fromVersion && $toVersion) {} // ignore + $languages = $this->wire()->languages; + $database = $this->wire()->database; + $sqls = array(); + foreach($languages as $language) { + if($language->isDefault()) continue; + $name = 'name' . $language->id; + if(!$database->columnExists("pages", $name)) continue; + if($database->indexExists("pages", "{$name}_parent_id")) { + $sqls[] = "ALTER TABLE pages DROP INDEX {$name}_parent_id"; + } + if(!$database->indexExists("pages", "parent_{$name}")) { + $sqls[] = "ALTER TABLE pages ADD INDEX parent_{$name}(parent_id, $name)"; + } + } + foreach($sqls as $sql) { + try { + $query = $database->prepare($sql); + $query->execute(); + } catch(\Exception $e) { + $this->warning($e->getMessage(), Notice::superuser); + } + } + } + }