Created
March 21, 2015 14:59
-
-
Save bfncs/4f52979da5ff7bf67d8a to your computer and use it in GitHub Desktop.
Multilanguage page name aware Page Path History module for Processwire
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* ProcessWire Page Path History | |
* | |
* Keeps track of past URLs where pages have lived and automatically 301 redirects | |
* to the new location whenever the past URL is accessed. | |
* This module is a fork of the original PagePathHistory by Ryan Cramer in the Processwire | |
* Core and should ideally be merged back there. | |
* | |
* Licensed under GNU/GPL v2 | |
*/ | |
class PagePathHistoryLanguage extends WireData implements Module { | |
public static function getModuleInfo() { | |
return array( | |
'title' => 'Page Path History Language', | |
'version' => 1, | |
'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. Handles multilanguage pagenames.", | |
'singular' => true, | |
'autoload' => true, | |
); | |
} | |
/** | |
* Table created by this module | |
* | |
*/ | |
const dbTableName = 'page_path_history_language'; | |
/** | |
* Minimum age in seconds that a page must be before we'll bother remembering it's previous path | |
* | |
*/ | |
const minimumAge = 120; | |
/** | |
* Maximum segments to support in a redirect URL | |
* | |
* Used to place a limit on recursion and paths | |
* | |
*/ | |
const maxSegments = 10; | |
/** | |
* @var bool True if multilanguage page name support is active | |
*/ | |
protected $isMultilanguage; | |
/** | |
* @var string The path that was requested, before any processing | |
*/ | |
protected $requestPath = ''; | |
/** | |
* Initialize the hooks | |
* | |
*/ | |
public function init() { | |
$this->addHookBefore('ProcessPageView::execute', $this, 'hookProcessPageViewExecute', array('priority' => 90)); | |
$this->pages->addHook('moved', $this, 'hookPageMoved'); | |
$this->pages->addHook('renamed', $this, 'hookPageMoved'); | |
$this->pages->addHook('deleted', $this, 'hookPageDeleted'); | |
$this->addHook('ProcessPageView::pageNotFound', $this, 'hookPageNotFound'); | |
} | |
/** | |
* Initialize the multilanguage specific hooks | |
*/ | |
public function ready() { | |
$this->isMultilanguage = $this->modules->isInstalled('LanguageSupportPageNames') && | |
count(wire('languages')); | |
if ($this->isMultilanguage) { | |
$this->addHookAfter('Page::loaded', $this, 'hookPageLoaded'); | |
$this->addHookAfter('Pages::saveReady', $this, 'hookPageSaveReady'); | |
$this->addHookAfter('Pages::saved', $this, 'hookPageSaved'); | |
} | |
} | |
public function hookProcessPageViewExecute(HookEvent $event) { | |
// Save now, since ProcessPageView removes $_GET['it'] when it executes | |
$it = isset($_GET['it']) ? $_GET['it'] : ''; | |
// Add leading slash if missing | |
if ('/' !== substr($it, 0, 1)) { | |
$it = "/{$it}"; | |
} | |
$this->requestPath = $it; | |
} | |
public function hookPageLoaded(HookEvent $event) { | |
$page = $event->object; | |
$pageId = $page->id; | |
// Return if page has already been processed or is admin page | |
if ($page->namePreviousLanguage || 'admin' === $page->template) { | |
return; | |
} | |
// Add all page name language versions to $page->namePreviousLanguage | |
$languages = $this->wire('languages'); | |
$languageNames = array(); | |
foreach ($languages as $language) { | |
if ($language->isDefault()) { | |
continue; | |
} | |
$languageNames[$language->id] = $page->localName($language); | |
} | |
$page->namePreviousLanguage = $languageNames; | |
} | |
public function hookPageSaveReady(HookEvent $event) { | |
$page = $event->arguments[0]; | |
$pageId = $page->id; | |
if (empty($page->namePreviousLanguage) || 'admin' === $page->template) { | |
return; | |
} | |
// Remove all unchanged language names from $page->namePreviousLanguage | |
$languages = $this->wire('languages'); | |
$languageNames = $page->namePreviousLanguage; | |
foreach($languages as $language) { | |
if ($language->isDefault() || empty($languageNames[$language->id])) { | |
continue; | |
} | |
$currentName = $page->localName($language); | |
$previousName = $languageNames[$language->id]; | |
if ($currentName === $previousName) { | |
unset($languageNames[$language->id]); | |
} | |
} | |
$page->namePreviousLanguage = $languageNames; | |
} | |
/** | |
* Trigger hookPageMoved after save if not moved or renamed in primary language. | |
* @param HookEvent $event | |
*/ | |
public function hookPageSaved(HookEvent $event) { | |
$page = $event->arguments[0]; | |
$changes = $event->arguments[1]; | |
// No need to do anything if page is renamed or moved anyway | |
$renamed = $page->namePrevious && ($page->namePrevious !== $page->name); | |
$moved = (bool) $page->parentPrevious; | |
if ($renamed || $moved) { | |
return; | |
} | |
// Trigger hook manually if name was changed in any language | |
$nameLanguageChanged = false; | |
$languages = wire('languages'); | |
foreach ($languages as $language) { | |
if ($language->isDefault()) { | |
continue; | |
} | |
$fieldName = "name{$language->id}"; | |
if (in_array($fieldName, $changes)) { | |
$nameLanguageChanged = true; | |
break; | |
} | |
} | |
if ($nameLanguageChanged) { | |
$this->hookPageMoved($event); | |
} | |
} | |
/** | |
* Hook called when a page is moved or renamed | |
* | |
*/ | |
public function hookPageMoved(HookEvent $event) { | |
$page = $event->arguments[0]; | |
if($page->template == 'admin') return; | |
$age = time() - $page->created; | |
if($age < self::minimumAge) 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 | |
$parentPage = $page->parentPrevious; | |
} else { | |
// the start of our redirect URL will be the current parent's URL (i.e. name changed) | |
$parentPage = $page->parent; | |
} | |
$paths = array(); | |
$currentPaths = array(); | |
// Process default language page name | |
$pageName = $page->namePrevious ? | |
$page->namePrevious : | |
$page->name; | |
$pastPath = $parentPage->path . $pageName; | |
$currentPath = $page->path; | |
if ($currentPath !== $pastPath) { | |
$paths[] = $pastPath; | |
$currentPaths[] = $currentPath; | |
} | |
// Process language page names | |
if ($this->isMultilanguage) { | |
$languages = wire('languages'); | |
/* @var LanguageSupportPageNames $languagePageNames */ | |
$languagePageNames = $this->modules->get('LanguageSupportPageNames'); | |
foreach ($languages as $language) { | |
if ($language->isDefault()) { | |
continue; | |
} | |
$parentPagePath = $parentPage->localPath($language); | |
$pageName = !empty($page->namePreviousLanguage[$language->id]) ? | |
$page->namePreviousLanguage[$language->id] : | |
$page->localName($language); | |
$pastPath = $parentPagePath . $pageName; | |
$currentPath = $page->localPath($language); | |
if ($currentPath !== $pastPath) { | |
// Remove language prefix from path | |
$paths[] = $pastPath; | |
$currentPaths[] = $currentPath; | |
} | |
} | |
} | |
/* @var WireDatabasePDO $database | |
* @var PDOStatement $query */ | |
$database = $this->wire('database'); | |
$query = $database->prepare("INSERT INTO " . self::dbTableName . " SET path=:path, pages_id=:pages_id, created=NOW()"); | |
// Insert all past paths | |
foreach ($paths as $path) { | |
$query->bindValue(":path", $path); | |
$query->bindValue(":pages_id", $page->id, PDO::PARAM_INT); | |
try { | |
$query->execute(); | |
} catch(Exception $e) { | |
// catch the exception because it means there is already a past URL (duplicate) | |
} | |
} | |
// delete any possible entries that overlap with the $page since are no longer applicable | |
$query = $database->prepare("DELETE FROM " . self::dbTableName . " WHERE path=:path LIMIT 1"); | |
foreach ($currentPaths as $path) { | |
$query->bindValue(":path", rtrim($path, '/')); | |
$query->execute(); | |
} | |
} | |
/** | |
* Hook called upon 404 from ProcessPageView::pageNotFound | |
* | |
*/ | |
public function hookPageNotFound(HookEvent $event) { | |
$page = $event->arguments[0]; | |
// 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) return; | |
$page = $this->getPage($this->requestPath); | |
if($page->id && $page->viewable()) { | |
// if a page was found, redirect to it | |
$this->session->redirect($page->url); | |
} | |
} | |
/** | |
* Given a previously existing path, return the matching Page object or NullPage if not found. | |
* | |
* @param string $path Historical path of page you want to retrieve | |
* @param int $level Recursion level for internal recursive use only | |
* @return Page|NullPage | |
* | |
*/ | |
protected function getPage($path, $level = 0) { | |
$page = new NullPage(); | |
$pathRemoved = ''; | |
$path = rtrim($path, '/'); | |
$cnt = 0; | |
/* @var Database $database */ | |
$database = $this->wire('database'); | |
while(strlen($path) && !$page->id && $cnt < self::maxSegments) { | |
$query = $database->prepare("SELECT pages_id FROM " . self::dbTableName . " WHERE path=:path"); | |
$query->bindValue(":path", $path); | |
$query->execute(); | |
if($query->rowCount() > 0) { | |
$pages_id = $query->fetchColumn(); | |
$page = $this->pages->get((int) $pages_id); | |
} else { | |
$pos = strrpos($path, '/'); | |
$pathRemoved = substr($path, $pos) . $pathRemoved; | |
$path = substr($path, 0, $pos); | |
} | |
$query->closeCursor(); | |
$cnt++; | |
} | |
// if no page was found, then we can stop trying now | |
if(!$page->id) return $page; | |
if($cnt > 1) { | |
// a parent match was found if our counter is > 1 | |
$parent = $page; | |
// use the new parent path and add the removed components back on to it | |
$path = rtrim($parent->path, '/') . $pathRemoved; | |
// see if it might exist at the new parent's URL | |
$page = $this->pages->get($path); | |
// if not, then go recursive, trying again | |
if(!$page->id && $level < self::maxSegments) $page = $this->getPage($path, $level+1); | |
} | |
return $page; | |
} | |
/** | |
* When a page is deleted, remove it from our redirects list as well | |
* | |
*/ | |
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(); | |
} | |
public function ___install() { | |
$sql = "CREATE TABLE " . self::dbTableName . " (" . | |
"path VARCHAR(255) NOT NULL, " . | |
"pages_id INT UNSIGNED NOT NULL, " . | |
"created TIMESTAMP NOT NULL, " . | |
"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); | |
} | |
public function ___uninstall() { | |
$this->wire('database')->query("DROP TABLE " . self::dbTableName); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment