artabro/wire/core/PageTraversal.php
2024-08-27 11:35:37 +02:00

1521 lines
54 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Page Traversal
*
* Provides implementation for Page traversal functions.
* Based upon the jQuery traversal functions.
*
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
* https://processwire.com
*
*/
class PageTraversal {
/**
* Return number of children, optionally with conditions
*
* Use this over $page->numChildren property when you want to specify a selector or when you want the result to
* include only visible children. See the options for the $selector argument.
*
* @param Page $page
* @param bool|string|int|array $selector
* When not specified, result includes all children without conditions, same as $page->numChildren property.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When boolean true, number includes only visible children (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all children without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable children (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @param array $options
* - `descendants` (bool): Use descendants rather than direct children
* @return int Number of children
*
*/
public function numChildren(Page $page, $selector = null, array $options = array()) {
$descendants = empty($options['descendants']) ? false : true;
$parentType = $descendants ? 'has_parent' : 'parent_id';
if(is_bool($selector)) {
// onlyVisible takes the place of selector
$onlyVisible = $selector;
$numChildren = $page->get('numChildren');
if(!$numChildren) {
return 0;
} else if($onlyVisible) {
return $page->_pages('count', "$parentType=$page->id");
} else if($descendants) {
return $this->numDescendants($page);
} else {
return $numChildren;
}
} else if($selector === 1) {
// viewable pages only
$numChildren = $page->get('numChildren');
if(!$numChildren) return 0;
$user = $page->wire()->user;
if($user->isSuperuser()) {
if($descendants) return $this->numDescendants($page);
return $numChildren;
} else if($user->hasPermission('page-edit')) {
return $page->_pages('count', "$parentType=$page->id, include=unpublished");
} else {
return $page->_pages('count', "$parentType=$page->id, include=hidden");
}
} else if(empty($selector) || (!is_string($selector) && !is_array($selector))) {
// no selector provided
if($descendants) return $this->numDescendants($page);
return $page->get('numChildren');
} else {
// selector string or array provided
if(is_string($selector)) {
$selector = "$parentType=$page->id, $selector";
} else if(is_array($selector)) {
$selector[$parentType] = $page->id;
}
return $page->_pages('count', $selector);
}
}
/**
* Return number of descendants, optionally with conditions
*
* Use this over $page->numDescendants property when you want to specify a selector or when you want the result to
* include only visible descendants. See the options for the $selector argument.
*
* @param Page $page
* @param bool|string|int|array $selector
* When not specified, result includes all descendants without conditions, same as $page->numDescendants property.
* When a string or array, a selector is assumed and quantity will be counted based on selector.
* When boolean true, number includes only visible descendants (excludes unpublished, hidden, no-access, etc.)
* When boolean false, number includes all descendants without conditions, including unpublished, hidden, no-access, etc.
* When integer 1 number includes viewable descendants (as opposed to visible, viewable includes hidden pages + it also includes unpublished pages if user has page-edit permission).
* @return int Number of descendants
*
*/
public function numDescendants(Page $page, $selector = null) {
if($selector === null) {
return $page->_pages('count', "has_parent=$page->id, include=all");
} else {
return $this->numChildren($page, $selector, array('descendants' => true));
}
}
/**
* Return this page's children pages, optionally filtered by a selector
*
* @param Page $page
* @param string|array $selector Selector to use, or blank to return all children
* @param array $options
* @return PageArray
*
*/
public function children(Page $page, $selector = '', $options = array()) {
if(!$page->numChildren) return $page->_pages()->newPageArray();
$defaults = array('caller' => 'page.children');
$options = array_merge($defaults, $options);
$sortfield = $page->sortfield();
if(is_array($selector)) {
// selector is array
$selector["parent_id"] = $page->id;
if(isset($selector["sort"])) $sortfield = '';
if($sortfield) $selector[] = array("sort", $sortfield);
} else {
// selector is string
$selector = trim("parent_id=$page->id, $selector", ", ");
if(strpos($selector, 'sort=') === false) $selector .= ", sort=$sortfield";
}
return $page->_pages('find', $selector, $options);
}
/**
* Return the page's first single child that matches the given selector.
*
* Same as children() but returns a Page object or NullPage (with id=0) rather than a PageArray
*
* @param Page $page
* @param string|array $selector Selector to use, or blank to return the first child.
* @param array $options
* @return Page|NullPage
*
*/
public function child(Page $page, $selector = '', $options = array()) {
if(!$page->numChildren) return $page->_pages()->newNullPage();
$defaults = array('getTotal' => false, 'caller' => 'page.child');
$options = array_merge($defaults, $options);
if(is_array($selector)) {
$selector["limit"] = 1;
$selector[] = array("start", "0");
} else {
$selector .= ($selector ? ', ' : '') . "limit=1";
if(strpos($selector, 'start=') === false) $selector .= ", start=0"; // prevent pagination
}
$children = $this->children($page, $selector, $options);
return count($children) ? $children->first() : $page->_pages()->newNullPage();
}
/**
* Return this page's parent pages, or the parent pages matching the given selector.
*
* @param Page $page
* @param string|array|bool $selector Optional selector string to filter parents by or boolean true for reverse order
* @return PageArray
*
*/
public function parents(Page $page, $selector = '') {
$parents = $page->wire()->pages->newPageArray();
$parent = $page->parent();
$method = $selector === true ? 'add' : 'prepend';
while($parent && $parent->id) {
$parents->$method($parent);
$parent = $parent->parent();
}
return !is_bool($selector) && strlen($selector) ? $parents->filter($selector) : $parents;
}
/**
* Return number of parents (depth relative to homepage) that this page has, optionally filtered by a selector
*
* For example, homepage has 0 parents and root level pages have 1 parent (which is the homepage), and the
* number increases the deeper the page is in the pages structure.
*
* @param Page $page
* @param string $selector Optional selector to filter by (default='')
* @return int Number of parents
*
*/
public function numParents(Page $page, $selector = '') {
$num = 0;
$parent = $page->parent();
while($parent && $parent->id) {
if($selector !== '' && !$parent->matches($selector)) continue;
$num++;
$parent = $parent->parent();
}
return $num;
}
/**
* Return all parent from current till the one matched by $selector
*
* @param Page $page
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @return PageArray
*
*/
public function parentsUntil(Page $page, $selector = '', $filter = '') {
$parents = $this->parents($page);
$matches = $page->wire()->pages->newPageArray();
$stop = false;
foreach($parents->reverse() as $parent) {
/** @var Page $parent */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $parent->id == $selector) {
$stop = true;
} else if($parent->matches($selector)) {
$stop = true;
}
} else if(is_array($selector) && !empty($selector)) {
if($parent->matches($selector)) $stop = true;
} else if(is_int($selector)) {
if($parent->id == $selector) $stop = true;
} else if($selector instanceof Page && $parent->id == $selector->id) {
$stop = true;
}
if($stop) break;
$matches->prepend($parent);
}
if(!empty($filter)) $matches->filter($filter);
return $matches;
}
/**
* Get the lowest-level, non-homepage parent of this page
*
* rootParents typically comprise the first level of navigation on a site.
*
* @param Page $page
* @return Page
*
*/
public function rootParent(Page $page) {
$parent = $page->parent;
if(!$parent || !$parent->id || $parent->id === 1) return $page;
$parents = $this->parents($page);
$parents->shift(); // shift off homepage
return $parents->first();
}
/**
* Return this Page's sibling pages, optionally filtered by a selector.
*
* Note that the siblings include the current page. To exclude the current page, specify "id!=$page".
*
* @param Page $page
* @param string $selector Optional selector to filter siblings by.
* @return PageArray
*
*/
public function siblings(Page $page, $selector = '') {
$parent = $page->parent();
$sort = $parent->sortfield();
if(is_array($selector)) {
$selector["parent_id"] = $page->parent_id;
$selector[] = array('sort', $sort);
} else {
$selector = "parent_id=$page->parent_id, $selector";
if(strpos($selector, 'sort=') === false) $selector .= ", sort=$sort";
$selector = trim($selector, ", ");
}
$options = array('caller' => 'page.siblings');
return $page->_pages('find', $selector, $options);
}
/**
* Get include mode specified in selector or blank if none
*
* @param string|array|Selectors $selector
* @return string
*
*/
protected function _getIncludeMode($selector) {
if(is_string($selector) && strpos($selector, 'include=') === false) return '';
if(is_array($selector)) return isset($selector['include']) ? $selector['include'] : '';
$selector = $selector instanceof Selectors ? $selector : new Selectors($selector);
$include = $selector->getSelectorByField('include');
return $include ? $include->value() : '';
}
/**
* Builds the PageFinder options for the _next() method
*
* @param Page $page
* @param string|array|Selectors $selector
* @param array $options
* @return array
*
*/
protected function _nextFinderOptions(Page $page, $selector, $options) {
$fo = array(
'findOne' => $options['all'] ? false : true,
'startAfterID' => $options['prev'] ? 0 : $page->id,
'stopBeforeID' => $options['prev'] ? $page->id : 0,
'returnVerbose' => $options['all'] ? false : true,
'alwaysAllowIDs' => array(),
);
if($page->isUnpublished() || $page->isHidden()) {
// allow next() to still move forward even though it is hidden or unpublished
$includeMode = $this->_getIncludeMode($selector);
if(!$includeMode || ($includeMode === 'hidden' && $page->isUnpublished())) {
$fo['alwaysAllowIDs'][] = $page->id;
}
}
if(!$options['until']) return $fo;
/***************************************************************
* All code below this specific to the 'until' option
*
*/
$until = $options['until'];
/** @var string $until */
if(is_array($until)) $until = (string) (new Selectors($until));
if(ctype_digit("$until")) {
// id or Page object
$stopPage = new WireData();
$stopPage->set('id', (int) $until);
} else if(strpos($until, '/') === 0) {
// page path
$stopPage = $page->_pages('get', $until);
} else if(is_array($selector) || is_array($options['until'])) {
// either selector or until is an array
$s = new Selectors($options['until']);
foreach(new Selectors($selector) as $item) $s->add($item);
$s->add(new SelectorEqual('limit', 1));
$stopPage = $page->_pages('find', $s)->first();
} else {
// selector string
$findOptions = $options['prev'] ? array() : array('startAfterID' => $page->id);
$stopPage = $page->_pages('find', "$selector, limit=1, $until", $findOptions)->first();
}
if($stopPage && $stopPage->id) {
if($options['prev']) {
$fo['startAfterID'] = $stopPage->id;
$fo['stopBeforeID'] = $page->id;
} else {
$fo['startAfterID'] = $page->id;
$fo['stopBeforeID'] = $stopPage->id;
}
}
return $fo;
}
/**
* Provides the core logic for next, prev, nextAll, prevAll, nextUntil, prevUntil
*
* @param Page $page
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest sibling(s) that match.
* @param array $options Options to modify behavior
* - `prev` (bool): When true, previous siblings will be returned rather than next siblings.
* - `all` (bool): If true, returns all nextAll or prevAll rather than just single sibling (default=false).
* - `until` (string): If specified, returns siblings until another is found matching the given selector (default=false).
* - `qty` (bool): If true, makes it return just the quantity that would match (default=false).
* @return Page|NullPage|PageArray|int Returns one of the following:
* - `PageArray` if the "all" or "until" option is specified.
* - `Page|NullPage` in other cases.
*
*/
protected function _next(Page $page, $selector = '', array $options = array()) {
$defaults = array(
'prev' => false, // get previous rather than next
'all' => false, // get multiple/all
'until' => '', // until selector string ('all' option assumed)
'qty' => false, // when true, returns just the quantity that would match ('all' option assumed)
);
$options = array_merge($defaults, $options);
$pages = $page->wire()->pages;
$parent = $page->parent();
if($options['until'] || $options['qty']) $options['all'] = true;
if(!$parent || !$parent->id) {
if($options['qty']) return 0;
return $options['all'] ? $pages->newPageArray() : $pages->newNullPage();
}
if(is_array($selector)) {
$selector['parent_id'] = $parent->id;
} else if(is_string($selector)) {
$selector = trim("parent_id=$parent->id, $selector", ", ");
} else if($selector instanceof Selectors) {
$selector->add(new SelectorEqual('parent_id', $parent->id));
} else {
throw new WireException('Selector must be string, array or Selectors object');
}
$pageFinder = $pages->getPageFinder();
$pageFinderOptions = $this->_nextFinderOptions($page, $selector, $options);
$rows = $pageFinder->find($selector, $pageFinderOptions);
if($options['qty']) {
$result = count($rows);
} else if(!count($rows)) {
$result = $options['all'] ? $pages->newPageArray() : $pages->newNullPage();
} else if($options['all']) {
$result = $pages->getById($rows, array(
'parent_id' => $parent->id,
'cache' => $page->loaderCache
));
if($options['prev']) $result = $result->reverse();
} else {
$row = reset($rows);
if($row && !empty($row['id'])) {
$result = $pages->getById(array($row['id']), array(
'template' => $page->wire()->templates->get($row['templates_id']),
'parent_id' => $row['parent_id'],
'getOne' => true,
'cache' => $page->loaderCache
));
} else {
$result = $pages->newNullPage();
}
}
return $result;
}
/**
* Return the index/position of the given page relative to its siblings
*
* If given a hidden or unpublished page, that page would not usually be part of the group of siblings.
* As a result, such pages will return what the value would be if they were visible (as of 3.0.121). This
* may overlap with the index of other pages, since indexes are relative to visible pages, unless you
* specify an include mode (see next paragraph).
*
* If you want this method to include hidden/unpublished pages as part of the index numbers, then
* specify boolean true for the $selector argument (which implies "include=all") OR specify a
* selector of "include=hidden", "include=unpublished" or "include=all".
*
* @param Page $page
* @param string|array|bool|Selectors $selector Selector to apply or boolean true for "include=all" (since 3.0.121).
* - Boolean true to include hidden and unpublished pages as part of the index numbers (same as "include=all").
* - An "include=hidden", "include=unpublished" or "include=all" selector to include them in the index numbers.
* - A string selector or selector array to filter the criteria for the returned index number.
* @return int Returns index number (zero-based)
*
*/
public function index(Page $page, $selector = '') {
if($selector === true) $selector = "include=all";
$index = $this->_next($page, $selector, array('prev' => true, 'all' => true, 'qty' => 'index'));
return $index;
}
/**
* Return the next sibling page
*
* @param Page $page
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest next sibling that matches.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
public function next(Page $page, $selector = '') {
return $this->_next($page, $selector);
}
/**
* Return the previous sibling page
*
* @param Page $page
* @param string|array|Selectors $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
public function prev(Page $page, $selector = '') {
return $this->_next($page, $selector, array('prev' => true));
}
/**
* Return all sibling pages after this one, optionally matching a selector
*
* @param Page $page
* @param string|array|Selectors $selector Optional selector. When specified, will filter the found siblings.
* @param array $options Options to pass to the _next() method
* @return PageArray Returns all matching pages after this one.
*
*/
public function nextAll(Page $page, $selector = '', array $options = array()) {
$defaults = array('all' => true);
$options = array_merge($options, $defaults);
return $this->_next($page, $selector, $options);
}
/**
* Return all sibling pages prior to this one, optionally matching a selector
*
* @param Page $page
* @param string|array|Selectors $selector Optional selector. When specified, will filter the found siblings.
* @param array $options Options to pass to the _next() method
* @return PageArray Returns all matching pages after this one.
*
*/
public function prevAll(Page $page, $selector = '', array $options = array()) {
$defaults = array(
'prev' => true,
'all' => true
);
$options = array_merge($options, $defaults);
return $this->_next($page, $selector, $options);
}
/**
* Return all sibling pages after this one until matching the one specified
*
* @param Page $page
* @param string|Page|array|Selectors $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param array $options Options to pass to the _next() method
* @return PageArray
*
*/
public function nextUntil(Page $page, $selector = '', $filter = '', array $options = array()) {
$defaults = array(
'all' => true,
'until' => $selector
);
$options = array_merge($options, $defaults);
return $this->_next($page, $filter, $options);
}
/**
* Return all sibling pages prior to this one until matching the one specified
*
* @param Page $page
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param array $options Options to pass to the _next() method
* @return PageArray
*
*/
public function prevUntil(Page $page, $selector = '', $filter = '', array $options = array()) {
$defaults = array(
'prev' => true,
'all' => true,
'until' => $selector
);
$options = array_merge($options, $defaults);
return $this->_next($page, $filter, $options);
}
/**
* Returns the URL to the page with $options
*
* You can specify an `$options` argument to this method with any of the following:
*
* - `pageNum` (int|string|bool): Specify pagination number, "+" for next pagination, "-" for previous pagination, or true for current.
* - `urlSegmentStr` (string|bool): Specify a URL segment string to append, or true (3.0.155+) for current.
* - `urlSegments` (array|bool): Specify regular array of URL segments to append (may be used instead of urlSegmentStr).
* Specify boolean true for current URL segments (3.0.155+).
* Specify associative array (in 3.0.155+) to make both keys and values part of the URL segment string.
* - `data` (array): Array of key=value variables to form a query string.
* - `http` (bool): Specify true to make URL include scheme and hostname (default=false).
* - `scheme` (string): Like the http option, makes URL include scheme and hostname, but you specify scheme with this, i.e. 'https' (3.0.178+)
* - `host` (string): Hostname to force use of, i.e. 'world.com' or 'hello.world.com'. The 'http' option is implied when host specified. (3.0.178+)
* - `language` (Language): Specify Language object to return URL in that Language.
*
* You can also specify any of the following for `$options` as shortcuts:
*
* - If you specify an `int` for options it is assumed to be the `pageNum` option.
* - If you specify `+` or `-` for options it is assumed to be the `pageNum` “next/previous pagination” option.
* - If you specify any other `string` for options it is assumed to be the `urlSegmentStr` option.
* - If you specify a `boolean` (true) for options it is assumed to be the `http` option.
*
* Please also note regarding `$options`:
*
* - This method honors template slash settings for page, URL segments and page numbers.
* - Any passed in URL segments are automatically sanitized with `Sanitizer::pageNameUTF8()`.
* - If using the `pageNum` or URL segment options please also make sure these are enabled on the pages template.
* - The query string generated by any `data` variables is entity encoded when output formatting is on.
* - The `language` option requires that the `LanguageSupportPageNames` module is installed.
* - The prefix for page numbers honors `$config->pageNumUrlPrefix` and multi-language prefixes as well.
*
* @param Page $page
* @param array|int|string|bool|Language $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()
*
*/
public function urlOptions(Page $page, $options = array()) {
$config = $page->wire()->config;
$template = $page->template;
$defaults = array(
'http' => is_bool($options) ? $options : false,
'scheme' => '',
'host' => '',
'pageNum' => is_int($options) || (is_string($options) && in_array($options, array('+', '-'))) ? $options : 1,
'data' => array(),
'urlSegmentStr' => is_string($options) ? $options : '',
'urlSegments' => array(),
'language' => is_object($options) && wireInstanceOf($options, 'Language') ? $options : null,
);
if(empty($options)) {
$url = rtrim($config->urls->root, '/') . $page->path();
if($template->slashUrls === 0 && $page->id > 1) $url = rtrim($url, '/');
return $url;
}
$options = is_array($options) ? array_merge($defaults, $options) : $defaults;
$sanitizer = $page->wire()->sanitizer;
$input = $page->wire()->input;
$languages = $page->wire()->languages;
$language = null;
$url = null;
if($options['urlSegments'] === true || $options['urlSegmentStr'] === true) {
$options['urlSegments'] = $input->urlSegments();
}
if($options['pageNum'] === true) {
$options['pageNum'] = $input->pageNum();
}
if(is_array($options['urlSegments']) && count($options['urlSegments'])) {
$str = '';
reset($options['urlSegments']);
if(is_string(key($options['urlSegments']))) {
// associative array converts to key/value style URL segments
foreach($options['urlSegments'] as $key => $value) {
$str .= "$key/$value/";
if(is_int($key)) $str = ''; // abort assoc array option if any int key found
if($str === '') break;
}
}
if(strlen($str)) {
$options['urlSegmentStr'] = rtrim($str, '/');
} else {
$options['urlSegmentStr'] = implode('/', $options['urlSegments']);
}
}
if($options['language'] && $languages && $languages->hasPageNames()) {
if(!is_object($options['language'])) {
$options['language'] = null;
} else if(!$options['language'] instanceof Page) {
$options['language'] = null;
} else if(strpos($options['language']->className(), 'Language') === false) {
$options['language'] = null;
}
if($options['language']) {
/** @var Language $language */
$language = $options['language'];
// localUrl method provided as hook by LanguageSupportPageNames
$url = $page->localUrl($language);
}
}
if(is_null($url)) {
$url = rtrim($config->urls->root, '/') . $page->path();
if($template->slashUrls === 0 && $page->id > 1) $url = rtrim($url, '/');
}
if(is_string($options['urlSegmentStr']) && strlen($options['urlSegmentStr'])) {
$url = rtrim($url, '/') . '/' . $sanitizer->pagePathNameUTF8(trim($options['urlSegmentStr'], '/'));
if($template->slashUrlSegments > -1) $url .= '/';
}
if($options['pageNum']) {
if($options['pageNum'] === '+') {
$options['pageNum'] = $input->pageNum + 1;
} else if($options['pageNum'] === '-' || $options['pageNum'] === -1) {
$options['pageNum'] = $input->pageNum - 1;
}
if((int) $options['pageNum'] > 1) {
$prefix = '';
if($language && $languages && $languages->hasPageNames()) {
$prefix = $languages->pageNames()->get("pageNumUrlPrefix$language");
}
if(!strlen($prefix)) $prefix = $config->pageNumUrlPrefix;
$url = rtrim($url, '/') . '/' . $prefix . ((int) $options['pageNum']);
if($template->slashPageNum) $url .= '/';
}
}
if(count($options['data'])) {
$query = http_build_query($options['data']);
if($page->of()) $query = $sanitizer->entities($query);
$url .= '?' . $query;
}
if($options['scheme']) {
$scheme = strtolower($options['scheme']);
if(strpos($scheme, '://') === false) $scheme .= '://';
if($scheme === 'https://' && $config->noHTTPS) $scheme = 'http' . '://';
$host = $options['host'] ? $options['host'] : $config->httpHost;
$url = "$scheme$host$url";
} else if($options['http'] || $options['host']) {
$mode = $config->noHTTPS ? -1 : $template->https;
switch($mode) {
case -1: $scheme = 'http'; break;
case 1: $scheme = 'https'; break;
default: $scheme = $config->https ? 'https' : 'http';
}
$host = $options['host'] ? $options['host'] : $config->httpHost;
$url = "$scheme://$host$url";
}
return $url;
}
/**
* Return all URLs that this page can be accessed from (excluding URL segments and pagination)
*
* This includes the current page URL, any other language URLs (for which page is active), and
* any past (historical) URLs the page was previously available at (which will redirect to it).
*
* - Returned URLs do not include additional URL segments or pagination numbers.
* - Returned URLs are indexed by language name, i.e. “default”, “fr”, “es”, etc.
* - If multi-language URLs not installed, then index is just “default”.
* - Past URLs are indexed by language; then ISO-8601 date, i.e. “default;2016-08-11T07:44:43-04:00”,
* where the date represents the last date that URL was considered current.
* - 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.
*
* @param Page $page
* @param array $options Options to modify default behavior:
* - `http` (bool): Make URLs include current scheme and hostname (default=false).
* - `past` (bool): Include past/historical URLs? (default=true)
* - `languages` (bool): Include other language URLs when supported/available? (default=true).
* - `language` (Language|int|string): Include only URLs for this language (default=null).
* Note: the `languages` option must be true if using the `language` option.
* @return array
*
*/
public function urls(Page $page, $options = array()) {
$defaults = array(
'http' => false,
'past' => true,
'languages' => true,
'language' => null,
);
$modules = $page->wire()->modules;
$options = array_merge($defaults, $options);
$languages = $options['languages'] ? $page->wire()->languages : null;
$slashUrls = $page->template->slashUrls;
$httpHostUrl = $options['http'] ? $page->wire()->input->httpHostUrl() : '';
$urls = array();
if($options['language'] && $languages) {
if(!$options['language'] instanceof Page) {
$options['language'] = $languages->get($options['language']);
}
if($options['language'] && $options['language']->id) {
$languages = array($options['language']);
}
}
// include other language URLs
if($languages && $languages->hasPageNames()) {
foreach($languages as $language) {
/** @var Language $language */
if(!$language->isDefault() && !$page->get("status$language")) continue;
$urls[$language->name] = $page->localUrl($language);
}
} else {
$urls = array('default' => $page->url());
}
// add in historical URLs
if($options['past'] && $modules->isInstalled('PagePathHistory')) {
/** @var PagePathHistory $history */
$history = $modules->get('PagePathHistory');
$rootUrl = $page->wire()->config->urls->root;
$pastPaths = $history->getPathHistory($page, array(
'language' => $options['language'],
'verbose' => true
));
foreach($pastPaths as $pathInfo) {
$key = '';
if(!empty($pathInfo['language'])) {
/** @var Language $language */
$language = $pathInfo['language'];
if($options['languages']) {
$key .= $language->name . ';';
} else {
// they asked to have multi-language excluded
if(!$language->isDefault()) continue;
}
}
$key .= wireDate('c', $pathInfo['date']);
$urls[$key] = $rootUrl . ltrim($pathInfo['path'], '/');
}
}
// update URLs for current expected slash and http settings
foreach($urls as $key => $url) {
if($url !== '/') $url = $slashUrls ? rtrim($url, '/') . '/' : rtrim($url, '/');
if($options['http']) $url = $httpHostUrl . $url;
$urls[$key] = $url;
}
return $urls;
}
/**
* Return the URL necessary to edit page
*
* - We recommend checking that the page is editable before outputting the editUrl().
* - If user opens URL in their browser and is not logged in, they must login to account with edit permission.
* - This method can also be accessed by property at `$page->editUrl` (without parenthesis).
*
* ~~~~~~
* if($page->editable()) {
* echo "<a href='$page->editUrl'>Edit this page</a>";
* }
* ~~~~~~
*
* @param Page $page
* @param array|bool|string $options Specify true for http option, specify name of field to find (3.0.151+), or use $options array:
* - `http` (bool): True to force scheme and hostname in URL (default=auto detect).
* - `language` (Language|bool): Optionally specify Language to start editor in, or boolean true to force current user language.
* - `find` (string): Name of field to find in the editor (3.0.151+)
* @return string URL for editing this page
*
*/
public function editUrl(Page $page, $options = array()) {
$config = $page->wire()->config;
$adminTemplate = $page->wire()->templates->get('admin'); /** @var Template $adminTemplate */
$https = $adminTemplate && ($adminTemplate->https > 0) && !$config->noHTTPS;
$url = ($https && !$config->https) ? 'https://' . $config->httpHost : '';
$url .= $config->urls->admin . "page/edit/?id=$page->id";
if($options === true || (is_array($options) && !empty($options['http']))) {
if(strpos($url, '://') === false) {
$url = ($https || $config->https ? 'https' : 'http' ) . '://' . $config->httpHost . $url;
}
}
$languages = $page->wire()->languages;
if($languages) {
$language = $page->wire()->user->language;
if(empty($options['language'])) {
if($page->wire()->page->template->id == $adminTemplate->id) $language = null;
} else if($options['language'] instanceof Page) {
$language = $options['language'];
} else if($options['language'] !== true) {
$language = $languages->get($options['language']);
}
if($language && $language->id) $url .= "&language=$language->id";
}
$append = $page->wire()->session->getFor($page, 'appendEditUrl');
if($append) $url .= $append;
if($options) {
if(is_string($options)) {
$find = $options;
} else if(is_array($options) && !empty($options['find'])) {
$find = $options['find'];
} else $find = '';
if($find && strpos($url, '#') === false) {
$url .= '#find-' . $page->wire()->sanitizer->fieldName($find);
}
}
return $url;
}
/**
* Returns the URL to the page, including scheme and hostname
*
* - This method is just like the `$page->url()` method except that it also includes scheme and hostname.
*
* - This method can also be accessed at the property `$page->httpUrl` (without parenthesis).
*
* - It is desirable to use this method when some page templates require https while others don't.
* This ensures local links will always point to pages with the proper scheme. For other cases, it may
* be preferable to use `$page->url()` since it produces shorter output.
*
* ~~~~~
* // Generating a link to this page using httpUrl
* echo "<a href='$page->httpUrl'>$page->title</a>";
* ~~~~~
*
* @param Page $page
* @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()
*
*/
public function httpUrl(Page $page, $options = array()) {
$template = $page->template();
if(!$template) return '';
if(is_array($options)) unset($options['http']);
if($options === true || $options === false) $options = array();
$url = $page->url($options);
if(strpos($url, '://')) return $url;
$config = $page->wire()->config;
$mode = $template->https;
if($mode > 0 && $config->noHTTPS) $mode = 0;
switch($mode) {
case -1: $scheme = 'http'; break;
case 1: $scheme = 'https'; break;
default: $scheme = $config->https ? 'https' : 'http';
}
$url = "$scheme://$config->httpHost$url";
return $url;
}
/**
* Return pages that are referencing the given one by way of Page references
*
* @param Page $page
* @param string|bool $selector Optional selector to filter results by or boolean true as shortcut for `include=all`.
* @param Field|string $field Limit to follower pages using this field,
* - or specify boolean TRUE to make it return array of PageArrays indexed by field name.
* @param bool $getCount Specify true to return counts rather than PageArray(s)
* @return PageArray|array|int
* @throws WireException Highly unlikely
*
*/
public function references(Page $page, $selector = '', $field = '', $getCount = false) {
/** @var FieldtypePage $fieldtype */
$fieldtype = $page->wire()->fieldtypes->get('FieldtypePage');
if(!$fieldtype) throw new WireException('Unable to find FieldtypePage');
if($selector === true) $selector = "include=all";
return $fieldtype->findReferences($page, $selector, $field, $getCount);
}
/**
* Return number of VISIBLE pages that are following (referencing) the given one by way of Page references
*
* Note that this excludes hidden, unpublished and otherwise non-accessible pages (access control).
* If you do not want to exclude these, use the numFollowers() function instead, OR specify "include=all" for
* the $selector argument.
*
* @param Page $page
* @param string $selector Filter count by this selector
* @param string|Field|bool $field Limit count to given Field or specify boolean true to return array of counts.
* @return int|array Returns count, or array of counts (if $field==true)
*
*/
public function hasReferences(Page $page, $selector = '', $field = '') {
return $this->references($page, $selector, $field, true);
}
/**
* Return number of ANY pages that are following (referencing) the given one by way of Page references
*
* @param Page $page
* @param string $selector Filter count by this selector
* @param string|Field|bool $field Limit count to given Field or specify boolean true to return array of counts.
* @return int|array Returns count, or array of counts (if $field==true)
*
*/
public function numReferences(Page $page, $selector = '', $field = '') {
if(stripos($selector, "include=") === false) $selector = rtrim("include=all, $selector", ', ');
return $this->hasReferences($page, $selector, $field);
}
/**
* Return pages that this page is referencing by way of Page reference fields
*
* @param Page $page
* @param bool|Field|string|int $field Limit results to requested field, or specify boolean true to return array indexed by field names.
* @param bool $getCount Specify true to return count(s) rather than pages.
* @return PageArray|int|array
*
*/
public function referencing(Page $page, $field = false, $getCount = false) {
$fieldName = '';
$byField = null;
if(is_bool($field) || is_null($field)) {
$byField = $field ? true : false;
} else if(is_string($field)) {
$fieldName = $page->wire()->sanitizer->fieldName($field);
} else if(is_int($field)) {
$field = $page->wire()->fields->get($field);
if($field) $fieldName = $field->name;
} else if($field instanceof Field) {
$fieldName = $field->name;
}
// results
$fieldCounts = array(); // counts indexed by field name (if count mode)
$pages = $page->wire()->pages;
$items = $pages->newPageArray();
$itemsByField = array();
foreach($page->template->fieldgroup as $f) {
/** @var Field $f */
if($fieldName && $field->name != $fieldName) continue;
if(!$f->type instanceof FieldtypePage) continue;
if($byField) $itemsByField[$f->name] = $pages->newPageArray();
$value = $page->get($f->name);
if($value instanceof Page && $value->id) {
$items->add($value);
if($byField) $itemsByField[$f->name]->add($value);
$fieldCounts[$f->name] = 1;
} else if($value instanceof PageArray && $value->count()) {
$items->import($value);
if($byField) $itemsByField[$f->name]->import($value);
$fieldCounts[$f->name] = $value->count();
} else {
unset($itemsByField[$f->name]);
}
}
if($getCount) return $byField ? $fieldCounts : $items->count();
if($byField) return $itemsByField;
return $items;
}
/**
* Return number of pages this one is following (referencing) by way of Page references
*
* @param Page $page
* @param bool $field Optionally limit to field, or specify boolean true to return array of counts per field.
* @return int|array
*
*/
public function numReferencing(Page $page, $field = false) {
return $this->referencing($page, $field, true);
}
/**
* Find other pages linking to the given one by way contextual links is textarea/html fields
*
* @param Page $page
* @param string $selector
* @param bool|string|Field $field
* @param array $options
* - `getIDs` (bool): Return array of page IDs rather than Page instances. (default=false)
* - `getCount` (bool): Return a total count (int) of found pages rather than Page instances. (default=false)
* - `confirm` (bool): Confirm that the links are present by looking at the actual page field data. (default=true)
* You can specify false for this option to make it perform faster, but with a potentially less accurate result.
* @return PageArray|array|int
* @throws WireException
*
*/
public function links(Page $page, $selector = '', $field = false, array $options = array()) {
/** @var FieldtypeTextarea $fieldtype */
$fieldtype = $page->wire()->fieldtypes->get('FieldtypeTextarea');
if(!$fieldtype) throw new WireException('Unable to find FieldtypeTextarea');
return $fieldtype->findLinks($page, $selector, $field, $options);
}
/**
* Return total found number of pages linking to this one with no exclusions
*
* @param Page $page
* @param bool $field
* @return int
*
*/
public function numLinks(Page $page, $field = false) {
return $this->links($page, true, $field, array('getCount' => true));
}
/**
* Return total number of pages visible to current user linking to this one
*
* @param Page $page
* @param bool $field
* @return array|int|PageArray
*
*/
public function hasLinks(Page $page, $field = false) {
return $this->links($page, '', $field, array('getCount' => true));
}
/******************************************************************************************************************
* LEGACY METHODS
*
* Following are legacy methods to support backwards compatibility with previous PW versions that used
* a $siblings argument for next/prev related methods.
*
*/
/**
* Return the next sibling page, within a group of provided siblings (that includes the current page)
*
* This method is the old version of the next() method and is only used if a $siblings argument is provided
* to the Page::next() call. It is much slower than the next() method.
*
* If given a PageArray of siblings (containing the current) it will return the next sibling relative to the provided PageArray.
*
* Be careful with this function when the page has a lot of siblings. It has to load them all, so this function is best
* avoided at large scale, unless you provide your own already-reduced siblings list (like from pagination)
*
* When using a selector, note that this method operates only on visible children. If you want something like "include=all"
* or "include=hidden", they will not work in the selector. Instead, you should provide the siblings already retrieved with
* one of those modifiers, and provide those siblings as the second argument to this function.
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will find nearest next sibling that matches.
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @return Page|NullPage Returns the next sibling page, or a NullPage if none found.
*
*/
public function nextSibling(Page $page, $selector = '', PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
$selector = '';
}
if(is_null($siblings)) {
$siblings = $page->parent->children();
} else if(!$siblings->has($page)) {
$siblings->prepend($page);
}
$next = $page;
do {
/** @var Page $next */
$next = $siblings->getNext($next, false);
if(empty($selector) || !$next || $next->matches($selector)) break;
} while($next->id);
if(is_null($next)) $next = $page->wire()->pages->newNullPage();
return $next;
}
/**
* Return the previous sibling page within a provided group of siblings that contains the current page
*
* This method is the old version of the prev() method and is only used if a $siblings argument is provided
* to the Page::prev() call. It is much slower than the prev() method.
*
* If given a PageArray of siblings (containing the current) it will return the previous sibling relative to the provided PageArray.
*
* Be careful with this function when the page has a lot of siblings. It has to load them all, so this function is best
* avoided at large scale, unless you provide your own already-reduced siblings list (like from pagination)
*
* When using a selector, note that this method operates only on visible children. If you want something like "include=all"
* or "include=hidden", they will not work in the selector. Instead, you should provide the siblings already retrieved with
* one of those modifiers, and provide those siblings as the second argument to this function.
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will find nearest previous sibling that matches.
* @param PageArray $siblings Optional siblings to use instead of the default. May also be specified as first argument when no selector needed.
* @return Page|NullPage Returns the previous sibling page, or a NullPage if none found.
*
*/
public function prevSibling(Page $page, $selector = '', PageArray $siblings = null) {
if($selector instanceof PageArray) {
// backwards compatible to when $siblings was first argument
$siblings = $selector;
$selector = '';
}
if(is_null($siblings)) {
$siblings = $page->parent->children();
} else if(!$siblings->has($page)) {
$siblings->add($page);
}
$prev = $page;
do {
/** @var Page $prev */
$prev = $siblings->getPrev($prev, false);
if(empty($selector) || !$prev || $prev->matches($selector)) break;
} while($prev->id);
if(is_null($prev)) $prev = $page->wire()->pages->newNullPage();
return $prev;
}
/**
* Return all sibling pages after this one, optionally matching a selector
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @return PageArray Returns all matching pages after this one.
*
*/
public function nextAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
} else if(!$siblings->has($page)) {
$siblings->prepend($page);
}
$id = $page->id;
$all = $page->wire()->pages->newPageArray();
$rec = false;
foreach($siblings as $sibling) {
if($sibling->id == $id) {
$rec = true;
continue;
}
if($rec) $all->add($sibling);
}
if(!empty($selector)) $all->filter($selector);
return $all;
}
/**
* Return all sibling pages before this one, optionally matching a selector
*
* @param Page $page
* @param string|array $selector Optional selector. When specified, will filter the found siblings.
* @param PageArray $siblings Optional siblings to use instead of the default.
* @return PageArray
*
*/
public function prevAllSiblings(Page $page, $selector = '', PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
} else if(!$siblings->has($page)) {
$siblings->add($page);
}
$id = $page->id;
$all = $page->wire()->pages->newPageArray();
foreach($siblings as $sibling) {
if($sibling->id == $id) break;
$all->add($sibling);
}
if(!empty($selector)) $all->filter($selector);
return $all;
}
/**
* Return all sibling pages after this one until matching the one specified
*
* @param Page $page
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector to filter matched pages by
* @param PageArray|null $siblings Optional PageArray of siblings to use instead of all from the page.
* @return PageArray
*
*/
public function nextUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
} else if(!$siblings->has($page)) {
$siblings->prepend($page);
}
$siblings = $this->nextAllSiblings($page, '', $siblings);
$all = $page->wire()->pages->newPageArray();
$stop = false;
foreach($siblings as $sibling) {
/** @var Page $sibling */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $sibling->id == $selector) {
$stop = true;
} else if($sibling->matches($selector)) {
$stop = true;
}
} else if(is_array($selector) && count($selector)) {
if($sibling->matches($selector)) $stop = true;
} else if(is_int($selector)) {
if($sibling->id == $selector) $stop = true;
} else if($selector instanceof Page && $sibling->id == $selector->id) {
$stop = true;
}
if($stop) break;
$all->add($sibling);
}
if(!empty($filter)) $all->filter($filter);
return $all;
}
/**
* Return all sibling pages before this one until matching the one specified
*
* @param Page $page
* @param string|Page|array $selector May either be a selector or Page to stop at. Results will not include this.
* @param string|array $filter Optional selector string to filter matched pages by
* @param PageArray|null $siblings Optional PageArray of siblings to use instead of all from the page.
* @return PageArray
*
*/
public function prevUntilSiblings(Page $page, $selector = '', $filter = '', PageArray $siblings = null) {
if(is_null($siblings)) {
$siblings = $page->parent()->children();
} else if(!$siblings->has($page)) {
$siblings->add($page);
}
$siblings = $this->prevAllSiblings($page, '', $siblings);
$all = $page->wire()->pages->newPageArray();
$stop = false;
foreach($siblings->reverse() as $sibling) {
/** @var Page $sibling */
if(is_string($selector) && strlen($selector)) {
if(ctype_digit("$selector") && $sibling->id == $selector) {
$stop = true;
} else if($sibling->matches($selector)) {
$stop = true;
}
} else if(is_array($selector) && count($selector)) {
if($sibling->matches($selector)) $stop = true;
} else if(is_int($selector)) {
if($sibling->id == $selector) $stop = true;
} else if($selector instanceof Page && $sibling->id == $selector->id) {
$stop = true;
}
if($stop) break;
$all->prepend($sibling);
}
if(!empty($filter)) $all->filter($filter);
return $all;
}
/**
* Return the next or previous sibling page (new fast version)
*
* @param Page $page
* @param bool $getNext Specify true to return next page, or false to return previous.
* @param string|array $selector Optional selector. When specified, will find nearest sibling that matches.
* @param array $options Options to modify behavior
* - `all` (bool): If true, returns all nextAll or prevAll rather than just single sibling (default=false).
* - `until` (string): If specified, returns all siblings until another is found matching the given selector.
* @return Page|NullPage|PageArray Returns the next/prev sibling page, or a NullPage if none found.
* Returns PageArray if 'all' or 'until' option is specified.
*
*/
/*
* KEEPING THIS AROUND AS ALTERNATIVE METHOD FOR SHORT TERM REFERENCE
* This method performs worse than _next() in most cases, but if there are millions of siblings,
* this method is likely to perform significantly faster. So we may add this back into the logic
* if need dictates. However, it can't accommodate all possible sorting scenarios.
*
protected function _nextAlternate(Page $page, $selector = '', array $options = array()) {
$defaults = array(
'prev' => false,
'all' => false,
'until' => '', // selector string
);
$options = array_merge($defaults, $options);
$getNext = !$options['prev'];
if($options['until']) {
if(is_array($options['until'])) {
$selectors = new Selectors($options['until']);
$options['until'] = (string) $selectors;
}
$options['all'] = true; // the 'all' option is assumed with 'until'
}
if(is_array($selector)) {
$selectors = new Selectors($selector);
$selector = (string) $selectors;
}
$pages = $page->wire('pages');
$parent = $page->parent();
$sanitizer = $page->wire('sanitizer');
if(!$parent || !$parent->id) {
// homepage or NullPage, quick exit
return $options['all'] ? $pages->newPageArray() : $pages->newNullPage();
}
$sortfield = $parent->sortfield();
$descending = strpos($sortfield, '-') === 0;
if($descending) $sortfield = ltrim($sortfield, '-');
if($getNext === false) $descending = !$descending;
$operator = $descending ? "<" : ">";
$value = $sanitizer->selectorValue($page->getUnformatted($sortfield));
$sortfield2 = $sortfield == 'sort' ? 'sort.value' : $sortfield;
$countSelector = rtrim("parent_id=$parent->id, $sortfield2=$value, $selector", ", ");
$sortSelector = $descending ? "sort=-$sortfield" : "sort=$sortfield";
$uniqueSorts = array('sort', 'id', 'name'); // sorts where same value never appears twice among siblings
$useSlower = false;
$isUniqueSort = in_array($sortfield, $uniqueSorts);
$next = false;
$nextAll = $options['all'] ? $pages->newPageArray() : false;
if(!$isUniqueSort) {
$field = $page->wire('fields')->get($sortfield);
if($field->type instanceof FieldtypePage) {
$sortfield2 .= ".name";
$sortSelector .= ".name";
}
} else {
$field = null;
}
// count how many other children have this same exact sort value
if(!$isUniqueSort && $pages->count($countSelector) > 1) {
// multiple siblings have the same sort value
// we will have to load them all to determine where $page fits in there
$siblings = $parent->children(rtrim("$sortfield2=$value, $selector", ", "));
if(!$getNext) $siblings = $siblings->reverse();
foreach($siblings as $sibling) {
if($next === true) {
$next = $sibling;
if($nextAll) {
$nextAll->add($next);
} else {
break;
}
} else if($sibling->id == $page->id) {
$next = true;
}
}
if(!$nextAll && $next && $next instanceof Page) {
return $next;
}
}
// page id exclusion will be used, so operator can include pages having sort value
if($nextAll && $nextAll->count() > 1) $operator .= '=';
// selector that that only matches pages having a higher/lower sortfield value than $page
$selector = rtrim("parent_id=$parent->id, id!=$page->id, $sortfield2$operator$value, $sortSelector, $selector", ", ");
if($options['until']) {
// multiple next/prev sibling pages until a particular one
$selector = $nextAll->each('id!={id}, ') . $selector;
// include matches only up until page matching 'until' selector
$until = $pages->find("$selector, $options[until], limit=1");
// setup for fast exclusion method
if($until->count()) {
$items = $pages->find($selector, array('untilID' => $until->first()->id));
} else {
$items = $pages->find($selector);
// use slower exclusion method when necessary, excluding pages after loaded
$exclude = false;
foreach($items as $item) {
if($exclude) {
$items->remove($item);
} else if($item->matches($options['until'])) {
$exclude = true;
$items->remove($item);
}
}
}
return $items;
} else if($nextAll) {
// multiple next/prev sibling pages
$selector = $nextAll->each('id!={id}, ') . $selector;
return $pages->find($selector);
} else {
// single next/prev sibling page
return $pages->findOne($selector);
}
}
*/
}