1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-13 18:24:57 +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 * Keeps track of past URLs where pages have lived and automatically 301 redirects
* to the new location whenever the past URL is accessed. * 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 * https://processwire.com
* *
* @method upgrade($fromVersion, $toVersion) * @method upgrade($fromVersion, $toVersion)
* @property int $minimumAge * @property int $minimumAge
* @property array|bool $rootSegments
* *
* *
*/ */
@@ -20,7 +21,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
public static function getModuleInfo() { public static function getModuleInfo() {
return array( return array(
'title' => 'Page Path History', '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.", '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, 'singular' => true,
'autoload' => true, 'autoload' => true,
@@ -62,6 +63,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
public function __construct() { public function __construct() {
parent::__construct(); parent::__construct();
$this->set('minimumAge', self::minimumAge); $this->set('minimumAge', self::minimumAge);
$this->set('rootSegments', false);
} }
/** /**
@@ -143,7 +145,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
if($result) { if($result) {
// delete any possible entries that overlap with the $page current path since are no longer applicable // 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 = $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(); $query->execute();
} }
@@ -161,13 +163,15 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*/ */
public function addPathHistory(Page $page, $path, $language = null) { 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; $table = self::dbTableName;
$path = $this->wire('sanitizer')->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii); $path = $sanitizer->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
$selector = "path=$path"; $selector = "path=$path";
if($this->wire('modules')->isInstalled('PagePaths')) $selector .= ", id!=$page->id"; if($modules->isInstalled('PagePaths')) $selector .= ", id!=$page->id";
if($this->wire('pages')->count($selector)) return false; if($this->wire()->pages->count($selector)) return false;
$language = $this->getLanguage($language); $language = $this->getLanguage($language);
@@ -186,6 +190,8 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$result = false; $result = false;
} }
$this->addRootSegment($path);
return $result; return $result;
} }
@@ -199,9 +205,9 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*/ */
public function deletePathHistory(Page $page, $path) { public function deletePathHistory(Page $page, $path) {
$database = $this->wire('database'); $database = $this->wire()->database;
$table = self::dbTableName; $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"; $sql = "DELETE FROM $table WHERE path=:path AND pages_id=:pages_id LIMIT 1";
$query = $database->prepare($sql); $query = $database->prepare($sql);
@@ -234,6 +240,7 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
} else { } else {
throw new WireException("Invalid param: instance of Page or boolean true expected"); 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; 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 * Get array of info about a path if it is in history
* *
@@ -846,7 +718,11 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$pathRemoved = ''; $pathRemoved = '';
$cnt = 0; $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, '/'); $path = '/' . trim($path, '/');
while(strlen($path) && !$page->id && $cnt < self::maxSegments) { while(strlen($path) && !$page->id && $cnt < self::maxSegments) {
@@ -915,6 +791,216 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
return $page; 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 * 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) { public function hookPageDeleted(HookEvent $event) {
$page = $event->arguments[0]; $page = $event->arguments[0];
$database = $this->wire('database'); $database = $this->wire()->database;
$query = $database->prepare("DELETE FROM " . self::dbTableName . " WHERE pages_id=:pages_id"); $table = self::dbTableName;
$query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id");
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT); $query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
$query->execute(); $query->execute();
} }
@@ -959,6 +1046,8 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
$event->return = (bool) $this->deletePathHistory($page, $this->urlToPath($url)); $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 * 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) { protected function urlToPath($url) {
$rootUrl = $this->wire('config')->urls->root; $rootUrl = $this->wire()->config->urls->root;
if(strlen($rootUrl) > 1 && strpos($url, $rootUrl) === 0) { if(strlen($rootUrl) > 1 && strpos($url, $rootUrl) === 0) {
$path = substr($url, strlen($rootUrl) - 1); $path = substr($url, strlen($rootUrl) - 1);
} else { } else {
@@ -1007,20 +1096,27 @@ class PagePathHistory extends WireData implements Module, ConfigurableModule {
*/ */
public function ___install() { public function ___install() {
$len = $this->wire('database')->getMaxIndexLength(); $database = $this->wire()->database;
$len = $database->getMaxIndexLength();
$table = self::dbTableName;
$sql = "CREATE TABLE " . self::dbTableName . " (" . if($database->tableExists($table)) {
"path VARCHAR($len) NOT NULL, " . $this->checkTableSchema();
"pages_id INT UNSIGNED NOT NULL, " . return;
"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() { 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($this->checkTableSchema()) {
if($fromVersion != $toVersion) $this->message("PagePathHistory v$fromVersion => v$toVersion"); if($fromVersion != $toVersion) $this->message("PagePathHistory v$fromVersion => v$toVersion");
} }
$this->rebuildRootSegments();
} }
/** /**

View File

@@ -9,6 +9,8 @@
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer * ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com * https://processwire.com
* *
* @property array $rootSegments
*
*/ */
class PagePaths extends WireData implements Module, ConfigurableModule { class PagePaths extends WireData implements Module, ConfigurableModule {
@@ -16,7 +18,7 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
public static function getModuleInfo() { public static function getModuleInfo() {
return array( return array(
'title' => 'Page Paths', '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).", '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, 'singular' => true,
'autoload' => true, 'autoload' => true,
@@ -35,6 +37,15 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
*/ */
protected $languages = null; protected $languages = null;
/**
* Construct
*
*/
public function __construct() {
$this->set('rootSegments', array());
parent::__construct();
}
/** /**
* Initialize the hooks * Initialize the hooks
* *
@@ -71,9 +82,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$this->updatePagePaths($page); $this->updatePagePaths($page);
} }
/** /**
* When a page is deleted * Hook called when a page is deleted
* *
* @param HookEvent $event * @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 = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id");
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT); $query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
$query->execute(); $query->execute();
$this->rebuildRootSegments();
} }
/** /**
@@ -325,7 +336,6 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $row; return $row;
} }
/** /**
* Rebuild all paths table starting with $page and descending to its children * 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"); $this->wire()->database->exec("DELETE FROM $table");
$page = $this->wire()->pages->get('/'); $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; static $level = 0;
$rootPageId = $this->wire()->config->rootPageID;
$database = $this->wire()->database; $database = $this->wire()->database;
$sanitizer = $this->wire()->sanitizer; $sanitizer = $this->wire()->sanitizer;
$languages = $this->getLanguages(); $languages = $this->getLanguages();
$table = self::dbTableName; $table = self::dbTableName;
$numUpdated = 1; $numUpdated = 1;
$homeDefaultName = ''; $homeDefaultName = '';
$rebuildRoot = false;
$level++; $level++;
if($hasChildren === null) { 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'); throw new WireException('Page object required on first call to updatePagePaths');
} }
$pageId = $page->id; $pageId = $page->id;
if($page->parent_id === $rootPageId) $rebuildRoot = true;
if($languages) { if($languages) {
// multi-language // multi-language
foreach($languages as $language) { foreach($languages as $language) {
@@ -445,6 +459,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$pageId = (int) "$page"; $pageId = (int) "$page";
} }
if($pageId === $rootPageId) $rebuildRoot = true;
// sanitize and prepare paths for DB storage // sanitize and prepare paths for DB storage
foreach($paths as $languageId => $path) { foreach($paths as $languageId => $path) {
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii); $path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
@@ -484,6 +500,8 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
$level--; $level--;
if($rebuildRoot && !$level) $this->rebuildRootSegments();
return $numUpdated; return $numUpdated;
} }
@@ -543,12 +561,106 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $numUpdated; 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 **********************************************************************************/ /*** LANGUAGES **********************************************************************************/
/** /**
* Returns Languages object or false if not available * Returns Languages object or false if not available
* *
* @return Languages|null|false * @return Languages|false
* *
*/ */
public function getLanguages() { public function getLanguages() {
@@ -564,7 +676,6 @@ class PagePaths extends WireData implements Module, ConfigurableModule {
return $this->languages; return $this->languages;
} }
/** /**
* @param Language|int|string $language * @param Language|int|string $language
* @return int Returns language ID or 0 for default 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) { public function ___upgrade($fromVersion, $toVersion) {
if($fromVersion && $toVersion) {} // ignore if($fromVersion && $toVersion) {} // ignore
$this->checkTableSchema(); $this->checkTableSchema();
$this->rebuildRootSegments();
} }
/** /**