287 lines
7.9 KiB
Text
287 lines
7.9 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 2016 by Ryan Cramer
|
||
|
* https://processwire.com
|
||
|
*
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
class PagePaths extends WireData implements Module {
|
||
|
|
||
|
public static function getModuleInfo() {
|
||
|
return array(
|
||
|
'title' => 'Page Paths',
|
||
|
'version' => 1,
|
||
|
'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). Currently supports only single languages sites.",
|
||
|
'singular' => true,
|
||
|
'autoload' => true,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Table created by this module
|
||
|
*
|
||
|
*/
|
||
|
const dbTableName = 'pages_paths';
|
||
|
|
||
|
/**
|
||
|
* @var Languages|false
|
||
|
*
|
||
|
*/
|
||
|
protected $languages = null;
|
||
|
|
||
|
/**
|
||
|
* Initialize the hooks
|
||
|
*
|
||
|
*/
|
||
|
public function init() {
|
||
|
$this->pages->addHook('moved', $this, 'hookPageMoved');
|
||
|
$this->pages->addHook('renamed', $this, 'hookPageMoved');
|
||
|
$this->pages->addHook('added', $this, 'hookPageMoved');
|
||
|
$this->pages->addHook('deleted', $this, 'hookPageDeleted');
|
||
|
}
|
||
|
|
||
|
public function ready() {
|
||
|
$page = $this->wire('page');
|
||
|
if($page->template == 'admin' && $page->name == 'module') {
|
||
|
$this->wire('modules')->addHookAfter('refresh', $this, 'hookModulesRefresh');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns Languages object or false if not available
|
||
|
*
|
||
|
* @return Languages|null
|
||
|
*
|
||
|
*/
|
||
|
public function getLanguages() {
|
||
|
if(!is_null($this->languages)) return $this->languages;
|
||
|
$languages = $this->wire('languages');
|
||
|
if(!$languages) return null;
|
||
|
if(!$this->wire('modules')->isInstalled('LanguageSupportPageNames')) {
|
||
|
$this->languages = false;
|
||
|
} else {
|
||
|
$this->languages = $this->wire('languages');
|
||
|
}
|
||
|
return $this->languages;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Hook to ProcessModule::refresh
|
||
|
*
|
||
|
* @param HookEvent $event
|
||
|
*
|
||
|
*/
|
||
|
public function hookModulesRefresh(HookEvent $event) {
|
||
|
if($event) {} // ignore
|
||
|
if($this->getLanguages()) {
|
||
|
$this->wire('session')->warning(
|
||
|
$this->_('Please uninstall the Core > PagePaths module (it is not compatible with LanguageSupportPageNames)')
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* When a page is deleted
|
||
|
*
|
||
|
* @param HookEvent $event
|
||
|
*
|
||
|
*/
|
||
|
public function hookPageDeleted(HookEvent $event) {
|
||
|
$page = $event->arguments[0];
|
||
|
$database = $this->wire('database');
|
||
|
$query = $database->prepare("DELETE FROM " . self::dbTableName . " WHERE pages_id=:pages_id");
|
||
|
$query->bindValue(":pages_id", $page->id, \PDO::PARAM_INT);
|
||
|
$query->execute();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a page ID, return the page path, NULL if not found, or boolean false if cannot be determined.
|
||
|
*
|
||
|
* @param int $id
|
||
|
* @return string|null|false
|
||
|
*
|
||
|
*/
|
||
|
public function getPath($id) {
|
||
|
if($this->getLanguages()) return false; // we do not support multi-language yet for this module
|
||
|
$table = self::dbTableName;
|
||
|
$database = $this->wire('database');
|
||
|
$query = $database->prepare("SELECT path FROM `$table` WHERE pages_id=:pages_id");
|
||
|
$query->bindValue(":pages_id", $id, \PDO::PARAM_INT);
|
||
|
$query->execute();
|
||
|
if(!$query->rowCount()) return null;
|
||
|
$path = $query->fetchColumn();
|
||
|
$path = strlen($path) ? $this->wire('sanitizer')->pagePathName("/$path/", Sanitizer::toUTF8) : "/";
|
||
|
return $path;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a page path, return the page ID or NULL if not found.
|
||
|
*
|
||
|
* @param string $path
|
||
|
* @return int|null
|
||
|
*
|
||
|
*/
|
||
|
public function getID($path) {
|
||
|
$table = self::dbTableName;
|
||
|
$database = $this->wire('database');
|
||
|
$path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
|
||
|
$path = trim($path, '/');
|
||
|
$query = $database->prepare("SELECT pages_id FROM $table WHERE path=:path");
|
||
|
$query->bindValue(":path", $path);
|
||
|
$query->execute();
|
||
|
if(!$query->rowCount()) return null;
|
||
|
$id = $query->fetchColumn();
|
||
|
return (int) $id;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
$n++;
|
||
|
$table = self::dbTableName;
|
||
|
$alias = "$table$n";
|
||
|
$value = $selector->value;
|
||
|
// $joinType = $selector->not ? 'leftjoin' : 'join';
|
||
|
|
||
|
$query->join("$table AS $alias ON pages.id=$alias.pages_id");
|
||
|
|
||
|
if(in_array($selector->operator, array('=', '!=', '<>', '>', '<', '>=', '<='))) {
|
||
|
if(!is_array($value)) $value = array($value);
|
||
|
$where = '';
|
||
|
foreach($value as $path) {
|
||
|
if($where) $where .= $selector->not ? " AND " : " OR ";
|
||
|
$path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
|
||
|
$path = $this->wire('database')->escapeStr(trim($path, '/'));
|
||
|
$where .= ($selector->not ? "NOT " : "") . "$alias.path{$selector->operator}'$path'";
|
||
|
}
|
||
|
$query->where("($where)");
|
||
|
|
||
|
} else {
|
||
|
if(is_array($value)) {
|
||
|
$error = "Multi value using '|' is not supported with path/url and '$selector->operator' operator";
|
||
|
throw new PageFinderSyntaxException($error);
|
||
|
}
|
||
|
if($selector->not) {
|
||
|
$error = "NOT mode isn't yet supported with path/url and '$selector->operator' operator";
|
||
|
throw new PageFinderSyntaxException($error);
|
||
|
}
|
||
|
/** @var DatabaseQuerySelectFulltext $ft */
|
||
|
$ft = $this->wire(new DatabaseQuerySelectFulltext($query));
|
||
|
$ft->match($alias, 'path', $selector->operator, trim($value, '/'));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates path for $page and all children
|
||
|
*
|
||
|
* @param int $id
|
||
|
* @param string $path
|
||
|
* @param bool $hasChildren Omit if true or unknown
|
||
|
* @param int $level Recursion level, you should omit this param
|
||
|
* @return int Number of paths updated
|
||
|
*
|
||
|
*/
|
||
|
protected function updatePagePath($id, $path, $hasChildren = true, $level = 0) {
|
||
|
|
||
|
$table = self::dbTableName;
|
||
|
$id = (int) $id;
|
||
|
$database = $this->wire('database');
|
||
|
$path = $this->wire('sanitizer')->pagePathName($path, Sanitizer::toAscii);
|
||
|
$path = trim($path, '/');
|
||
|
$_path = $database->escapeStr($path);
|
||
|
$numUpdated = 1;
|
||
|
|
||
|
$sql = "INSERT INTO $table (pages_id, path) VALUES(:id, :path) " .
|
||
|
"ON DUPLICATE KEY UPDATE pages_id=VALUES(pages_id), path=VALUES(path)";
|
||
|
|
||
|
$query = $database->prepare($sql);
|
||
|
$query->bindValue(":id", $id, \PDO::PARAM_INT);
|
||
|
$query->bindValue(":path", $_path);
|
||
|
$query->execute();
|
||
|
|
||
|
if($hasChildren) {
|
||
|
|
||
|
$sql = "SELECT pages.id, pages.name, COUNT(children.id) 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", $id, \PDO::PARAM_INT);
|
||
|
$query->execute();
|
||
|
|
||
|
while($row = $query->fetch(\PDO::FETCH_NUM)) {
|
||
|
list($id, $name, $numChildren) = $row;
|
||
|
$numUpdated += $this->updatePagePath($id, "$path/$name", $numChildren > 0, $level+1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!$level) $this->message(sprintf($this->_n('Updated %d path', 'Updated %d paths', $numUpdated), $numUpdated));
|
||
|
|
||
|
return $numUpdated;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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, " .
|
||
|
"path text CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL, " .
|
||
|
"PRIMARY KEY pages_id (pages_id), " .
|
||
|
"UNIQUE KEY path (path(500)), " .
|
||
|
"FULLTEXT KEY path_fulltext (path)" .
|
||
|
") ENGINE=$engine DEFAULT CHARSET=$charset";
|
||
|
|
||
|
$database->query($sql);
|
||
|
$numUpdated = $this->updatePagePath(1, '/');
|
||
|
if($numUpdated) {} // ignore
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Uninstall the module
|
||
|
*
|
||
|
*/
|
||
|
public function ___uninstall() {
|
||
|
$this->wire('database')->query("DROP TABLE " . self::dbTableName);
|
||
|
}
|
||
|
|
||
|
}
|