851 lines
23 KiB
Text
851 lines
23 KiB
Text
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire Page Paths
|
||
*
|
||
* Keeps a cache of page paths to improve performance and
|
||
* make paths more queryable by selectors.
|
||
*
|
||
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
* @property array $rootSegments
|
||
*
|
||
*/
|
||
|
||
class PagePaths extends WireData implements Module, ConfigurableModule {
|
||
|
||
public static function getModuleInfo() {
|
||
return array(
|
||
'title' => 'Page Paths',
|
||
'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,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Table created by this module
|
||
*
|
||
*/
|
||
const dbTableName = 'pages_paths';
|
||
|
||
/**
|
||
* @var Languages|false
|
||
*
|
||
*/
|
||
protected $languages = null;
|
||
|
||
/**
|
||
* Construct
|
||
*
|
||
*/
|
||
public function __construct() {
|
||
$this->set('rootSegments', array());
|
||
parent::__construct();
|
||
}
|
||
|
||
/**
|
||
* Initialize the hooks
|
||
*
|
||
*/
|
||
public function init() {
|
||
$pages = $this->wire()->pages;
|
||
$pages->addHook('moved', $this, 'hookPageMoved');
|
||
$pages->addHook('renamed', $this, 'hookPageMoved');
|
||
$pages->addHook('added', $this, 'hookPageMoved');
|
||
$pages->addHook('deleted', $this, 'hookPageDeleted');
|
||
}
|
||
|
||
/**
|
||
* API ready
|
||
*
|
||
*/
|
||
public function ready() {
|
||
if($this->wire()->languages) {
|
||
$this->addHookBefore('LanguageSupportFields::languageDeleted', $this, 'hookLanguageDeleted');
|
||
}
|
||
}
|
||
|
||
/*** HOOKS ******************************************************************************************/
|
||
|
||
/**
|
||
* Hook called when a page is moved or renamed
|
||
*
|
||
* @param HookEvent $event
|
||
*
|
||
*/
|
||
public function hookPageMoved(HookEvent $event) {
|
||
$page = $event->arguments[0];
|
||
// $this->updatePagePath($page->id, $page->path);
|
||
$this->updatePagePaths($page);
|
||
}
|
||
|
||
/**
|
||
* Hook called when a page is deleted
|
||
*
|
||
* @param HookEvent $event
|
||
*
|
||
*/
|
||
public function hookPageDeleted(HookEvent $event) {
|
||
$table = self::dbTableName;
|
||
$page = $event->arguments[0];
|
||
$database = $this->wire()->database;
|
||
$query = $database->prepare("DELETE FROM $table WHERE pages_id=:pages_id");
|
||
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
|
||
$query->execute();
|
||
$this->rebuildRootSegments();
|
||
}
|
||
|
||
/**
|
||
* When a language is deleted
|
||
*
|
||
* @param HookEvent $event
|
||
*
|
||
*/
|
||
public function hookLanguageDeleted(HookEvent $event) {
|
||
$languages = $this->getLanguages();
|
||
if(!$languages) return;
|
||
$language = $event->arguments[0]; /** @var Language $language */
|
||
if(!$language->id || $language->isDefault()) return;
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
$sql = "DELETE FROM $table WHERE language_id=:language_id";
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':language_id', $language->id, \PDO::PARAM_INT);
|
||
$this->executeQuery($query);
|
||
}
|
||
|
||
/*** PUBLIC API *************************************************************************************/
|
||
|
||
/**
|
||
* Given a page ID, return the page path, NULL if not found, or boolean false if cannot be determined.
|
||
*
|
||
* @param int $pageId Page ID
|
||
* @param int $languageId Optionally specify language ID for path or 0 for default language
|
||
* @return string|null Returns path or null if not found
|
||
*
|
||
*/
|
||
public function getPath($pageId, $languageId = 0) {
|
||
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
|
||
$languageId = $this->languageId($languageId);
|
||
|
||
$sql = "SELECT path FROM `$table` WHERE pages_id=:pages_id AND language_id=:language_id";
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
|
||
$query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
|
||
$path = null;
|
||
|
||
if(!$this->executeQuery($query)) return $path;
|
||
|
||
if($query->rowCount()) {
|
||
$path = $query->fetchColumn();
|
||
$path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
|
||
}
|
||
|
||
$query->closeCursor();
|
||
|
||
return $path;
|
||
}
|
||
|
||
/**
|
||
* Given a page ID, return all paths found for page
|
||
*
|
||
* Return value is indexed by language ID (and index 0 for default language)
|
||
*
|
||
* @param int $pageId Page ID
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getPaths($pageId) {
|
||
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$paths = array();
|
||
|
||
$sql = "SELECT path, language_id FROM `$table` WHERE pages_id=:pages_id ";
|
||
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
|
||
|
||
if(!$this->executeQuery($query)) return $paths;
|
||
|
||
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||
$path = $row[0];
|
||
$languageId = (int) $row[1];
|
||
$path = strlen($path) ? $sanitizer->pagePathName("/$path/", Sanitizer::toUTF8) : '/';
|
||
$paths[$languageId] = $path;
|
||
}
|
||
|
||
$query->closeCursor();
|
||
|
||
return $paths;
|
||
}
|
||
|
||
/**
|
||
* Given a page path, return the page ID or NULL if not found.
|
||
*
|
||
* @param string $path
|
||
* @return int|null
|
||
*
|
||
*/
|
||
public function getID($path) {
|
||
$id = $this->getPageId($path);
|
||
return $id ? $id : null;
|
||
}
|
||
|
||
/**
|
||
* Given a page path, return the page ID or 0 if not found.
|
||
*
|
||
* @param string|array $path
|
||
* @return int|null
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function getPageID($path) {
|
||
$a = $this->getPageAndLanguageId($path);
|
||
return $a[0];
|
||
}
|
||
|
||
/**
|
||
* Given a page path return array of [ page_id, language_id ]
|
||
*
|
||
* If not found, returned page_id and language_id will be 0.
|
||
*
|
||
* @param string|array $path
|
||
* @return array
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function getPageAndLanguageID($path) {
|
||
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
$paths = is_array($path) ? array_values($path) : array($path);
|
||
$bindValues = array();
|
||
$wheres = array();
|
||
|
||
foreach($paths as $n => $path) {
|
||
$path = $this->wire()->sanitizer->pagePathName($path, Sanitizer::toAscii);
|
||
$path = trim($path, '/');
|
||
$wheres[] = "path=:path$n";
|
||
$bindValues["path$n"] = $path;
|
||
}
|
||
|
||
$where = implode(' OR ', $wheres);
|
||
$sql = "SELECT pages_id, language_id FROM $table WHERE $where LIMIT 1";
|
||
$query = $database->prepare($sql);
|
||
$row = array(0, 0);
|
||
|
||
foreach($bindValues as $bindKey => $bindValue) {
|
||
$query->bindValue(":$bindKey", $bindValue);
|
||
}
|
||
|
||
if(!$this->executeQuery($query)) return $row;
|
||
|
||
if($query->rowCount()) {
|
||
$row = $query->fetch(\PDO::FETCH_NUM);
|
||
}
|
||
|
||
$query->closeCursor();
|
||
|
||
return array((int) $row[0], (int) $row[1]);
|
||
}
|
||
|
||
/**
|
||
* Get page information about a given path
|
||
*
|
||
* Returned array includes the following:
|
||
*
|
||
* - `id` (int): ID of page for given path
|
||
* - `language_id` (int): ID of language path was for, or 0 for default language
|
||
* - `templates_id` (int): ID of template used by page
|
||
* - `parent_id` (int): ID of parent page
|
||
* - `status` (int): Status value for page ($page->status)
|
||
* - `path` (string): Path that was found
|
||
*
|
||
* @param string $path
|
||
* @return array|bool Returns info array on success, boolean false if not found
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function getPageInfo($path) {
|
||
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$database = $this->wire()->database;
|
||
$languages = $this->wire()->languages;
|
||
$config = $this->wire()->config;
|
||
|
||
$table = self::dbTableName;
|
||
$useUTF8 = $config->pageNameCharset === 'UTF8';
|
||
|
||
if($languages && !$languages->hasPageNames()) $languages = null;
|
||
|
||
if($useUTF8) {
|
||
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
|
||
}
|
||
|
||
$columns = array(
|
||
'pages_paths.path AS path',
|
||
'pages_paths.pages_id AS id',
|
||
'pages_paths.language_id AS language_id',
|
||
'pages.templates_id AS templates_id',
|
||
'pages.parent_id AS parent_id',
|
||
'pages.status AS status'
|
||
);
|
||
|
||
if($languages) {
|
||
foreach($languages as $language) {
|
||
if($language->isDefault()) continue;
|
||
$columns[] = "pages.status$language->id AS status$language->id";
|
||
}
|
||
}
|
||
|
||
$cols = implode(', ', $columns);
|
||
|
||
$sql =
|
||
"SELECT $cols FROM $table " .
|
||
"JOIN pages ON pages_paths.pages_id=pages.id " .
|
||
"WHERE pages_paths.path=:path";
|
||
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(':path', trim($path, '/'));
|
||
|
||
if(!$this->executeQuery($query)) return false;
|
||
|
||
$row = $query->fetch(\PDO::FETCH_ASSOC);
|
||
$query->closeCursor();
|
||
|
||
if(!$row) return false;
|
||
|
||
foreach($row as $key => $value) {
|
||
if($key === 'id' || strpos($key, 'status') === 0 || strpos($key, '_id')) {
|
||
$row[$key] = (int) $value;
|
||
}
|
||
}
|
||
|
||
if($useUTF8 && $row) {
|
||
$row['path'] = $sanitizer->pagePathName($row['path'], Sanitizer::toUTF8);
|
||
}
|
||
|
||
return $row;
|
||
}
|
||
/**
|
||
* Rebuild all paths table starting with $page and descending to its children
|
||
*
|
||
* @param Page|null $page Page to start rebuild from or omit to rebuild all
|
||
* @return int Number of paths added
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function rebuild(Page $page = null) {
|
||
set_time_limit(3600);
|
||
$table = self::dbTableName;
|
||
if($page === null) {
|
||
// rebuild all
|
||
$this->wire()->database->exec("DELETE FROM $table");
|
||
$page = $this->wire()->pages->get('/');
|
||
}
|
||
$result = $this->updatePagePaths($page, true);
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Perform a path match for use by PageFinder
|
||
*
|
||
* @param DatabaseQuerySelect $query
|
||
* @param Selector $selector
|
||
* @throws PageFinderSyntaxException
|
||
*
|
||
*/
|
||
public function getMatchQuery(DatabaseQuerySelect $query, Selector $selector) {
|
||
|
||
static $n = 0;
|
||
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$database = $this->wire()->database;
|
||
|
||
$n++;
|
||
$table = self::dbTableName;
|
||
$alias = "$table$n";
|
||
$value = $selector->value;
|
||
$operator = $selector->operator;
|
||
// $joinType = $selector->not ? 'leftjoin' : 'join';
|
||
|
||
$query->join("$table AS $alias ON pages.id=$alias.pages_id");
|
||
|
||
if(in_array($operator, array('=', '!=', '<>', '>', '<', '>=', '<='))) {
|
||
if(!is_array($value)) $value = array($value);
|
||
$where = '';
|
||
foreach($value as $path) {
|
||
if($where) $where .= $selector->not ? " AND " : " OR ";
|
||
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
|
||
$path = $database->escapeStr(trim($path, '/'));
|
||
$where .= ($selector->not ? "NOT " : "") . "$alias.path{$operator}'$path'";
|
||
}
|
||
$query->where("($where)");
|
||
|
||
} else {
|
||
if(is_array($value)) {
|
||
$error = "Multi value using '|' is not supported with path/url and '$operator' operator";
|
||
throw new PageFinderSyntaxException($error);
|
||
}
|
||
if($selector->not) {
|
||
$error = "NOT mode isn't yet supported with path/url and '$operator' operator";
|
||
throw new PageFinderSyntaxException($error);
|
||
}
|
||
/** @var DatabaseQuerySelectFulltext $ft */
|
||
$ft = $this->wire(new DatabaseQuerySelectFulltext($query));
|
||
$ft->match($alias, 'path', $operator, trim($value, '/'));
|
||
}
|
||
}
|
||
|
||
/*** PROTECTED API **********************************************************************************/
|
||
|
||
/**
|
||
* Updates path for page and all children
|
||
*
|
||
* @param Page|int $page
|
||
* @param bool|null $hasChildren Does this page have children? Specify false if known not to have children, true otherwise.
|
||
* @param array $paths Paths indexed by language ID, use index 0 for default language.
|
||
* @return int Number of paths updated
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
protected function updatePagePaths($page, $hasChildren = null, array $paths = array()) {
|
||
|
||
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) {
|
||
$hasChildren = $page instanceof Page ? $page->numChildren > 0 : true;
|
||
}
|
||
|
||
if(empty($paths)) {
|
||
// determine the paths
|
||
if(!is_object($page) || !$page instanceof Page) {
|
||
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) {
|
||
$languageId = $language->isDefault() ? 0 : $language->id;
|
||
$paths[$languageId] = $page->localPath($language);
|
||
if($pageId === 1 && !$languageId) $homeDefaultName = $page->name;
|
||
}
|
||
} else {
|
||
// single language
|
||
$paths[0] = $page->path();
|
||
}
|
||
} else {
|
||
// $paths already populated
|
||
$pageId = (int) "$page";
|
||
}
|
||
|
||
if($pageId === $rootPageId) $rebuildRoot = true;
|
||
|
||
// sanitize and prepare paths for DB storage
|
||
foreach($paths as $languageId => $path) {
|
||
$path = $sanitizer->pagePathName($path, Sanitizer::toAscii);
|
||
$paths[$languageId] = trim($path, '/');
|
||
}
|
||
|
||
$sql =
|
||
"INSERT INTO $table (pages_id, language_id, path) " .
|
||
"VALUES(:pages_id, :language_id, :path) " .
|
||
"ON DUPLICATE KEY UPDATE " .
|
||
"pages_id=VALUES(pages_id), language_id=VALUES(language_id), path=VALUES(path)";
|
||
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(":pages_id", $pageId, \PDO::PARAM_INT);
|
||
|
||
foreach($paths as $languageId => $path) {
|
||
$query->bindValue(":language_id", $languageId, \PDO::PARAM_INT);
|
||
$query->bindValue(":path", $path);
|
||
if($this->executeQuery($query)) $numUpdated += $query->rowCount();
|
||
}
|
||
|
||
if($hasChildren) {
|
||
if($homeDefaultName && $homeDefaultName !== 'home' && empty($paths[0])) {
|
||
// for when homepage has a name (lang segment) but it isn’t used on actual homepage
|
||
// but is used on children
|
||
$paths[0] = $homeDefaultName;
|
||
}
|
||
$numUpdated += $this->updatePagePathsChildren($pageId, $paths);
|
||
}
|
||
|
||
if($level === 1 && $numUpdated > 0) {
|
||
$this->message(
|
||
sprintf($this->_n('Updated %d path', 'Updated %d paths', $numUpdated), $numUpdated),
|
||
Notice::admin
|
||
);
|
||
}
|
||
|
||
$level--;
|
||
|
||
if($rebuildRoot && !$level) $this->rebuildRootSegments();
|
||
|
||
return $numUpdated;
|
||
}
|
||
|
||
/**
|
||
* Companion to updatePagePaths method to handle children
|
||
*
|
||
* @param int $pageId
|
||
* @param array $paths Paths indexed by language ID, index 0 for default language
|
||
* @return int
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
protected function updatePagePathsChildren($pageId, array $paths) {
|
||
|
||
$database = $this->wire()->database;
|
||
$languages = $this->getLanguages();
|
||
$nameColumns = array('pages.name AS name');
|
||
$numUpdated = 0;
|
||
|
||
if($languages) {
|
||
foreach($languages as $language) {
|
||
if($language->isDefault()) continue;
|
||
$nameColumns[] = "pages.name$language->id AS name$language->id";
|
||
}
|
||
}
|
||
|
||
$sql =
|
||
"SELECT pages.id AS id, " . implode(', ', $nameColumns) . ", " .
|
||
"COUNT(children.id) AS kids " .
|
||
"FROM pages " .
|
||
"LEFT JOIN pages AS children ON children.id=pages.parent_id " .
|
||
"WHERE pages.parent_id=:id " .
|
||
"GROUP BY pages.id ";
|
||
|
||
$query = $database->prepare($sql);
|
||
$query->bindValue(":id", $pageId, \PDO::PARAM_INT);
|
||
$rows = array();
|
||
|
||
if(!$this->executeQuery($query)) return $numUpdated;
|
||
|
||
while($row = $query->fetch(\PDO::FETCH_ASSOC)) {
|
||
$rows[] = $row;
|
||
}
|
||
|
||
$query->closeCursor();
|
||
|
||
foreach($rows as $row) {
|
||
$childPaths = array();
|
||
foreach($paths as $languageId => $path) {
|
||
$key = $languageId ? "name$languageId" : "name";
|
||
$name = !empty($row[$key]) ? $row[$key] : $row["name"];
|
||
$childPaths[$languageId] = "$path/$name";
|
||
}
|
||
$numUpdated += $this->updatePagePaths((int) $row['id'], $row['kids'] > 0, $childPaths);
|
||
}
|
||
|
||
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|false
|
||
*
|
||
*/
|
||
public function getLanguages() {
|
||
if($this->languages !== null) return $this->languages;
|
||
$languages = $this->wire()->languages;
|
||
if(!$languages) {
|
||
$this->languages = false;
|
||
} else if($languages->hasPageNames()) {
|
||
$this->languages = $languages;
|
||
} else {
|
||
$this->languages = false;
|
||
}
|
||
return $this->languages;
|
||
}
|
||
|
||
/**
|
||
* @param Language|int|string $language
|
||
* @return int Returns language ID or 0 for default language
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
protected function languageId($language) {
|
||
$language = $this->language($language);
|
||
if(!$language->id || $language->isDefault()) return 0;
|
||
return $language->id;
|
||
}
|
||
|
||
/**
|
||
* @param Language|int|string $language
|
||
* @return Language|NullPage
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
protected function language($language) {
|
||
$languages = $this->getLanguages();
|
||
if(!$languages) return new NullPage();
|
||
if(is_object($language)) return ($language instanceof Language ? $language : new NullPage());
|
||
return $languages->get($language);
|
||
}
|
||
|
||
/*** MODULE MAINT *******************************************************************************/
|
||
|
||
/**
|
||
* Execute a query/PDOStatement
|
||
*
|
||
* @param \PDOStatement $query
|
||
* @param bool $throw Allow exceptions to be thrown? (default=true)
|
||
* @return bool
|
||
* @throws \PDOException|WireException
|
||
*
|
||
*/
|
||
protected function executeQuery($query, $throw = true) {
|
||
try {
|
||
$result = $query->execute();
|
||
} catch(\Exception $e) {
|
||
if(!$this->checkTableSchema()) {
|
||
if($throw) throw $e;
|
||
$this->error($e->getMessage(), Notice::superuser | Notice::log);
|
||
}
|
||
$result = false;
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Check db schema
|
||
*
|
||
* @return bool True if changes made, false if not
|
||
*
|
||
*/
|
||
protected function checkTableSchema() {
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
if(!$database->columnExists($table, 'language_id')) {
|
||
$sqls = array(
|
||
"ALTER TABLE $table ADD language_id INT UNSIGNED NOT NULL DEFAULT 0 AFTER pages_id",
|
||
"ALTER TABLE $table DROP PRIMARY KEY, ADD PRIMARY KEY(pages_id, language_id)",
|
||
"ALTER TABLE $table ADD INDEX language_id (language_id)",
|
||
"ALTER TABLE $table DROP INDEX path, ADD UNIQUE KEY path(path(500), language_id)",
|
||
);
|
||
foreach($sqls as $sql) {
|
||
$database->exec($sql);
|
||
}
|
||
$this->message("Added language_id column to table $table", Notice::admin);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Upgrade module
|
||
*
|
||
* @param $fromVersion
|
||
* @param $toVersion
|
||
* @since 3.0.186
|
||
*
|
||
*/
|
||
public function ___upgrade($fromVersion, $toVersion) {
|
||
if($fromVersion && $toVersion) {} // ignore
|
||
$this->checkTableSchema();
|
||
$this->rebuildRootSegments();
|
||
}
|
||
|
||
/**
|
||
* Install the module
|
||
*
|
||
*/
|
||
public function ___install() {
|
||
|
||
$table = self::dbTableName;
|
||
$database = $this->wire()->database;
|
||
$engine = $this->wire()->config->dbEngine;
|
||
$charset = $this->wire()->config->dbCharset;
|
||
|
||
$database->query("DROP TABLE IF EXISTS $table");
|
||
|
||
$sql =
|
||
"CREATE TABLE $table (" .
|
||
"pages_id int(10) unsigned NOT NULL, " .
|
||
"language_id int unsigned NOT NULL DEFAULT 0, " .
|
||
"path text CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, " .
|
||
"PRIMARY KEY (pages_id, language_id), " .
|
||
"UNIQUE KEY path (path(500), language_id), " .
|
||
"INDEX language_id (language_id), " .
|
||
"FULLTEXT KEY path_fulltext (path)" .
|
||
") ENGINE=$engine DEFAULT CHARSET=$charset";
|
||
|
||
$database->query($sql);
|
||
}
|
||
|
||
/**
|
||
* Uninstall the module
|
||
*
|
||
*/
|
||
public function ___uninstall() {
|
||
$this->wire()->database->query("DROP TABLE " . self::dbTableName);
|
||
}
|
||
|
||
/**
|
||
* Module config
|
||
*
|
||
* @param InputfieldWrapper $inputfields
|
||
*
|
||
*/
|
||
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
|
||
|
||
$session = $this->wire()->session;
|
||
$input = $this->wire()->input;
|
||
$numPages = 0;
|
||
$numRows = -1;
|
||
|
||
if($input->requestMethod('POST')) {
|
||
if($input->post('_rebuild')) $session->setFor($this, 'rebuild', true);
|
||
} else {
|
||
$numPages = $this->wire()->pages->count("id>0, include=all");
|
||
if($session->getFor($this, 'rebuild')) {
|
||
$session->removeFor($this, 'rebuild');
|
||
$timer = Debug::timer();
|
||
$this->rebuild();
|
||
$elapsed = Debug::timer($timer);
|
||
$this->message(sprintf($this->_('Completed rebuild in %d seconds'), $elapsed), Notice::noGroup);
|
||
} else {
|
||
$table = self::dbTableName;
|
||
$query = $this->wire()->database->prepare("SELECT COUNT(*) FROM $table");
|
||
if($this->executeQuery($query, false)) {
|
||
$numRows = (int) $query->fetchColumn();
|
||
$query->closeCursor();
|
||
}
|
||
}
|
||
}
|
||
|
||
$f = $inputfields->InputfieldCheckbox;
|
||
$f->attr('name', '_rebuild');
|
||
$f->label = sprintf($this->_('Rebuild page paths index for %d pages'), $numPages);
|
||
$f->label2 = $this->_('Rebuild now');
|
||
if($numPages) $f->description =
|
||
$this->_('Estimated rebuild time is up to 5 seconds per 1000 pages.') . ' ' .
|
||
sprintf($this->_('There are %d pages to process.'), $numPages);
|
||
if($numRows > 0) {
|
||
$f->notes = sprintf($this->_('There are currently %d rows stored by this module (path paths and versions of path paths).'), $numRows);
|
||
} else if($numRows === 0 && $input->requestMethod('GET')) {
|
||
$this->warning($this->_('Please choose the “rebuild now” option to create your page paths index.'));
|
||
}
|
||
|
||
$inputfields->add($f);
|
||
}
|
||
|
||
}
|