From 94524a8776250b7035a4dcbb85490736cbbc568d Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 28 Jun 2018 12:47:05 -0400 Subject: [PATCH] Add new find methods to FieldtypePage, FieldtypeTextarea and MarkupQA for finding page references and links specific to a given page. The public interfaces to these will be in the Page class (coming in next commit). --- wire/core/MarkupQA.php | 109 +++++++++++++++++- wire/modules/Fieldtype/FieldtypePage.module | 85 +++++++++----- .../Fieldtype/FieldtypeTextarea.module | 42 ++++++- 3 files changed, 205 insertions(+), 31 deletions(-) diff --git a/wire/core/MarkupQA.php b/wire/core/MarkupQA.php index e5572dc7..d6ea3d32 100644 --- a/wire/core/MarkupQA.php +++ b/wire/core/MarkupQA.php @@ -414,7 +414,8 @@ class MarkupQA extends Wire { $pageID = $pwid; $languageID = 0; } - + + $pageID = (int) $pageID; $full = $matches[0][$key]; $start = $matches[1][$key]; $href = $matches[3][$key]; @@ -427,7 +428,7 @@ class MarkupQA extends Wire { $language = null; } - $livePath = $this->wire('pages')->getPath((int) $pageID, array( + $livePath = $this->wire('pages')->getPath($pageID, array( 'language' => $language )); @@ -469,6 +470,108 @@ class MarkupQA extends Wire { } } + /** + * Find pages linking to another + * + * @param Page $page Page to find links to, or omit to use page specified in constructor + * @param array $fieldNames Field names to look in or omit to use field specified in constructor + * @param string $selector Optional selector to use as a filter + * @param array $options Additional 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 + * + */ + public function findLinks(Page $page = null, $fieldNames = array(), $selector = '', array $options = array()) { + + $defaults = array( + 'getIDs' => false, + 'getCount' => false, + 'confirm' => true + ); + + $options = array_merge($defaults, $options); + + if($options['getIDs']) { + $result = array(); + } else if($options['getCount']) { + $result = 0; + } else { + $result = $this->wire('pages')->newPageArray(); + } + + if(!$page) $page = $this->page; + if(!$page) return $result; + + if(empty($fieldNames)) { + if($this->field) $fieldNames[] = $this->field->name; + if(empty($fieldNames)) return $result; + } + + if($selector === true) $selector = "include=all"; + $op = strlen("$page->id") > 3 ? "~=" : "%="; + $selector = implode('|', $fieldNames) . "$op'$page->id', id!=$page->id, $selector"; + $selector = trim($selector, ', '); + + + // find pages + if($options['getCount'] && !$options['confirm']) { + // just return a count + return $this->wire('pages')->count($selector); + } else { + // find the IDs + $checkIDs = array(); + $foundIDs = $this->wire('pages')->findIDs($selector); + if(!count($foundIDs)) return $result; + if($options['confirm']) { + $checkIDs = array_flip($foundIDs); + $foundIDs = array(); + } + } + + // confirm results + foreach($fieldNames as $fieldName) { + if(!count($checkIDs)) break; + $field = $this->wire('fields')->get($fieldName); + if(!$field) continue; + $table = $field->getTable(); + $ids = implode(',', array_keys($checkIDs)); + $sql = "SELECT * FROM `$table` WHERE `pages_id` IN($ids)"; + $query = $this->wire('database')->prepare($sql); + $query->execute(); + + while($row = $query->fetch(\PDO::FETCH_ASSOC)) { + $pageID = (int) $row['pages_id']; + if(isset($foundIDs[$pageID])) continue; + $row = implode(' ', $row); + $find = "data-pwid=$page->id"; + // first check if it might be there + if(!strpos($row, $find)) continue; + // then confirm with a more accurate check + if(!strpos($row, "$find ") && !strpos($row, "$find\t") && !strpos($row, "$find-")) continue; + // at this point we have confirmed that this item links to $page + unset($checkIDs[$pageID]); + $foundIDs[$pageID] = $pageID; + } + + $query->closeCursor(); + } + + if(count($foundIDs)) { + if($options['getIDs']) { + $result = $foundIDs; + } else if($options['getCount']) { + $result = count($foundIDs); + } else { + $result = $this->wire('pages')->getById($foundIDs); + } + } + + return $result; + } + /** * Display and log a warning about a path that didn't resolve * @@ -554,7 +657,7 @@ class MarkupQA extends Wire { list($name, $val) = explode('=', $attr); $name = strtolower($name); - $val = trim($val, "\"' "); + $val = trim($val, "\"'> "); if($name == 'alt' && !strlen($val)) { $replaceAlt = $attr; diff --git a/wire/modules/Fieldtype/FieldtypePage.module b/wire/modules/Fieldtype/FieldtypePage.module index ae276b67..5a2bef4d 100644 --- a/wire/modules/Fieldtype/FieldtypePage.module +++ b/wire/modules/Fieldtype/FieldtypePage.module @@ -9,7 +9,7 @@ * /wire/core/Fieldtype.php * /wire/core/FieldtypeMulti.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * */ @@ -19,7 +19,7 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule public static function getModuleInfo() { return array( 'title' => 'Page Reference', - 'version' => 104, + 'version' => 105, 'summary' => 'Field that stores one or more references to ProcessWire pages', 'permanent' => true, ); @@ -1367,8 +1367,6 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule /** * Return pages referencing the given $page, optionally indexed by field name * - * Not currently applicable, for future use. - * * The default behavior when no arguments are provided (except $page) is to simply return a PageArray of all pages * referencing the given one (excluding hidden, unpublished, trash, no-access, etc.). Specify "include=all" * as your $selector if you want to include all pages without filtering. If the quantity may be large, @@ -1377,26 +1375,37 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule * * @param Page $page Page to get references for * @param string|bool $selector Optionally filter/modify returned result, i.e. "limit=10, include=all", etc. - * Or specify boolean TRUE to get a count. + * Or boolean TRUE as shortcut for "include=all". * @param bool|Field|null $field Optionally specify Field to limit results to (default includes all fields of this type), * Or boolean TRUE to return array indexed by field name. + * @param bool $getCount Specify true to get a count (int) rather than a PageArray (default=false) * @return PageArray|array|int Returns one of the following, according to the provided arguments: * - returns PageArray as default behavior, including when given a $selector string and/or Field object. * - returns array of PageArray objects if $field argument is TRUE ($selector may be populated string or blank string). * - returns int if the count option (boolean true) specified for $selector. * */ - private function findReferences(Page $page, $selector = '', $field = false) { + public function findReferences(Page $page, $selector = '', $field = false, $getCount = false) { + /** @var Pages $pages */ + $pages = $this->wire('pages'); + // modifier option defaults - $getCount = false; $byField = false; + $includeAll = $selector === true || $selector === "include=all"; + $findLimit = 200; $fieldName = ''; - // determine which modifier options are used + // determine whether to use include=all if($selector === true) { - $getCount = true; - $selector = ''; + $selector = "include=all"; + } else if(strlen($selector) && !$includeAll && strpos($selector, "include=all") !== false) { + foreach(new Selectors($selector) as $s) { + if($s->field() === 'include' && $s->value() === 'all') { + $includeAll = true; + break; + } + } } if(is_bool($field) || is_null($field)) { @@ -1411,48 +1420,72 @@ class FieldtypePage extends FieldtypeMulti implements Module, ConfigurableModule } // results - $fieldNames = array(); - $fieldCounts = array(); + $fieldNames = array(); // field names that point to $page, array of [ field_id => field_name ] + $fieldCounts = array(); // counts indexed by field name (if count mode) $total = 0; - + + // first determine which fields have references to $page foreach($this->wire('fields') as $field) { + if($fieldName && $field->name != $fieldName) continue; if(!$field->type instanceof FieldtypePage) continue; + $table = $field->getTable(); $sql = "SELECT COUNT(*) FROM `$table` WHERE data=:id"; $query = $this->wire('database')->prepare($sql); $query->bindValue(':id', $page->id, \PDO::PARAM_INT); $query->execute(); + $cnt = (int) $query->fetchColumn(); - if($cnt) { + if($cnt > 0) { $fieldNames[$field->id] = $field->name; $fieldCounts[$field->name] = $cnt; $total += $cnt; } + $query->closeCursor(); } - // return count or array of counts - if($getCount) return $byField ? $fieldCounts : $total; - - // no references found and PageArray requested, return blank PageArray - if(!$total) return $this->wire('pages')->newPageArray(); + // if they just asked for the count, then we have all that we need to finish now + if($getCount && $includeAll) { + // return count or array of counts + return $byField ? $fieldCounts : $total; + } + // if there was nothing found, finish early + if(!$total) { + // no references found and PageArray requested, return blank PageArray + return $byField ? array() : $pages->newPageArray(); + } + + // perform another find() to filter results, and return requested result type if($byField) { // return array of PageArrays indexed by fieldName $result = array(); foreach($fieldNames as $fieldName) { - $selector = "$fieldName=$page->id, $selector"; - $items = $this->wire('pages')->find(rtrim($selector, ", ")); - if($items->count()) { - $result[$fieldName] = $items; + $s = rtrim("$fieldName=$page->id, $selector", ', '); + if($getCount) { + $cnt = $pages->count($s); + if($cnt) $result[$fieldName] = $cnt; + } else { + if($total > $findLimit) { + $items = $pages->findMany($s); + } else { + $items = $pages->find($s); + } + if($items->count()) $result[$fieldName] = $items; } } - } else { // return PageArray of all references - $selector = implode('|', $fieldNames) . "=$page->id, $selector"; - $result = $this->wire('pages')->find(rtrim($selector, ", ")); + $selector = rtrim(implode('|', $fieldNames) . "=$page->id, $selector", ', '); + if($getCount) { + $result = $pages->count($selector); + } else if($total > $findLimit) { + $result = $pages->findMany($selector); + } else { + $result = $pages->find($selector); + } } return $result; diff --git a/wire/modules/Fieldtype/FieldtypeTextarea.module b/wire/modules/Fieldtype/FieldtypeTextarea.module index 9cd90d2b..aaed4e19 100644 --- a/wire/modules/Fieldtype/FieldtypeTextarea.module +++ b/wire/modules/Fieldtype/FieldtypeTextarea.module @@ -8,7 +8,7 @@ * For documentation about the fields used in this class, please see: * /wire/core/Fieldtype.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * * Properties set to $field that is using this type, acceessed by $field->get('property'): @@ -24,7 +24,7 @@ class FieldtypeTextarea extends FieldtypeText { public static function getModuleInfo() { return array( 'title' => 'Textarea', - 'version' => 106, + 'version' => 107, 'summary' => 'Field that stores multiple lines of text', 'permanent' => true, ); @@ -373,5 +373,43 @@ class FieldtypeTextarea extends FieldtypeText { return $value; } + /** + * Find abstracted HTML/href attribute Textarea links to given $page + * + * @param Page $page Find links to this page + * @param string|bool $selector Optionally filter by selector or specify boolean true to assume "include=all". + * @param string|Field $field Optionally limit to searching given field name/instance. + * @param array $options Options to modify return value: + * - `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 + * + */ + public function findLinks(Page $page, $selector = '', $field = '', array $options = array()) { + + $searchFields = array(); + if($selector === true) $selector = "include=all"; + + foreach($this->wire('fields') as $f) { + if($field) { + if("$f" != "$field") continue; + } else { + // limit to fields with contentTypeHTML and htmlLinkAbstract + $contentType = $f->get('contentType'); + if(empty($contentType)) continue; + if($contentType != self::contentTypeHTML && $contentType != self::contentTypeImageHTML) continue; + $htmlOptions = $f->get('htmlOptions'); + if(!is_array($htmlOptions) || !in_array(self::htmlLinkAbstract, $htmlOptions)) continue; + } + $searchFields[$f->name] = $f->name; + } + + if(!count($searchFields)) return $this->wire('pages')->newPageArray(); + + return $this->markupQA()->findLinks($page, $searchFields, $selector, $options); + } + }