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

Updates to PagePaths and PagePathHistory modules, adding support for root segments, among other things

This commit is contained in:
Ryan Cramer
2021-10-13 13:32:59 -04:00
parent 511a068b69
commit abcce91e4b
2 changed files with 379 additions and 170 deletions

View File

@@ -6,11 +6,12 @@
* Keeps track of past URLs where pages have lived and automatically 301 redirects
* to the new location whenever the past URL is accessed.
*
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @method upgrade($fromVersion, $toVersion)
* @property int $minimumAge
* @property array|bool $rootSegments
*
*
*/
@@ -20,7 +21,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => 'Page Path History',
'version' => 7,
'version' => 8,
'summary' => "Keeps track of past URLs where pages have lived and automatically redirects (301 permament) to the new location whenever the past URL is accessed.",
'singular' => true,
'autoload' => true,
@@ -62,6 +63,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
public function __construct() {
parent::__construct();
$this->set('minimumAge', self::minimumAge);
$this->set('rootSegments', false);
}
/**
@@ -143,7 +145,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
if($result) {
// delete any possible entries that overlap with the $page current path since are no longer applicable
$query = $database->prepare("DELETE FROM $table WHERE path=:path LIMIT 1");
$query->bindValue(":path", rtrim($this->wire('sanitizer')->pagePathName($page->path, Sanitizer::toAscii), '/'));
$query->bindValue(":path", rtrim($this->wire()->sanitizer->pagePathName($page->path, Sanitizer::toAscii), '/'));
$query->execute();
}
@@ -160,14 +162,16 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*
*/
public function addPathHistory(Page $page, $path, $language = null) {
$database = $this->wire('database');
$sanitizer = $this->wire()->sanitizer;
$database = $this->wire()->database;
$modules = $this->wire()->modules;
$table = self::dbTableName;
$path = $this->wire('sanitizer')->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
$path = $sanitizer->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
$selector = "path=$path";
if($this->wire('modules')->isInstalled('PagePaths')) $selector .= ", id!=$page->id";
if($this->wire('pages')->count($selector)) return false;
if($modules->isInstalled('PagePaths')) $selector .= ", id!=$page->id";
if($this->wire()->pages->count($selector)) return false;
$language = $this->getLanguage($language);
@@ -186,6 +190,8 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$result = false;
}
$this->addRootSegment($path);
return $result;
}
@@ -199,9 +205,9 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*/
public function deletePathHistory(Page $page, $path) {
$database = $this->wire('database');
$database = $this->wire()->database;
$table = self::dbTableName;
$path = $this->wire('sanitizer')->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
$path = $this->wire()->sanitizer->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
$sql = "DELETE FROM $table WHERE path=:path AND pages_id=:pages_id LIMIT 1";
$query = $database->prepare($sql);
@@ -234,6 +240,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
} else {
throw new WireException("Invalid param: instance of Page or boolean true expected");
}
$this->rebuildRootSegments();
}
/**
@@ -515,141 +522,6 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
return $paths;
}
/**
* Hook called when a page is moved or renamed
*
* @param HookEvent $event
*
*/
public function hookPageMoved(HookEvent $event) {
/** @var Page $page */
$page = $event->arguments[0];
/** @var Languages $languages */
$languages = $this->getLanguages();
$age = time() - $page->created;
if($page->template->name === 'admin' || $this->wire()->pages->cloning || $age < $this->minimumAge) return;
// note that the paths we store have no trailing slash
if($languages) {
$parent = $page->parent();
$parentPrevious = $page->parentPrevious;
if($parentPrevious && $parentPrevious->id == $parent->id) $parentPrevious = null;
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$namePrevious = $page->get("-name$language");
if(!$namePrevious && !$parentPrevious) continue;
if(!$namePrevious) $namePrevious = $page->name;
$languages->setLanguage($language);
$pathPrevious = $parentPrevious ? $parentPrevious->path() : $page->parent()->path();
$pathPrevious = rtrim($pathPrevious, '/') . "/$namePrevious";
$this->setPathHistory($page, $pathPrevious, $language->id);
$languages->unsetLanguage();
}
}
if(!$page->namePrevious) {
// abort saving a former URL if it looks like there isn't going to be one
if(!$page->parentPrevious || $page->parentPrevious->id == $page->parent->id) return;
}
if($page->parentPrevious) {
// if former or current parent is in trash, then don't bother saving redirects
if($page->parentPrevious->isTrash() || $page->parent->isTrash()) return;
// the start of our redirect URL will be the previous parent's URL
$path = $page->parentPrevious->path;
} else {
// the start of our redirect URL will be the current parent's URL (i.e. name changed)
$path = $page->parent->path;
}
if($page->namePrevious) {
$path = rtrim($path, '/') . '/' . $page->namePrevious;
} else {
$path = rtrim($path, '/') . '/' . $page->name;
}
// do not save paths that reference recovery format used by trash
// example: /blog/posts/5134.3096.83_page-name
if(strpos($path, '.') !== false && strpos($path, '_') !== false) {
if(preg_match('!/\d+\.\d+\.\d+_!', $path)) return;
}
// do not save paths that match any untitled page name
// example: /blog/posts/untitled-123123
$untitled = $this->wire()->pages->names()->untitledPageName();
if(strpos($path, $untitled) !== false) {
if(preg_match('!/' . preg_quote($untitled) . '[-]!', $path)) return;
}
if($languages) $languages->setDefault();
$this->setPathHistory($page, $path);
if($languages) $languages->unsetDefault();
}
/**
* Hook called upon 404 from ProcessPageView::pageNotFound
*
* @param HookEvent $event
*
*/
public function hookPageNotFound(HookEvent $event) {
/** @var Page $page */
$page = $event->arguments(0);
/** @var Wire404Exception $exception */
$exception = $event->arguments(4);
// If there is a page object set, then it means the 404 was triggered
// by the user not having access to it, or by the $page's template
// throwing a 404 exception. In either case, we don't want to do a
// redirect if there is a $page since any 404 is intentional there.
if($page && $page->id) {
// it did resolve to a Page: maybe a front-end 404
if(!$exception) {
// pageNotFound was called without an Exception
return;
} else if($exception->getCode() == Wire404Exception::codeFunction) {
// the wire404() function was called: allow PagePathHistory
} else if($exception->getMessage() === "1") {
// also allow PagePathHistory to operate when: throw new WireException(true);
} else {
// likely user didn't have access or intentional 404 that should not redirect
return;
}
}
$languages = $this->getLanguages();
$languagePageNames = $languages ? $languages->pageNames() : null;
if($languagePageNames) {
// the LanguageSupportPageNames may change the original requested path, so we ask it for the original
$path = $languagePageNames->getRequestPath();
$path = $path ? $this->wire()->sanitizer->pagePathName($path) : $event->arguments(1);
} else {
$path = $event->arguments(1);
}
$page = $this->getPage($path);
if($page->id && $page->viewable()) {
// if a page was found, redirect to it...
$language = $page->get('_language');
if($language && $languages) {
// ...optionally for a specific language
if($page->get("status$language")) {
$languages->setLanguage($language);
}
}
$this->session->redirect($page->url);
}
}
/**
* Get array of info about a path if it is in history
*
@@ -846,7 +718,11 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$pathRemoved = '';
$cnt = 0;
if(!$level) $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
if(!$level) {
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
if(!$this->isRootSegment($path)) return $pages->newNullPage();
}
$path = '/' . trim($path, '/');
while(strlen($path) && !$page->id && $cnt < self::maxSegments) {
@@ -915,6 +791,216 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
return $page;
}
/*** ROOT SEGMENTS ***********************************************************/
/**
* Get all root segments
*
* @return array
* @since 3.0.186
*
*/
public function getRootSegments() {
if(is_array($this->rootSegments)) return $this->rootSegments;
return $this->rebuildRootSegments();
}
/**
* Is/was given segment ever a root segment?
*
* @param string $segment Segment or path containing it (in ascii format)
* @return bool
* @since 3.0.186
*
*/
public function isRootSegment($segment) {
$segment = trim($segment, '/');
if(strpos($segment, '/')) list($segment,) = explode('/', $segment, 2);
$segments = $this->getRootSegments();
return in_array($segment, $segments, true);
}
/**
* Add a root segment
*
* @param string $segment May be a segment or path to extract it from (in ascii format)
* @return bool True if added, false if it was already present
* @since 3.0.186
*
*/
protected function addRootSegment($segment) {
$segment = trim($segment, '/');
if(strpos($segment, '/')) list($segment,) = explode('/', $segment, 2);
$rootSegments = $this->rootSegments;
if(!is_array($rootSegments)) $rootSegments = array();
if(in_array($segment, $rootSegments, true)) return false;
$rootSegments[] = $segment;
$this->rootSegments = $rootSegments;
$this->wire()->modules->saveConfig($this, 'rootSegments', $rootSegments);
return true;
}
/**
* Rebuild all root segments
*
* @return array
* @since 3.0.186
*
*/
protected function rebuildRootSegments() {
$segments = array();
$sql = 'SELECT path FROM ' . self::dbTableName;
$query = $this->wire()->database->prepare($sql);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_NUM)) {
$path = trim($row[0], '/');
list($segment,) = explode('/', $path, 2);
$segments[$segment] = $segment;
}
$query->closeCursor();
$segments = array_values($segments);
$this->rootSegments = $segments;
$this->wire()->modules->saveConfig($this, 'rootSegments', $segments);
return $segments;
}
/*** HOOKS *******************************************************************/
/**
* Hook called when a page is moved or renamed
*
* @param HookEvent $event
*
*/
public function hookPageMoved(HookEvent $event) {
/** @var Page $page */
$page = $event->arguments[0];
/** @var Languages $languages */
$languages = $this->getLanguages();
$age = time() - $page->created;
if($page->template->name === 'admin' || $this->wire()->pages->cloning || $age < $this->minimumAge) return;
// note that the paths we store have no trailing slash
if($languages) {
$parent = $page->parent();
$parentPrevious = $page->parentPrevious;
if($parentPrevious && $parentPrevious->id == $parent->id) $parentPrevious = null;
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$namePrevious = $page->get("-name$language");
if(!$namePrevious && !$parentPrevious) continue;
if(!$namePrevious) $namePrevious = $page->name;
$languages->setLanguage($language);
$pathPrevious = $parentPrevious ? $parentPrevious->path() : $page->parent()->path();
$pathPrevious = rtrim($pathPrevious, '/') . "/$namePrevious";
$this->setPathHistory($page, $pathPrevious, $language->id);
$languages->unsetLanguage();
}
}
if(!$page->namePrevious) {
// abort saving a former URL if it looks like there isn't going to be one
if(!$page->parentPrevious || $page->parentPrevious->id == $page->parent->id) return;
}
if($page->parentPrevious) {
// if former or current parent is in trash, then don't bother saving redirects
if($page->parentPrevious->isTrash() || $page->parent->isTrash()) return;
// the start of our redirect URL will be the previous parent's URL
$path = $page->parentPrevious->path;
} else {
// the start of our redirect URL will be the current parent's URL (i.e. name changed)
$path = $page->parent->path;
}
if($page->namePrevious) {
$path = rtrim($path, '/') . '/' . $page->namePrevious;
} else {
$path = rtrim($path, '/') . '/' . $page->name;
}
// do not save paths that reference recovery format used by trash
// example: /blog/posts/5134.3096.83_page-name
if(strpos($path, '.') !== false && strpos($path, '_') !== false) {
if(preg_match('!/\d+\.\d+\.\d+_!', $path)) return;
}
// do not save paths that match any untitled page name
// example: /blog/posts/untitled-123123
$untitled = $this->wire()->pages->names()->untitledPageName();
if(strpos($path, $untitled) !== false) {
if(preg_match('!/' . preg_quote($untitled) . '[-]!', $path)) return;
}
if($languages) $languages->setDefault();
$this->setPathHistory($page, $path);
if($languages) $languages->unsetDefault();
}
/**
* Hook called upon 404 from ProcessPageView::pageNotFound
*
* @param HookEvent $event
*
*/
public function hookPageNotFound(HookEvent $event) {
/** @var Page $page */
$page = $event->arguments(0);
/** @var Wire404Exception $exception */
$exception = $event->arguments(4);
// If there is a page object set, then it means the 404 was triggered
// by the user not having access to it, or by the $page's template
// throwing a 404 exception. In either case, we don't want to do a
// redirect if there is a $page since any 404 is intentional there.
if($page && $page->id) {
// it did resolve to a Page: maybe a front-end 404
if(!$exception) {
// pageNotFound was called without an Exception
return;
} else if($exception->getCode() == Wire404Exception::codeFunction) {
// the wire404() function was called: allow PagePathHistory
} else if($exception->getMessage() === "1") {
// also allow PagePathHistory to operate when: throw new WireException(true);
} else {
// likely user didn't have access or intentional 404 that should not redirect
return;
}
}
$languages = $this->getLanguages();
$languagePageNames = $languages ? $languages->pageNames() : null;
if($languagePageNames) {
// the LanguageSupportPageNames may change the original requested path, so we ask it for the original
$path = $languagePageNames->getRequestPath();
$path = $path ? $this->wire()->sanitizer->pagePathName($path) : $event->arguments(1);
} else {
$path = $event->arguments(1);
}
$page = $this->getPage($path);
if($page->id && $page->viewable()) {
// if a page was found, redirect to it...
$language = $page->get('_language');
if($language && $languages) {
// ...optionally for a specific language
if($page->get("status$language")) {
$languages->setLanguage($language);
}
}
$this->session->redirect($page->url);
}
}
/**
* When a page is deleted, remove it from our redirects list as well
*
@@ -923,8 +1009,9 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*/
public function hookPageDeleted(HookEvent $event) {
$page = $event->arguments[0];
$database = $this->wire('database');
$query = $database->prepare("DELETE FROM " . self::dbTableName . " WHERE pages_id=:pages_id");
$database = $this->wire()->database;
$table = self::dbTableName;
$query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id");
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
$query->execute();
}
@@ -958,6 +1045,8 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$url = $event->arguments(0);
$event->return = (bool) $this->deletePathHistory($page, $this->urlToPath($url));
}
/*** MODULE ******************************************************************/
/**
* Given URL that may include a root subdirectory, convert it to path relative to root subdirectory
@@ -967,7 +1056,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*
*/
protected function urlToPath($url) {
$rootUrl = $this->wire('config')->urls->root;
$rootUrl = $this->wire()->config->urls->root;
if(strlen($rootUrl) > 1 && strpos($url, $rootUrl) === 0) {
$path = substr($url, strlen($rootUrl) - 1);
} else {
@@ -1006,21 +1095,28 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*
*/
public function ___install() {
$len = $this->wire('database')->getMaxIndexLength();
$database = $this->wire()->database;
$len = $database->getMaxIndexLength();
$table = self::dbTableName;
if($database->tableExists($table)) {
$this->checkTableSchema();
return;
}
$sql = "CREATE TABLE " . self::dbTableName . " (" .
"path VARCHAR($len) NOT NULL, " .
"pages_id INT UNSIGNED NOT NULL, " .
"language_id INT UNSIGNED DEFAULT 0, " . // v2
"created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " .
"PRIMARY KEY path (path), " .
"INDEX pages_id (pages_id), " .
"INDEX created (created) " .
") ENGINE={$this->config->dbEngine} DEFAULT CHARSET={$this->config->dbCharset}";
$this->wire('database')->exec($sql);
$sql =
"CREATE TABLE $table (" .
"path VARCHAR($len) NOT NULL, " .
"pages_id INT UNSIGNED NOT NULL, " .
"language_id INT UNSIGNED DEFAULT 0, " . // v2
"created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " .
"PRIMARY KEY path (path), " .
"INDEX pages_id (pages_id), " .
"INDEX created (created) " .
") ENGINE={$this->config->dbEngine} DEFAULT CHARSET={$this->config->dbCharset}";
$database->exec($sql);
}
/**
@@ -1028,7 +1124,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*
*/
public function ___uninstall() {
$this->wire('database')->query("DROP TABLE " . self::dbTableName);
$this->wire()->database->query("DROP TABLE " . self::dbTableName);
}
/**
@@ -1042,6 +1138,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
if($this->checkTableSchema()) {
if($fromVersion != $toVersion) $this->message("PagePathHistory v$fromVersion => v$toVersion");
}
$this->rebuildRootSegments();
}
/**

View File

@@ -8,6 +8,8 @@
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @property array $rootSegments
*
*/
@@ -16,7 +18,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => 'Page Paths',
'version' => 3,
'version' => 4,
'summary' => "Enables page paths/urls to be queryable by selectors. Also offers potential for improved load performance. Builds an index at install (may take time on a large site).",
'singular' => true,
'autoload' => true,
@@ -35,6 +37,15 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
*/
protected $languages = null;
/**
* Construct
*
*/
public function __construct() {
$this->set('rootSegments', array());
parent::__construct();
}
/**
* Initialize the hooks
*
@@ -71,9 +82,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$this->updatePagePaths($page);
}
/**
* When a page is deleted
* Hook called when a page is deleted
*
* @param HookEvent $event
*
@@ -85,6 +95,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id");
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
$query->execute();
$this->rebuildRootSegments();
}
/**
@@ -325,7 +336,6 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $row;
}
/**
* Rebuild all paths table starting with $page and descending to its children
*
@@ -342,7 +352,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$this->wire()->database->exec("DELETE FROM $table");
$page = $this->wire()->pages->get('/');
}
return $this->updatePagePaths($page, true);
$result = $this->updatePagePaths($page, true);
return $result;
}
/**
@@ -411,12 +422,14 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
static $level = 0;
$rootPageId = $this->wire()->config->rootPageID;
$database = $this->wire()->database;
$sanitizer = $this->wire()->sanitizer;
$languages = $this->getLanguages();
$table = self::dbTableName;
$numUpdated = 1;
$homeDefaultName = '';
$rebuildRoot = false;
$level++;
if($hasChildren === null) {
@@ -429,6 +442,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
throw new WireException('Page object required on first call to updatePagePaths');
}
$pageId = $page->id;
if($page->parent_id === $rootPageId) $rebuildRoot = true;
if($languages) {
// multi-language
foreach($languages as $language) {
@@ -444,6 +458,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
// $paths already populated
$pageId = (int) "$page";
}
if($pageId === $rootPageId) $rebuildRoot = true;
// sanitize and prepare paths for DB storage
foreach($paths as $languageId => $path) {
@@ -483,6 +499,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
}
$level--;
if($rebuildRoot && !$level) $this->rebuildRootSegments();
return $numUpdated;
}
@@ -542,13 +560,107 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $numUpdated;
}
/*** ROOT SEGMENTS ******************************************************************************/
/**
* Is given segment/page name a root segment?
*
* A root segment is one that is owned by the homepage or a direct parent of the homepage, i.e.
* /about/ might be a root page segment and /de/ might be a root language segment. If it is a
* root page segment like /about/ then this will return the ID of that page. If it is a root
* language segment like /de/ then it will return the homepage ID (1).
*
* @param string $segment Page name string or path containing it
* @return int Returns page ID or 0 for no match.
* @since 3.0.186
*
*/
public function isRootSegment($segment) {
$segment = trim($segment, '/');
if(strpos($segment, '/')) list($segment,) = explode('/', $segment, 2);
$rootSegments = $this->getRootSegments();
$key = array_search($segment, $rootSegments);
if($key === false) return 0;
$key = ltrim($key, '_');
if(strpos($key, '.')) {
list($pageId, /*$languageId*/) = explode('.', $key, 2);
} else {
$pageId = $key;
}
return (int) $pageId;
}
/**
* Get root segments
*
* @param bool $rebuild
* @return array
* @since 3.0.186
*
*/
public function getRootSegments($rebuild = false) {
if(empty($this->rootSegments) || $rebuild) $this->rebuildRootSegments();
return $this->rootSegments;
}
/**
* Rebuild root segments stored in module config
*
* @since 3.0.186
*
*/
protected function rebuildRootSegments() {
$database = $this->wire()->database;
$config = $this->wire()->config;
$languages = $this->getLanguages();
$cols = array('id', 'name');
$segments = array();
if($languages) {
foreach($languages as $language) {
if(!$language->isDefault()) $cols[] = "name$language->id";
}
}
$sql = 'SELECT ' . implode(',', $cols) . ' FROM pages WHERE parent_id=:id ';
if($languages) $sql .= 'OR id=:id';
$query = $database->prepare($sql);
$query->bindValue(':id', $config->rootPageID, \PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(\PDO::FETCH_ASSOC)){
$id = (int) $row['id'];
unset($row['id']);
foreach($row as $col => $name) {
if(!strlen($name)) continue;
if($id === 1 && $col === 'name' && $name === Pages::defaultRootName) continue; // skip "/home/"
$col = str_replace('name', '', $col);
if(strlen($col)) {
$segments["_$id.$col"] = $name; // _pageID.languageID i.e. 123.456
} else {
$segments["_$id"] = $name; // _pageID i.e. 123
}
}
}
$query->closeCursor();
$this->rootSegments = $segments;
$this->wire()->modules->saveConfig($this, 'rootSegments', $segments);
return $segments;
}
/*** LANGUAGES **********************************************************************************/
/**
* Returns Languages object or false if not available
*
* @return Languages|null|false
* @return Languages|false
*
*/
public function getLanguages() {
@@ -564,7 +676,6 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $this->languages;
}
/**
* @param Language|int|string $language
* @return int Returns language ID or 0 for default language
@@ -650,6 +761,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
public function ___upgrade($fromVersion, $toVersion) {
if($fromVersion && $toVersion) {} // ignore
$this->checkTableSchema();
$this->rebuildRootSegments();
}
/**