Blame | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Multi-language support page names module** ProcessWire 3.x, Copyright 2017 by Ryan Cramer* https://processwire.com** @property int $moduleVersion* @property int $inheritInactive* @property int $useHomeSegment**/class LanguageSupportPageNames extends WireData implements Module, ConfigurableModule {/*** Return information about the module**/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'));}/*** The path that was requested, before processing**/protected $requestPath = '';/*** Language that should be set for this request**/protected $setLanguage = null;/*** Whether to force a 404 when ProcessPageView runs**/protected $force404 = null;/*** Whether to bypass the functions provided by this module (like for a secure pagefile request)**/protected $bypass = false;/*** Default configuration data**/static protected $defaultConfigData = array(/*** module version, for schema changes when necessary**/'moduleVersion' => 0,/*** Whether an 'inactive' state (status123=0) should inherit to children** Note: we don't have a reasonable way to make this work with PageFinder queries,* so it is not anything more than a placeholder at present.**/'inheritInactive' => 0,/*** Whether or not the default language homepage should be served by a language segment.**/'useHomeSegment' => 0,);/*** Populate default config data**/public function __construct() {foreach(self::$defaultConfigData as $key => $value) $this->set($key, $value);}/*** Initialize the module, save the requested path**/public function init() {$this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute');$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) {$pageNumUrlPrefix = $this->get("pageNumUrlPrefix$language");if($pageNumUrlPrefix) $pageNumUrlPrefixes[] = $pageNumUrlPrefix;$fields->setNative("name$language");$fields->setNative("status$language");}if(count($pageNumUrlPrefixes)) {$pageNumUrlPrefixes[] = $config->pageNumUrlPrefix; // original/fallback prefix$config->set('pageNumUrlPrefixes', $pageNumUrlPrefixes);}}/*** Attach hooks**/public function ready() {$this->checkModuleVersion();$this->addHookAfter('Page::path', $this, 'hookPagePath');$this->addHookAfter('Page::viewable', $this, 'hookPageViewable');$this->addHookBefore('Page::render', $this, 'hookPageRender');$this->addHook('Page::localName', $this, 'hookPageLocalName');$this->addHook('Page::localUrl', $this, 'hookPageLocalUrl');$this->addHook('Page::localHttpUrl', $this, 'hookPageLocalHttpUrl');$this->addHook('Page::localPath', $this, 'hookPageLocalPath');// 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 properlyif($this->bypass) return;// 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);return;}$page = $this->wire('page');if($page->template == 'admin' && $page->process && in_array('WirePageEditor', wireClassImplements($page->process))) {// when in admin, add inputs for each language's page nameif(!in_array('ProcessPageType', wireClassParents($page->process))) {$page->addHookBefore('WirePageEditor::execute', $this, 'hookWirePageEditorExecute');$this->addHookAfter('InputfieldPageName::render', $this, 'hookInputfieldPageNameRenderAfter');$this->addHookAfter('InputfieldPageName::processInput', $this, 'hookInputfieldPageNameProcess');}}$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');$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;}}/*** Is the given path a site assets path? (i.e. /site/)** Determines whether this is a path we should attempt to perform any language processing on.** @param string $path* @return bool**/protected function isAssetPath($path) {$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/assetsarray_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;}/*** Given a page path, return an updated version that lacks the language segment** It extracts the language segment and uses that to later set the language** @param string $path* @return string**/public function updatePath($path) {if($path === '/' || !strlen($path)) return $path;$trailingSlash = substr($path, -1) == '/';$testPath = trim($path, '/') . '/';$home = $this->wire('pages')->get(1);foreach($this->wire('languages') as $language) {$name = $language->isDefault() ? $home->get("name") : $home->get("name$language");if($name == Pages::defaultRootName) continue;if(!strlen($name)) continue;$name = "$name/";if(strpos($testPath, $name) === 0) {$this->setLanguage = $language;$path = substr($testPath, strlen($name));}}if(!$trailingSlash && $path != '/') $path = rtrim($path, '/');return $path;}/*** Determine language from requested path, and if a redirect needs to be performed** 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.**/protected function verifyPath($requestPath) {$languages = $this->wire('languages');if(!count($languages)) 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;array_shift($parentsAndPage); // shift off the homepage$redirectURL = '';$setLanguage = $this->setLanguage;// determine if we should set the current language based on requested URLif(!$setLanguage) foreach($parentsAndPage as $p) {/** @var Page $p */$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;}}}// check to see if the $page or any of its parents has an inactive status for the $setLanguageif($setLanguage && !$setLanguage->isDefault()) {$active = true;if($this->inheritInactive) {// inactive status on a parent inherits through to childrenforeach($parentsAndPage as $p) {$status = $p->get("status$setLanguage");if(!$status) $active = false;}} else {// inactive status only applies to the page itself$active = $page->get("status$setLanguage") > 0;// https://github.com/processwire/processwire-issues/issues/463// $active = $page->get("status$setLanguage") > 0 || $page->template->noLang;}// if page is inactive for a language, and it's not editable, send a 404if(!$active && !$page->editable() && $page->id != $config->http404PageID) {// 404 or redirect to default language version$this->force404 = true;return '';}}// set the languageif(!$setLanguage) $setLanguage = $languages->get('default');$user->language = $setLanguage;$this->setLanguage = $setLanguage;$languages->setLocale();// if $page is the 404 page, exit out nowif($page->id == $config->http404PageID) return '';// determine if requested URL was correct or if we need to redirect$hasSlashURL = substr($requestPath, -1) == '/';$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;// URL segmentsif(strlen($urlSegmentStr)) {$expectedPath .= '/' . $urlSegmentStr;$useSlashURL = $hasSlashURL;}// page numbersif($pageNum > 1) {$prefix = $this->get("pageNumUrlPrefix$user->language");if(empty($prefix)) $prefix = $this->wire('config')->pageNumUrlPrefix;$expectedPath .= (strlen($expectedPath) ? "/" : "") . "$prefix$pageNum";$useSlashURL = false;}$expectedPathLength = strlen($expectedPath);if($expectedPathLength) {$requestPath = substr($requestPath, 0, $expectedPathLength);}if(trim($expectedPath, '/') != trim($requestPath, '/')) {if($expectedPathLength && $useSlashURL) $expectedPath .= '/';$redirectURL = $this->wire('config')->urls->root . ltrim($expectedPath, '/');} else if($useSlashURL && !$hasSlashURL && strlen($expectedPath)) {$redirectURL = $this->wire('config')->urls->root . $expectedPath . '/';} else if(!$useSlashURL && $hasSlashURL && $pageNum == 1) {$redirectURL = $this->wire('config')->urls->root . $expectedPath;}return $redirectURL;}/*** Given a page and language, return the path to the page in that language** @param Page $page* @param Language $language* @return string**/public function getPagePath(Page $page, Language $language) {$isDefault = $language->isDefault();if(!$isDefault && $page->template && $page->template->noLang) {$language = $this->wire()->languages->getDefault();$isDefault = true;}if($page->id == 1) {// special case: homepage$name = $isDefault ? '' : $page->get("name$language");if($isDefault && $this->useHomeSegment) $name = $page->name;if($name == Pages::defaultRootName || !strlen($name)) return '/';return $page->template->slashUrls ? "/$name/" : "/$name";}$path = '';foreach($page->parents() as $parent) {$name = $isDefault ? $parent->get("name") : $parent->get("name$language|name");if($parent->id == 1) {// bypass ProcessWire's default homepage name of 'home', as we don't want it in URLsif($name == Pages::defaultRootName) continue;// avoid having default language name inherited at homepage level// if($isDefault && $name === $parent->get("name")) continue;}if(strlen($name)) $path .= "/" . $name;}$name = $page->get("name$language|name");$path = strlen($name) ? "$path/$name/" : "$path/";if(!$page->template->slashUrls && $path != '/') $path = rtrim($path, '/');return $path;}/*** Hook in before ProcesssPageView::execute to capture and modify $_GET[it] as needed** @param HookEvent $event**/public function hookProcessPageViewExecute(HookEvent $event) {/** @var ProcessPageView $process */$process = $event->object;$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;}}/*** Hook in before ProcesssPageView::render to throw 404 when appropriate** @param HookEvent $event* @throws WireException**/public function hookPageRender(HookEvent $event) {if($event) {}if($this->force404) {$this->force404 = false; // prevent another 404 on the 404 pagethrow new Wire404Exception('Not available in requested language', Wire404Exception::codeLanguage);// $page = $event->wire('page');// if(!$page || ($page->id != $event->wire('config')->http404PageID)) {// throw new Wire404Exception();// }}}/*** Hook in after ProcesssPageView::viewable account for specific language versions** May be passed a Language name or page to check viewable for that language** @param HookEvent $event**/public function hookPageViewable(HookEvent $event) {if(!$event->return) return;/** @var Page $page */$page = $event->object;// if(wire('user')->isSuperuser() || $page->editable()) return;/** @var Language $language */$language = $event->arguments(0);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 argumentif($language->isDefault()) return; // we accept the result of the original viewable() call$status = $page->get("status$language");$event->return = $status > 0 && $status < Page::statusUnpublished;}/*** Hook into WirePageEditor (i.e. ProcessPageEdit) to remove the non-applicable default home name of 'home'** @param HookEvent $event**/public function hookWirePageEditorExecute(HookEvent $event) {/** @var WirePageEditor $editor */$editor = $event->object;$page = $editor->getPage();if($page && $page->id == 1) {if($page->name == Pages::defaultRootName) $page->name = '';}}/*** Hook into the page name render for when in ProcessPageEdit** Adds additional inputs for each language** @param HookEvent $event**/public function hookInputfieldPageNameRenderAfter(HookEvent $event) {/** @var InputfieldPageName $inputfield */$inputfield = $event->object;if($inputfield->languageSupportLabel) return; // prevent recursion$page = $this->process instanceof WirePageEditor ? $this->process->getPage() : $this->wire('pages')->newNullPage();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');$savedLanguage = $user->language;$savedValue = $inputfield->attr('value');$savedName = $inputfield->attr('name');$savedID = $inputfield->attr('id');$trackChanges = $inputfield->trackChanges();$inputfield->setTrackChanges(false);$checkboxLabel = $this->_('Active?');$out = '';$language = $languages->getDefault();$user->language = $language;$inputfield->languageSupportLabel = $language->get('title|name');$out .= $inputfield->render();$editable = true;if($page->id && !$page->editable('name', false)) $editable = false;// add labels and inputs for other languagesforeach($languages as $language) {if($language->isDefault()) continue;$user->language = $language;$value = $page->get("name$language");if(is_null($value)) $value = $savedValue;$id = "$savedID$language";$name = "$savedName$language";$label = $language->get('title|name');$inputfield->languageSupportLabel = $label;$inputfield->attr('id', $id);$inputfield->attr('name', $name);$inputfield->attr('value', $value);$inputfield->checkboxName = "status" . $language->id;$inputfield->checkboxValue = 1;$inputfield->checkboxLabel = $checkboxLabel;if($page->id > 0) $inputfield->checkboxChecked = $page->get($inputfield->checkboxName) > 0;if(!$editable) $inputfield->attr('disabled', 'disabled');$out .= $inputfield->render();}// restore language that was saved in the 'before' hook$user->language = $savedLanguage;// restore Inputfield values back to what they were$inputfield->attr('name', $savedName);$inputfield->attr('savedID', $savedID);$inputfield->attr('value', $savedValue);$inputfield->setTrackChanges($trackChanges);$event->return = $out;}/*** Process the input data from hookInputfieldPageNameRender** @todo Just move this to the InputfieldPageName module rather than using hooks** @param HookEvent $event**/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();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');foreach($languages as $language) {if($language->isDefault()) continue;if(!$languages->editable($language)) continue;// set language status$key = "status" . (int) $language->id;$value = (int) $input->{"$key$inputfield->checkboxSuffix"};if($page->get($key) != $value) {$inputfield->trackChange($key);$inputfield->trackChange('value');if($page->id) $page->set($key, $value);else $page->setQuietly($key, $value);}// set language page name$name = $inputfield->attr('name') . $language;$value = $sanitizer->pageNameUTF8($input->$name);// if it matches the value for the default language, avoid double storing itif($value === $page->name) $value = '';// if it matches the value already on the page, then no need to go further$key = "name$language";if($value == $page->get($key)) continue;$parentID = $page->parent_id;if(!$parentID) $parentID = (int) $this->wire('input')->post('parent_id');if(!$this->checkLanguagePageName($language, $page, $parentID, $value, $inputfield)) continue;if($page->id) {$page->set($key, $value);} else {$page->setQuietly($key, $value); // avoid non-template exception when new page}}}/*** Check changed page name for given language** @param Language $language* @param Page $page* @param int $parentID* @param string $value New page name* @param Wire|null $errorTarget Object to send error to (Inputfield likely)* @return bool True if all good, false if not**/public function checkLanguagePageName(Language $language, Page $page, $parentID, $value, Wire $errorTarget = null) {// verify that it does not conflict with another page inheriting name from default language$isValid = true;$nameKey = "name$language->id";if(!strlen($value)) return true;if($this->wire('config')->pageNameCharset == 'UTF8') {$value = $this->wire('sanitizer')->pageName($value, Sanitizer::toAscii);}$sql ="SELECT id, name, $nameKey FROM pages " ."WHERE parent_id=:parent_id " ."AND id!=:id " ."AND (" ."(name=:newName AND $nameKey IS NULL) " . // default name matches and lang name inherits it (is null)"OR ($nameKey=:newName2)" . // or lang name is same as requested one")";$query = $this->wire('database')->prepare($sql);$query->bindValue(':parent_id', $parentID, \PDO::PARAM_INT);$query->bindValue(':newName', $value);$query->bindValue(':newName2', $value);$query->bindValue(':id', $page->id, \PDO::PARAM_INT);try {$query->execute();$row = $query->fetch(\PDO::FETCH_ASSOC);if($row) {$isValid = false;if($errorTarget) $errorTarget->error(sprintf($this->_('A sibling page (id=%1$d) is already using name "%2$s" for language: %3$s'),$row['id'], $value, $language->get('title|name')));}} catch(\Exception $e) {$this->error($e->getMessage());$isValid = false;}$query->closeCursor();return $isValid;}/*** Hook into PageFinder::getQuery to add language status check** @param HookEvent $event**/public function hookPageFinderGetQuery(HookEvent $event) {$query = $event->return;/** @var PageFinder $pageFinder */$pageFinder = $event->object;$options = $pageFinder->getOptions();// don't enforce language status check with findAll is activeif(!empty($options['findAll'])) return;// don't apply exclusions when output formatting is offif(!$this->wire('pages')->outputFormatting) return;$language = $this->wire('user')->language;if(!$language || $language->isDefault()) return;$status = "status" . (int) $language->id;$query->where("pages.$status>0");}/*** Hook into Page::path to localize path for current language** @param HookEvent $event**/public function hookPagePath(HookEvent $event) {/** @var Page $page */$page = $event->object;if($page->template->name == 'admin') return;$language = $this->wire()->user->language;if(!$language) $language = $this->wire()->languages->getDefault();$event->return = $this->getPagePath($page, $language);}/*** Add a Page::localName function with optional $language as argument** event param Language|string|int|bool Optional language, or boolean true for behavior of 2nd argument.* event param bool Substitute default language page name when page name is not defined for requested language.* event return string Localized language name or blank if not set** @param HookEvent $event**/public function hookPageLocalName(HookEvent $event) {/** @var Page $page */$page = $event->object;$language = $this->getLanguage($event->arguments(0));$nameField = $language->isDefault() ? "name" : "name$language";$value = $page->get($nameField);if(is_null($value)) $value = '';if(empty($value) && $nameField !== 'name' && ($event->arguments(0) === true || $event->arguments(1) === true)) {$value = $page->name;}$event->return = $value;}/*** Add a Page::localPath function with optional $language as argument** event param Language|string|int Optional language* event return string Localized language path** @param HookEvent $event**/public function hookPageLocalPath(HookEvent $event) {/** @var Page $page */$page = $event->object;$language = $this->getLanguage($event->arguments(0));$event->return = $this->getPagePath($page, $language);}/*** Add a Page::localUrl function with optional $language as argument** event param Language|string|int Optional language* event return string Localized language URL** @param HookEvent $event**/public function hookPageLocalUrl(HookEvent $event) {/** @var Page $page */$page = $event->object;$language = $this->getLanguage($event->arguments(0));$event->return = $this->wire('config')->urls->root . ltrim($this->getPagePath($page, $language), '/');}/*** Add a Page::localHttpUrl function with optional $language as argument** event param Language|string|int Optional language* event return string Localized language name or blank if not set** @param HookEvent $event**/public function hookPageLocalHttpUrl(HookEvent $event) {$this->hookPageLocalUrl($event);$url = $event->return;$event->return = $this->wire('input')->scheme() . "://" . $this->wire('config')->httpHost . $url;}/*** Given an object, integer or string, return the Language object instance** @param int|string|Language* @return Language**/protected function getLanguage($language) {if(is_object($language)) {if($language instanceof Language) return $language;$language = '';}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(!$language || !$language->id || !$language instanceof Language) {$language = $this->wire('languages')->get('default');}return $language;}/*** Update pages table for new column when a language is added** @param Language|Page $language**/public function languageAdded(Page $language) {static $languagesAdded = array();if(!$language->id || $language->name == 'default') return;if($language instanceof Language && $language->isDefault()) return;if(isset($languagesAdded[$language->id])) return;$name = "name" . (int) $language->id;$status = "status" . (int) $language->id;$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 column $status" => "ALTER TABLE pages ADD $status INT UNSIGNED NOT NULL DEFAULT " . Page::statusOn,);foreach($sqls as $label => $sql) {try {$database->exec($sql);} catch(\Exception $e) {$this->error("$label: " . $e->getMessage(), Notice::log);$errors++;}}if(!$errors) $languagesAdded[$language->id] = $language->id;}/*** Hook called when language is added** @param HookEvent $event**/public function hookLanguageAdded(HookEvent $event) {$language = $event->arguments[0];$this->languageAdded($language);}/*** Update pages table to remove column when a language is deleted** @param Language|Page $language**/protected function languageDeleted(Page $language) {if(!$language->id || $language->name == 'default') return;$name = "name" . (int) $language->id;$status = "status" . (int) $language->id;$database = $this->wire('database');try {$database->exec("ALTER TABLE pages DROP INDEX {$name}_parent_id");$database->exec("ALTER TABLE pages DROP $name");$database->exec("ALTER TABLE pages DROP $status");} catch(\Exception $e) {// $this->error($e->getMessage(), Notice::log); // error message can be ignored here}}/*** Hook called when language is deleted** @param HookEvent $event**/public function hookLanguageDeleted(HookEvent $event) {$language = $event->arguments[0];$this->languageDeleted($language);}/*** Hook called immediately before a page is saved** Here we make use of the 'extraData' return property of the saveReady hook* to bundle in the language name fields into the query.** @param HookEvent $event**/public function hookPageSaveReady(HookEvent $event) {/** @var Page $page */$page = $event->arguments[0];/** @var Pages $pages */$pages = $event->object;/** @var Sanitizer $sanitizer */$sanitizer = $this->wire('sanitizer');/** @var array $extraData */$extraData = $event->return;$alwaysActiveTypes = array('User', 'Role', 'Permission', 'Language');$pageNameCharset = $this->wire('config')->pageNameCharset;$isCloning = $pages->editor()->isCloning();if(!is_array($extraData)) $extraData = array();foreach($this->wire('languages') as $language) {if($language->isDefault()) continue;$language_id = (int) $language->id;// populate a name123 field for each language$name = "name$language_id";$value = $sanitizer->pageNameUTF8($page->get($name));if(!strlen($value)) {$value = 'NULL';} else if($isCloning) {// this save is the result of a clone() operation// make sure that the name is unique for other languages$value = $pages->names()->uniquePageName(array('name' => $value,'page' => $page,'language' => $language,));}if($pageNameCharset == 'UTF8') {$extraData[$name] = $sanitizer->pageName($value, Sanitizer::toAscii);} else {$extraData[$name] = $value;}// populate a status123 field for each language$name = "status$language_id";if(method_exists($page, 'getForPage')) {// repeater page, pull status from 'for' page$value = (int) $page->getForPage()->get($name);} else if(in_array($page->className(), $alwaysActiveTypes)) {// User, Role, Permission or Language: assume active status$value = Page::statusOn;} else {// regular page$value = (int) $page->get($name);}$extraData[$name] = $value;}$event->return = $extraData;}/*** Hook into Pages::setupNew** Used to assign a $page->name when none has been assigned, like if a user has added* a page in another language but not configured anything for default language** @param HookEvent $event**/public function hookPageSetupNew(HookEvent $event) {/** @var Page $page */$page = $event->arguments[0];// if page already has a name, then no need to continueif($page->name) return;// 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');$userTrackChanges = $user->trackChanges();$userLanguage = $user->language;if($userTrackChanges) $user->setTrackChanges(false);foreach($this->wire('languages') as $language) {if($language->isDefault()) continue;$user->language = $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);} else {$page->name = $this->wire('sanitizer')->pageName($title, Sanitizer::translate);}}}if($page->name) break;}// restore user to previous state$user->language = $userLanguage;if($userTrackChanges) $user->setTrackChanges(true);}/*** Hook called immediately after a page is saved** @param HookEvent $event**/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');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) {if($language->isDefault()) continue;$namePrevious = $page->get("-name$language");if(!$namePrevious) continue;$name = $sanitizer->pageNameUTF8($page->get("name$language"));if($sanitizer->pageNameUTF8($namePrevious) != $name) {$renamed = true;break;}}// trigger renamed hook if one of the language names changedif($renamed) $this->wire('pages')->renamed($page);}}/*** Return the unsanitized/original requested path** @return string**/public function getRequestPath() {return $this->requestPath;}/*** Return the Language that the given path is in or null if can't determine** @param string $path Page path without without installation subdir or URL segments or page numbers* @param Page $page If you already know the $page that resulted from the path, provide it here for faster performance* @return Language|null**/public function getPagePathLanguage($path, Page $page = null) {$languages = $this->wire('languages');if(!$page || !$page->id) $page = $this->wire('pages')->getByPath($path, array('useLanguages' => true,'useHistory' => true));$foundLanguage = null;$path = trim($path, '/');// a blank path can only be homepage in default languageif(!strlen($path)) return $languages->getDefault();// first check entire path for a matchif($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);$firstPart = reset($parts);$lastPart = end($parts);$tests = array($firstPart => $homepage);if($homepage->id != $page->id && $firstPart != $lastPart) $tests[$lastPart] = $page;foreach($tests as $part => $p) {if(!$p->id) continue;$duplicates = 0; // count duplicate names, which would invalidate any $foundLanguageforeach($languages as $language) {$key = 'name' . ($language->isDefault() ? '' : $language->id);$name = $p->get($key);if($name === $part) {$foundLanguage = $language;$duplicates++;}}if($foundLanguage && $duplicates > 1) $foundLanguage = null;if($foundLanguage) break;}if(!$foundLanguage && $page->parent_id > $homepageID && count($parts) > 1) {// if language not yet found, go recursive on the parent path before we throw in the towelarray_pop($parts);$foundLanguage = $this->getPagePathLanguage(implode('/', $parts), $page->parent());}return $foundLanguage;}/*** Check to make sure that the status table exists and creates it if not** @param bool $force**/public function checkModuleVersion($force = false) {$info = self::getModuleInfo();if(!$force) {if($info['version'] == $this->moduleVersion) return;}$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) {if($language->isDefault()) continue;$status = "status" . (int) $language->id;$database->exec("ALTER TABLE pages ADD $status INT UNSIGNED NOT NULL DEFAULT " . Page::statusOn);$this->message("Added status column for language: $language->name", Notice::log);}}// save module version in config dataif($info['version'] != $this->moduleVersion) {$data = $this->wire('modules')->getModuleConfigData($this);$data['moduleVersion'] = $info['version'];$this->wire('modules')->saveModuleConfigData($this, $data);}}/*** Module interactive configuration fields** @param array $data* @return InputfieldWrapper**/public function getModuleConfigInputfields(array $data) {$module = $this->wire('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');$name = "pageNumUrlPrefix$language";if($language->isDefault() && empty($data[$name])) $data[$name] = $defaultUrlPrefix;$f->attr('name', $name);$f->attr('value', isset($data[$name]) ? $data[$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.'),$defaultUrlPrefix);$f->required = false;$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;}/*** Install the module**/public function ___install() {foreach($this->wire('languages') as $language) {$this->languageAdded($language);}}/*** Uninstall the module**/public function ___uninstall() {foreach($this->wire('languages') as $language) {$this->languageDeleted($language);}}}