mirror of
https://github.com/processwire/processwire.git
synced 2025-08-22 22:34:15 +02:00
Add $page->addUrl($url, [$language]);
and $page->removeUrl($url);
methods that allow you to add or remove redirects to a page programatically. This is provided by updates to the PagePathHistory module, which also received several unrelated updates, like support for virtual path history, which is historical URLs for a page determined by changes to parent pages.
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
* @property string $title The page’s title (headline) text
|
||||
* @property string $path The page’s URL path from the homepage (i.e. /about/staff/ryan/)
|
||||
* @property string $url The page’s URL path from the server's document root
|
||||
* @property array $urls All URLs the page is accessible from, whether current, former and multi-language. #pw-advanced
|
||||
* @property array $urls All URLs the page is accessible from, whether current, former and multi-language. #pw-group-urls
|
||||
* @property string $httpUrl Same as $page->url, except includes scheme (http or https) and hostname.
|
||||
* @property Page|string|int $parent The parent Page object or a NullPage if there is no parent. For assignment, you may also use the parent path (string) or id (integer). #pw-group-traversal
|
||||
* @property Page|null $parentPrevious Previous parent, if parent was changed. #pw-group-traversal
|
||||
@@ -47,6 +47,7 @@
|
||||
* @property int $hasChildren The number of visible children this page has. Excludes unpublished, no-access, hidden, etc. #pw-group-traversal
|
||||
* @property int $numVisibleChildren Verbose alias of $hasChildren #pw-internal
|
||||
* @property int $numDescendants Number of descendants (quantity of children, and their children, and so on). @since 3.0.116 #pw-group-traversal
|
||||
* @property int $numParents Number of parent pages (i.e. depth) @since 3.0.117 #pw-group-traversal
|
||||
* @property PageArray $children All the children of this page. Returns a PageArray. See also $page->children($selector). #pw-group-traversal
|
||||
* @property Page|NullPage $child The first child of this page. Returns a Page. See also $page->child($selector). #pw-group-traversal
|
||||
* @property PageArray $siblings All the sibling pages of this page. Returns a PageArray. See also $page->siblings($selector). #pw-group-traversal
|
||||
@@ -72,7 +73,7 @@
|
||||
* @property int|null $statusPrevious Previous status, if status was changed. #pw-group-status
|
||||
* @property string statusStr Returns space-separated string of status names active on this page. #pw-group-status
|
||||
* @property Fieldgroup $fieldgroup Fieldgroup used by page template. Shorter alias for $page->template->fieldgroup (same as $page->fields) #pw-advanced
|
||||
* @property string $editUrl URL that this page can be edited at. #pw-group-advanced
|
||||
* @property string $editUrl URL that this page can be edited at. #pw-group-urls
|
||||
* @property string $editURL Alias of $editUrl. #pw-internal
|
||||
* @property PageRender $render May be used for field markup rendering like $page->render->title. #pw-advanced
|
||||
* @property bool $loaderCache Whether or not pages loaded as a result of this one may be cached by PagesLoaderCache. #pw-internal
|
||||
@@ -114,6 +115,12 @@
|
||||
* @property bool $sortable #pw-group-access
|
||||
* @property bool $listable #pw-group-access
|
||||
*
|
||||
* Methods added by PagePathHistory.module (installed by default)
|
||||
* --------------------------------------------------------------
|
||||
* @method bool addUrl($url, $language = null) Add a new URL that redirects to this page and save immediately (returns false if already taken). #pw-group-urls #pw-group-manipulation
|
||||
* @method bool removeUrl($url) Remove a URL that redirects to this page and save immediately. #pw-group-urls #pw-group-manipulation
|
||||
* Note: you can use the $page->urls() method to get URLs added by PagePathHistory.
|
||||
*
|
||||
* Methods added by LanguageSupport.module (not installed by default)
|
||||
* -----------------------------------------------------------------
|
||||
* @method Page setLanguageValue($language, $field, $value) Set value for field in language (requires LanguageSupport module). $language may be ID, language name or Language object. Field should be field name (string). #pw-group-languages
|
||||
@@ -122,9 +129,9 @@
|
||||
* Methods added by LanguageSupportPageNames.module (not installed by default)
|
||||
* ---------------------------------------------------------------------------
|
||||
* @method string localName($language = null, $useDefaultWhenEmpty = false) Return the page name in the current user’s language, or specify $language argument (Language object, name, or ID), or TRUE to use default page name when blank (instead of 2nd argument). #pw-group-languages
|
||||
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
|
||||
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
|
||||
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages
|
||||
* @method string localPath($language = null) Return the page path in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
* @method string localUrl($language = null) Return the page URL in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
* @method string localHttpUrl($language = null) Return the page URL (including scheme and hostname) in the current user's language, or specify $language argument (Language object, name, or ID). #pw-group-languages #pw-group-urls
|
||||
*
|
||||
* Methods added by ProDrafts.module (if installed)
|
||||
* ------------------------------------------------
|
||||
@@ -2851,6 +2858,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
*
|
||||
* #pw-hookable
|
||||
* #pw-group-common
|
||||
* #pw-group-urls
|
||||
*
|
||||
* ~~~~~
|
||||
* // Difference between path and url on site running from subdirectory /my-site/
|
||||
@@ -2976,6 +2984,9 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* ]);
|
||||
* ~~~~~
|
||||
*
|
||||
* #pw-group-common
|
||||
* #pw-group-urls
|
||||
*
|
||||
* @param array|int|string|bool|Language|null $options Optionally specify options to modify default behavior (see method description).
|
||||
* @return string Returns page URL, for example: `/my-site/about/contact/`
|
||||
* @see Page::path(), Page::httpUrl(), Page::editUrl(), Page::localUrl()
|
||||
@@ -3002,7 +3013,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* - If PagePathHistory core module is not installed then past/historical URLs are excluded.
|
||||
* - You can disable past/historical or multi-language URLs by using the $options argument.
|
||||
*
|
||||
* #pw-advanced
|
||||
* #pw-group-urls
|
||||
*
|
||||
* @param array $options Options to modify default behavior:
|
||||
* - `http` (bool): Make URLs include current scheme and hostname (default=false).
|
||||
@@ -3012,6 +3023,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* Note: the `languages` option must be true if using the `language` option.
|
||||
* @return array
|
||||
* @since 3.0.107
|
||||
* @see Page::addUrl(), page::removeUrl()
|
||||
*
|
||||
*/
|
||||
public function urls($options = array()) {
|
||||
@@ -3034,6 +3046,9 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* echo "<a href='$page->httpUrl'>$page->title</a>";
|
||||
* ~~~~~
|
||||
*
|
||||
* #pw-group-common
|
||||
* #pw-group-urls
|
||||
*
|
||||
* @param array $options For details on usage see `Page::url()` options argument.
|
||||
* @return string Returns full URL to page, for example: `https://processwire.com/about/`
|
||||
* @see Page::url(), Page::localHttpUrl()
|
||||
@@ -3072,7 +3087,7 @@ class Page extends WireData implements \Countable, WireMatchable {
|
||||
* }
|
||||
* ~~~~~~
|
||||
*
|
||||
* #pw-group-advanced
|
||||
* #pw-group-urls
|
||||
*
|
||||
* @param array|bool $options Specify boolean true to force URL to include scheme and hostname, or use $options array:
|
||||
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
|
||||
|
@@ -20,7 +20,7 @@ class PagePathHistory extends WireData implements Module {
|
||||
public static function getModuleInfo() {
|
||||
return array(
|
||||
'title' => 'Page Path History',
|
||||
'version' => 3,
|
||||
'version' => 4,
|
||||
'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,
|
||||
'autoload' => true,
|
||||
@@ -64,6 +64,8 @@ class PagePathHistory extends WireData implements Module {
|
||||
$this->pages->addHook('renamed', $this, 'hookPageMoved');
|
||||
$this->pages->addHook('deleted', $this, 'hookPageDeleted');
|
||||
$this->addHook('ProcessPageView::pageNotFound', $this, 'hookPageNotFound');
|
||||
$this->addHook('Page::addUrl', $this, 'hookPageAddUrl');
|
||||
$this->addHook('Page::removeUrl', $this, 'hookPageRemoveUrl');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,18 +117,45 @@ class PagePathHistory extends WireData implements Module {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a history path for a page
|
||||
* Set a history path for a page and delete any existing entries for page’s current path
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $path
|
||||
* @param Language|int $language
|
||||
* @return bool True on success, or false if path already consumed in history
|
||||
*
|
||||
*/
|
||||
public function setPathHistory(Page $page, $path, $language = null) {
|
||||
|
||||
$database = $this->wire('database');
|
||||
$table = self::dbTableName;
|
||||
$result = $this->addPathHistory($page, $path, $language);
|
||||
|
||||
if($result) {
|
||||
// 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->bindValue(":path", rtrim($this->wire('sanitizer')->pagePathName($page->path, Sanitizer::toAscii), '/'));
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a history path for a page
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $path
|
||||
* @param null|Language $language
|
||||
* @return bool True if path was added, or false if it likely overlaps with an existing path
|
||||
*
|
||||
*/
|
||||
public function addPathHistory(Page $page, $path, $language = null) {
|
||||
|
||||
$database = $this->wire('database');
|
||||
$table = self::dbTableName;
|
||||
$path = $this->wire('sanitizer')->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
|
||||
if($this->wire('pages')->count("path=$path")) return false;
|
||||
$language = $this->getLanguage($language);
|
||||
|
||||
$sql = "INSERT INTO $table SET path=:path, pages_id=:pages_id, created=NOW()";
|
||||
@@ -138,15 +167,39 @@ class PagePathHistory extends WireData implements Module {
|
||||
if($language) $query->bindValue(':language_id', $language->id, \PDO::PARAM_INT);
|
||||
|
||||
try {
|
||||
$query->execute();
|
||||
$result = $query->execute();
|
||||
} catch(\Exception $e) {
|
||||
// ignore the exception because it means there is already a past URL (duplicate)
|
||||
$result = false;
|
||||
}
|
||||
|
||||
// delete any possible entries that overlap with the $page since are no longer applicable
|
||||
$query = $database->prepare("DELETE FROM $table WHERE path=:path LIMIT 1");
|
||||
$query->bindValue(":path", rtrim($this->wire('sanitizer')->pagePathName($page->path, Sanitizer::toAscii), '/'));
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete path entry for given page and path
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $path
|
||||
* @return int
|
||||
*
|
||||
*/
|
||||
public function deletePathHistory(Page $page, $path) {
|
||||
|
||||
$database = $this->wire('database');
|
||||
$table = self::dbTableName;
|
||||
$path = $this->wire('sanitizer')->pagePathName('/' . trim($path, '/'), Sanitizer::toAscii);
|
||||
|
||||
$sql = "DELETE FROM $table WHERE path=:path AND pages_id=:pages_id LIMIT 1";
|
||||
$query = $database->prepare($sql);
|
||||
$query->bindValue(':path', $path);
|
||||
$query->bindValue(':pages_id', $page->id, \PDO::PARAM_INT);
|
||||
$query->execute();
|
||||
|
||||
$cnt = $query->rowCount();
|
||||
$query->closeCursor();
|
||||
|
||||
return $cnt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,6 +213,8 @@ class PagePathHistory extends WireData implements Module {
|
||||
* @param Language|null|array|bool Specify an option below:
|
||||
* - `language` (Language|int|string): Limit returned paths to this language. If none specified, then all languages are included.
|
||||
* - `verbose` (bool): Return associative array for each path with additional info (date and language, if present).
|
||||
* - `virtual` (bool): Return history that includes auto-determined virtual entries from parent history? (default=true)
|
||||
* What this does is also include changes to parent pages that would affect overall URL to requested page.
|
||||
* - Or you may specify the `language` option for the options argument.
|
||||
* - Or you may specify boolean `true` for options argument as a shortcut for the `verbose` option.
|
||||
* @return array of paths
|
||||
@@ -167,9 +222,13 @@ class PagePathHistory extends WireData implements Module {
|
||||
*/
|
||||
public function getPathHistory(Page $page, $options = array()) {
|
||||
|
||||
static $level = 0;
|
||||
$level++;
|
||||
|
||||
$defaults = array(
|
||||
'language' => !is_array($options) && !is_bool($options) ? $options : null,
|
||||
'verbose' => is_bool($options) ? $options : false,
|
||||
'virtual' => true,
|
||||
);
|
||||
|
||||
/** @var WireDatabasePDO $database */
|
||||
@@ -231,12 +290,184 @@ class PagePathHistory extends WireData implements Module {
|
||||
$value = $path;
|
||||
}
|
||||
|
||||
$paths[] = $value;
|
||||
$paths[$path] = $value;
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
// intentionally blank
|
||||
}
|
||||
|
||||
if($options['virtual']) {
|
||||
// get changes to current and previous parents as well
|
||||
foreach($paths as $value) {
|
||||
$virtualPaths = $this->getVirtualHistory($page, $value, $options);
|
||||
foreach($virtualPaths as $virtualPath => $virtualInfo) {
|
||||
if(isset($paths[$virtualPath])) continue;
|
||||
$paths[$virtualPath] = $virtualInfo;
|
||||
}
|
||||
}
|
||||
if($level === 1 && $options['verbose']) {
|
||||
$paths = $this->sortVerbosePathInfo($paths);
|
||||
}
|
||||
}
|
||||
|
||||
$level--;
|
||||
|
||||
return array_values($paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort verbose paths by date
|
||||
*
|
||||
* @param array $paths Verbose paths
|
||||
* @param bool $newest Sort newest to oldest? Specify false so sort oldest to newest. (default=true)
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
protected function sortVerbosePathInfo(array $paths, $newest = true) {
|
||||
|
||||
$sortPaths = array();
|
||||
|
||||
foreach($paths as $value) {
|
||||
$date = strtotime($value['date']);
|
||||
while(isset($sortPaths[$date])) $date++;
|
||||
$sortPaths[$date] = $value;
|
||||
}
|
||||
|
||||
if($newest) {
|
||||
krsort($sortPaths);
|
||||
} else {
|
||||
ksort($sortPaths);
|
||||
}
|
||||
|
||||
return $sortPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get history which includes entries not actually in pages_paths table reflecting changes to parents
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string|array $path
|
||||
* @param array $options
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
protected function getVirtualHistory(Page $page, $path, array $options) {
|
||||
|
||||
$paths = array();
|
||||
$checkParents = array();
|
||||
|
||||
if(is_array($path)) {
|
||||
// path is verbose info
|
||||
$pathInfo = $path;
|
||||
$path = $pathInfo['path'];
|
||||
} else {
|
||||
// path is string
|
||||
$pathInfo = array('path');
|
||||
}
|
||||
|
||||
// separate page name and parent path
|
||||
$parts = explode('/', trim($path, '/'));
|
||||
$pageName = array_pop($parts);
|
||||
$parentPath = implode('/', $parts);
|
||||
|
||||
// if page’s current parent is not homepage, include it
|
||||
if($page->parent_id > 1) {
|
||||
$checkParents[] = $page->parent;
|
||||
}
|
||||
|
||||
// if historical parent path differs from page’s current parent path, include it
|
||||
if($parentPath !== '/' && $parentPath != $page->parent()->path()) {
|
||||
$parent = $this->wire('pages')->get("/$parentPath");
|
||||
if(!$parent->id) $parent = $this->getPage($parentPath);
|
||||
// if parent from path is different from current page parent, include in our list of parents to check
|
||||
if($parent->id > 1 && $parent->id != $page->parent_id) {
|
||||
$checkParents[] = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
// get paths for each parent
|
||||
foreach($checkParents as $parent) {
|
||||
$parentPaths = $this->getVirtualHistoryParent($page, $pageName, $pathInfo, $parent, $options);
|
||||
foreach($parentPaths as $parentPath => $parentInfo) {
|
||||
if(!isset($paths[$parentPath])) {
|
||||
$paths[$parentPath] = $parentInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get virtual history for page in context of a specific parent (companion to getVirtualHistory method)
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $pageName Historical name (or same as page->name)
|
||||
* @param array|string $pagePathInfo Path or pathInfo array
|
||||
* @param Page $parent
|
||||
* @param array $options
|
||||
* @return array
|
||||
*
|
||||
*/
|
||||
protected function getVirtualHistoryParent(Page $page, $pageName, array $pagePathInfo, Page $parent, array $options) {
|
||||
|
||||
$paths = array();
|
||||
|
||||
// get path history for this parent
|
||||
$parentPaths = $this->getPathHistory($parent, $options);
|
||||
|
||||
// pageNamesDates is array of name => timestamp
|
||||
$pageNamesDates = array(
|
||||
$pageName => isset($pagePathInfo['date']) ? strtotime($pagePathInfo['date']) : 0
|
||||
);
|
||||
|
||||
// if historical name differs from current name, include current name in pageNamesDates
|
||||
if($page->name != $pageName) {
|
||||
$pageNamesDates[$page->name] = $page->modified;
|
||||
}
|
||||
|
||||
// iterate through each of the names this page has had, along with the date that it was changed to it
|
||||
foreach($pageNamesDates as $name => $date) {
|
||||
|
||||
// iterate through all possible parent paths
|
||||
foreach($parentPaths as $parentPathInfo) {
|
||||
|
||||
$parentPath = $options['verbose'] ? $parentPathInfo['path'] : $parentPathInfo;
|
||||
|
||||
// create path that is historical parent path plus current iteration of page name
|
||||
$path = $parentPath . '/' . $name;
|
||||
|
||||
// if we've already got this path, skip it
|
||||
if(isset($paths[$path])) continue;
|
||||
|
||||
// non-verbose mode only includes paths
|
||||
if(empty($options['verbose'])) {
|
||||
$paths[$path] = $path;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if parent change date is older than page change date, then we can skip it
|
||||
if(strtotime($parentPathInfo['date']) < $date) continue;
|
||||
|
||||
// $path .= " $parentInfo[date] | $pathInfo[date]";
|
||||
|
||||
// create verbose info for this entry
|
||||
$pathInfo = array(
|
||||
'path' => $path,
|
||||
'date' => $parentPathInfo['date'],
|
||||
'virtual' => $parent->id
|
||||
);
|
||||
|
||||
// if parent is specific to a language, include that info in the verbose value
|
||||
if(isset($parentPathInfo['language'])) {
|
||||
$pathInfo['language'] = $parentPathInfo['language'];
|
||||
}
|
||||
|
||||
$paths[$path] = $pathInfo;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
@@ -447,6 +678,53 @@ class PagePathHistory extends WireData implements Module {
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation for $page->addUrl($url, [$language]) method
|
||||
*
|
||||
* @param HookEvent $event
|
||||
*
|
||||
*/
|
||||
public function hookPageAddUrl(HookEvent $event) {
|
||||
/** @var Page $page */
|
||||
$page = $event->object;
|
||||
/** @var string $url */
|
||||
$url = $event->arguments(0);
|
||||
/** @var Language|null $language */
|
||||
$language = $event->arguments(1);
|
||||
$event->return = $this->addPathHistory($page, $this->urlToPath($url), $language);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation for $page->removeUrl($url, [$language]) method
|
||||
*
|
||||
* @param HookEvent $event
|
||||
*
|
||||
*/
|
||||
public function hookPageRemoveUrl(HookEvent $event) {
|
||||
/** @var page $page */
|
||||
$page = $event->object;
|
||||
/** @var string $url */
|
||||
$url = $event->arguments(0);
|
||||
$event->return = (bool) $this->deletePathHistory($page, $this->urlToPath($url));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given URL that may include a root subdirectory, convert it to path relative to root subdirectory
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*
|
||||
*/
|
||||
protected function urlToPath($url) {
|
||||
$rootUrl = $this->wire('config')->urls->root;
|
||||
if(strlen($rootUrl) > 1 && strpos($url, $rootUrl) === 0) {
|
||||
$path = substr($url, strlen($rootUrl) - 1);
|
||||
} else {
|
||||
$path = $url;
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install
|
||||
*
|
||||
|
Reference in New Issue
Block a user