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:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user