'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); } }