diff --git a/wire/core/Page.php b/wire/core/Page.php index c59e2102..ea0a34be 100644 --- a/wire/core/Page.php +++ b/wire/core/Page.php @@ -8,7 +8,7 @@ * 1. Providing get/set access to the Page's properties * 2. Accessing the related hierarchy of pages (i.e. parents, children, sibling pages) * - * ProcessWire 3.x, Copyright 2017 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * * #pw-summary Class used by all Page objects in ProcessWire. @@ -29,9 +29,10 @@ * @property int $id The numbered ID of the current page #pw-group-system * @property string $name The name assigned to the page, as it appears in the URL #pw-group-system #pw-group-common * @property string $namePrevious Previous name, if changed. Blank if not. #pw-advanced - * @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 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 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 @@ -74,6 +75,11 @@ * @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 + * @property int $numReferences Total number of pages referencing this page with Page reference fields. #pw-group-traversal + * @property int $hasReferences Number of visible pages (to current user) referencing this page with page reference fields. #pw-group-traversal + * @property int $numReferencing Total number of other pages this page is pointing to (referencing) with Page fields. #pw-group-traversal + * @property int $numLinks Total number of pages manually linking to this page in Textarea/HTML fields. #pw-group-traversal + * @property int $hasLinks Number of visible pages (to current user) linking to this page in Textarea/HTML fields. #pw-group-traversal * * @property Page|null $_cloning Internal runtime use, contains Page being cloned (source), when this Page is the new copy (target). #pw-internal * @property bool|null $_hasAutogenName Internal runtime use, set by Pages class when page as auto-generated name. #pw-internal @@ -92,6 +98,7 @@ * @method bool deleteable() Returns true if the page is deleteable by the current user, false if not. #pw-group-access * @method bool deletable() Alias of deleteable(). #pw-group-access * @method bool trashable($orDeleteable = false) Returns true if the page is trashable by the current user, false if not. #pw-group-access + * @method bool restorable() Returns true if page is in the trash and is capable of being restored to its original location. #pw-group-access * @method bool addable($pageToAdd = null) Returns true if the current user can add children to the page, false if not. Optionally specify the page to be added for additional access checking. #pw-group-access * @method bool moveable($newParent = null) Returns true if the current user can move this page. Optionally specify the new parent to check if the page is moveable to that parent. #pw-group-access * @method bool sortable() Returns true if the current user can change the sort order of the current page (within the same parent). #pw-group-access @@ -132,6 +139,8 @@ * @method string getMarkup($key) Return the markup value for a given field name or {tag} string. #pw-internal * @method string|mixed renderField($fieldName, $file = '') Returns rendered field markup, optionally with file relative to templates/fields/. #pw-internal * @method string|mixed renderValue($value, $file) Returns rendered markup for $value using $file relative to templates/fields/. #pw-internal + * @method PageArray references($selector = '', $field = '') Return pages that are pointing to this one by way of Page reference fields. #pw-group-traversal + * @method PageArray links($selector = '', $field = '') Return pages that link to this one contextually in Textarea/HTML fields. #pw-group-traversal * */ @@ -178,6 +187,12 @@ class Page extends WireData implements \Countable, WireMatchable { */ const statusSystem = 16; + /** + * Page has a globally unique name and no other pages may have the same name + * + */ + const statusUnique = 32; + /** * Page has pending draft changes (name: "draft"). * #pw-internal @@ -535,6 +550,7 @@ class Page extends WireData implements \Countable, WireMatchable { * - "p": Property name maps to same property name in $this * - "m": Property name maps to same method name in $this * - "n": Property name maps to same method name in $this, but may be overridden by custom field + * - "t": Property name maps to PageTraversal method with same name, if not overridden by custom field * - [blank]: needs additional logic to be handled ([blank]='') * * @var array @@ -555,8 +571,10 @@ class Page extends WireData implements \Countable, WireMatchable { 'editUrl' => 'm', 'fieldgroup' => '', 'filesManager' => 'm', - 'hasParent' => 'parents', 'hasChildren' => 'm', + 'hasLinks' => 't', + 'hasParent' => 'parents', + 'hasReferences' => 't', 'httpUrl' => 'm', 'id' => 's', 'index' => 'n', @@ -568,6 +586,7 @@ class Page extends WireData implements \Countable, WireMatchable { 'isPublic' => 'm', 'isTrash' => 'm', 'isUnpublished' => 'm', + 'links' => 'n', 'listable' => 'm', 'modified' => 's', 'modifiedStr' => '', @@ -578,6 +597,8 @@ class Page extends WireData implements \Countable, WireMatchable { 'namePrevious' => 'p', 'next' => 'm', 'numChildren' => 's', + 'numLinks' => 't', + 'numReferences' => 't', 'output' => 'm', 'outputFormatting' => 'p', 'parent' => 'm', @@ -589,6 +610,8 @@ class Page extends WireData implements \Countable, WireMatchable { 'publishable' => 'm', 'published' => 's', 'publishedStr' => '', + 'references' => 'n', + 'referencing' => 't', 'render' => '', 'rootParent' => 'm', 'siblings' => 'm', @@ -603,6 +626,7 @@ class Page extends WireData implements \Countable, WireMatchable { 'templatePrevious' => 'p', 'trashable' => 'm', 'url' => 'm', + 'urls' => 'm', 'viewable' => 'm' ); @@ -1006,18 +1030,21 @@ class Page extends WireData implements \Countable, WireMatchable { if(isset(self::$basePropertiesAlternates[$key])) $key = self::$basePropertiesAlternates[$key]; if(isset(self::$baseProperties[$key])) { $type = self::$baseProperties[$key]; - if($type == 'p') { + if($type === 'p') { // local property return $this->$key; - } else if($type == 'm') { + } else if($type === 'm') { // local method return $this->{$key}(); - } else if($type == 'n') { + } else if($type === 'n') { // local method, possibly overridden by $field if(!$this->wire('fields')->get($key)) return $this->{$key}(); - } else if($type == 's') { + } else if($type === 's') { // settings property return $this->settings[$key]; + } else if($type === 't') { + // map to method in PageTraversal, if not overridden by field + if(!$this->wire('fields')->get($key)) return $this->traversal()->{$key}($this); } else if($type) { // defined local method return $this->{$type}(); @@ -2346,6 +2373,42 @@ class Page extends WireData implements \Countable, WireMatchable { if($siblings) return $this->traversal()->prevUntilSiblings($this, $selector, $filter, $siblings); return $this->traversal()->prevUntil($this, $selector, $filter); } + + /** + * Return pages that have Page reference fields pointing to this one (references) + * + * By default this excludes pages that are hidden, unpublished and pages excluded due to access control for the current user. + * To prevent these exclusions specify an include mode in the selector, i.e. `include=all`, or you can use + * boolean `true` as a shortcut to specify that you do not want any exclusions. + * + * #pw-group-traversal + * + * @param string|bool $selector Optional selector to filter results by, or boolean true as shortcut for `include=all`. + * @param Field|string|bool $field Optionally limit to pages using specified field (name or Field object), + * - OR specify boolean TRUE to return array of PageArrays indexed by field names. + * - If $field argument not specified, it searches all applicable Page fields. + * @return PageArray|array + * + */ + public function ___references($selector = '', $field = '') { + return $this->traversal()->references($this, $selector, $field); + } + + /** + * Return pages linking to this one (in Textarea/HTML fields) + * + * Applies only to Textarea fields with “html” content-type and link abstraction enabled. + * + * #pw-group-traversal + * + * @param string|bool $selector Optional selector to filter by or boolean true for “include=all”. (default='') + * @param string|Field $field Optionally limit results to specified field. (default=all applicable Textarea fields) + * @return PageArray + * + */ + public function ___links($selector = '', $field = '') { + return $this->traversal()->links($this, $selector, $field); + } /** * Get languages active for this page and viewable by current user @@ -2812,6 +2875,35 @@ class Page extends WireData implements \Countable, WireMatchable { 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. + * + * #pw-advanced + * + * @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($options = array()) { + return $this->traversal()->urls($this, $options); + } + /** * Returns the URL to the page, including scheme and hostname * @@ -2886,6 +2978,10 @@ class Page extends WireData implements \Countable, WireMatchable { $url = ($https ? 'https://' : 'http://') . $config->httpHost . $url; } } + if($this->wire('languages') && $this->wire('page')->template->id != $adminTemplate->id) { + $language = $this->wire('user')->language; + if($language) $url .= "&language=$language->id"; + } $append = $this->wire('session')->getFor($this, 'appendEditUrl'); if($append) $url .= $append; return $url; diff --git a/wire/core/PageTraversal.php b/wire/core/PageTraversal.php index 535321be..905d059a 100644 --- a/wire/core/PageTraversal.php +++ b/wire/core/PageTraversal.php @@ -576,7 +576,258 @@ class PageTraversal { 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, + ); + + /** @var Modules $modules */ + $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() : ''; + + 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 && $modules->isInstalled('LanguageSupportPageNames')) { + foreach($languages as $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')) { + $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'])) { + if($options['languages']) { + $key .= $pathInfo['language']->name . ';'; + } else { + // they asked to have multi-language excluded + if(!$pathInfo['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 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) { + $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 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 = ''; + 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) + $items = $page->wire('pages')->newPageArray(); + $itemsByField = array(); + + foreach($page->template->fieldgroup as $f) { + if($fieldName && $field->name != $fieldName) continue; + if(!$f->type instanceof FieldtypePage) continue; + if($byField) $itemsByField[$f->name] = $this->wire('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