From dfa8cc7b746ed0fd6d3990b526ffe5f839222878 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 23 Aug 2018 10:30:12 -0400 Subject: [PATCH] Refactoring of the PagesEditor class, primarily addition of a new PagesNames class for handling page names, duplicate names, untitled pages, incrementing page names, etc. --- wire/config.php | 10 + wire/core/Config.php | 1 + wire/core/Pages.php | 17 + wire/core/PagesEditor.php | 226 +++----- wire/core/PagesNames.php | 535 ++++++++++++++++++ .../ProcessPageEdit/ProcessPageEdit.module | 40 +- 6 files changed, 653 insertions(+), 176 deletions(-) create mode 100644 wire/core/PagesNames.php diff --git a/wire/config.php b/wire/config.php index d797b716..0a1def96 100644 --- a/wire/config.php +++ b/wire/config.php @@ -806,6 +806,16 @@ $config->pageNameCharset = 'ascii'; */ $config->pageNameWhitelist = '-_.abcdefghijklmnopqrstuvwxyz0123456789æåäßöüđжхцчшщюяàáâèéëêěìíïîõòóôøùúûůñçčćďĺľńňŕřšťýžабвгдеёзийклмнопрстуфыэęąśłżź'; +/** + * Name to use for untitled pages + * + * When page has this name, the name will be changed automatically (to a field like title) when it is possible to do so. + * + * @var string + * + */ +$config->pageNameUntitled = "untitled"; + /** * Maximum paginations * diff --git a/wire/core/Config.php b/wire/core/Config.php index 59f05e87..236d06f1 100644 --- a/wire/core/Config.php +++ b/wire/core/Config.php @@ -77,6 +77,7 @@ * * @property string $pageNameCharset Character set for page names, must be 'ascii' (default, lowercase) or 'UTF8' (uppercase). #pw-group-URLs * @property string $pageNameWhitelist Whitelist of characters allowed in UTF8 page names. #pw-group-URLs + * @property string $pageNameUntitled Name to use for untitled pages (default="untitled"). #pw-group-URLs * @property string $pageNumUrlPrefix Prefix used for pagination URLs. Default is "page", resulting in "/page1", "/page2", etc. #pw-group-URLs * @property array $pageNumUrlPrefixes Multiple prefixes that may be used for detecting pagination (internal use, for multi-language) #pw-group-URLs * @property int $maxUrlSegments Maximum number of extra stacked URL segments allowed in a page's URL (including page numbers) #pw-group-URLs diff --git a/wire/core/Pages.php b/wire/core/Pages.php index e90057fb..a42ea5ae 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -122,6 +122,12 @@ class Pages extends Wire { */ protected $editor; + /** + * @var PagesNames + * + */ + protected $names; + /** * @var PagesLoaderCache * @@ -1343,6 +1349,17 @@ class Pages extends Wire { if(!$this->editor) $this->editor = $this->wire(new PagesEditor($this)); return $this->editor; } + + /** + * @return PagesNames + * + * #pw-internal + * + */ + public function names() { + if(!$this->names) $this->names = $this->wire(new PagesNames($this)); + return $this->names; + } /** * @return PagesLoaderCache diff --git a/wire/core/PagesEditor.php b/wire/core/PagesEditor.php index fdafa77b..b8d23292 100644 --- a/wire/core/PagesEditor.php +++ b/wire/core/PagesEditor.php @@ -22,20 +22,12 @@ class PagesEditor extends Wire { */ protected $cloning = 0; - /** - * Name for autogenerated page names when fields to generate name aren't populated - * - * @var string - * - */ - protected $untitledPageName = 'untitled'; - /** * @var Pages * */ - protected $pages; - + protected $pages; + public function __construct(Pages $pages) { $this->pages = $pages; @@ -283,8 +275,11 @@ class PagesEditor extends Wire { * Auto-populate some fields for a new page that does not yet exist * * Currently it does this: + * + * - Assigns a parent if one is not already assigned. * - Sets up a unique page->name based on the format or title if one isn't provided already. - * - Assigns a 'sort' value'. + * - Assigns a sort value. + * - Populates any default values for fields. * * @param Page $page * @@ -292,9 +287,9 @@ class PagesEditor extends Wire { public function setupNew(Page $page) { $parent = $page->parent(); - if(!$parent->id) { - // auto-assign a parent, if we can find one in family settings + // assign parent + if(!$parent->id) { $parentTemplates = $page->template->parentTemplates; $parent = null; @@ -307,13 +302,17 @@ class PagesEditor extends Wire { if($parent->id) $page->parent = $parent; } - if(!strlen($page->name)) $this->pages->setupPageName($page); + // assign page name + if(!strlen($page->name)) { + $this->pages->setupPageName($page); // call through $pages intended, so it can be hooked + } + // assign sort order if($page->sort < 0) { - // auto assign a sort $page->sort = $page->parent->numChildren(); } + // assign any default values for fields foreach($page->template->fieldgroup as $field) { if($page->isLoaded($field->name)) continue; // value already set if(!$page->hasField($field)) continue; // field not valid for page @@ -348,115 +347,7 @@ class PagesEditor extends Wire { * */ public function setupPageName(Page $page, array $options = array()) { - - $defaults = array( - 'format' => '', - ); - $options = array_merge($defaults, $options); - $format = $options['format']; - $sanitizer = $this->wire('sanitizer'); - - if(strlen($page->name)) { - // make sure page starts with "untitled" or "untitled-" - if($page->name != $this->untitledPageName && strpos($page->name, "$this->untitledPageName-") !== 0) { - // page already has a name and it's not a temporary/untitled one - // so we do nothing - return ''; - } - // page starts with our untitled name, but is it in the exact format we use? - if($page->name != $this->untitledPageName) { - $parts = explode('-', $page->name); - array_shift($parts); // shift off 'untitled'; - $parts = implode('', $parts); // put remaining back together - // if we were left with something other than digits, - // this is not an auto-generated name, so leave as-is - if(!ctype_digit($parts)) return ''; - } - } - - if(!strlen($format)) { - $parent = $page->parent(); - if($parent && $parent->id) $format = $parent->template->childNameFormat; - } - - if(!strlen($format)) { - if(strlen($page->title)) { - // default format is title - $format = 'title'; - } else { - // if page has no title, default format is date - $format = 'Y-m-d H:i:s'; - } - } - - $pageName = ''; - - if(strlen($format)) { - // @todo add option to auto-gen name from any page property/field - - if($format == 'title') { - if(strlen($page->title)) $pageName = $page->title; - else $pageName = $this->untitledPageName; - - } else if(!ctype_alnum($format) && !preg_match('/^[-_a-zA-Z0-9]+$/', $format)) { - // it is a date format - $pageName = date($format); - } else { - - // predefined format - $pageName = $format; - } - - } else if(strlen($page->title)) { - $pageName = $page->title; - - } else { - // no name will be assigned - } - - if($pageName == $this->untitledPageName && strpos($page->name, $this->untitledPageName) === 0) { - // page already has untitled name, and there's no need to re-assign the untitled name - return ''; - } - - $name = ''; - if(strlen($pageName)) { - // make the name unique - - if($this->wire('config')->pageNameCharset === 'UTF8') { - $pageName = $sanitizer->pageNameUTF8($pageName); - } else { - $pageName = $sanitizer->pageName($pageName, Sanitizer::translate); - } - $numChildren = $page->parent->numChildren(); - $n = 0; - - do { - $name = $pageName; - if($n > 0) { - $nStr = "-" . ($numChildren + $n); - if($n > 100) { - // if we've reached this many dups, start adding a random element to it - $nStr = '_' . mt_rand() . $nStr; - } - if(strlen($name) + strlen($nStr) > Pages::nameMaxLength) { - $name = substr($name, 0, Pages::nameMaxLength - strlen($nStr)); - } - $name .= $nStr; - } - $n++; - } while($n < 200 && $this->pages->count("parent=$page->parent, name=" . $sanitizer->selectorValue($name) . ", include=all")); - - if($this->pages->count("parent=$page->parent, name=" . $sanitizer->selectorValue($name) . ", include=all") > 0) { - // this is now extremely unlikely - throw new WireException("Unable to generate unique name for page $page->id"); - } - - $page->name = $sanitizer->pageNameUTF8($name); - $page->set('_hasAutogenName', true); // for savePageQuery, provides adjustName behavior for new pages - } - - return $name; + return $this->pages->names()->setupNewPageName($page, isset($options['format']) ? $options['format'] : ''); } /** @@ -563,7 +454,9 @@ class PagesEditor extends Wire { } $sql = ''; - if(strpos($page->name, $this->untitledPageName) === 0) $this->pages->setupPageName($page); + if($this->pages->names()->isUntitledPageName($page->name)) { + $this->pages->setupPageName($page); + } $data = array( 'parent_id' => (int) $page->parent_id, @@ -632,53 +525,19 @@ class PagesEditor extends Wire { $query->bindValue(":$column", $value, is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR); } - $n = 0; $tries = 0; $maxTries = 100; do { $result = false; - $errorCode = 0; - + $keepTrying = false; try { - $result = false; $result = $database->execute($query); - } catch(\Exception $e) { - - $errorCode = $e->getCode(); - - // while setupNew() already attempts to uniqify a page name with an incrementing - // number, there is a chance that two processes running at once might end up with - // the same number, so we account for the possibility here by re-trying queries - // that trigger duplicate-entry exceptions - - if($errorCode == 23000 && ($page->_hasAutogenName || $options['adjustName'])) { - // Integrity constraint violation: 1062 Duplicate entry 'background-3552' for key 'name3894_parent_id' - // attempt to re-generate page name - $nameField = 'name'; - // account for the duplicate possibly being a multi-language name field - if($this->wire('languages') && preg_match('/\b(name\d*)_parent_id\b/', $e->getMessage(), $matches)) $nameField = $matches[1]; - // get either 'name' or 'name123' (where 123 is language ID) - $pageName = $page->$nameField; - // determine if current name format already has a trailing number - if(preg_match('/^(.+?)-(\d+)$/', $pageName, $matches)) { - // page already has a trailing number - $n = (int) $matches[2]; - $pageName = $matches[1]; - } - $nStr = '-' . (++$n); - if(strlen($pageName) + strlen($nStr) > Pages::nameMaxLength) $pageName = substr($pageName, 0, Pages::nameMaxLength - strlen($nStr)); - $page->name = $pageName . $nStr; - $query->bindValue(":$nameField", $this->wire('sanitizer')->pageName($page->name, Sanitizer::toAscii)); - - } else { - // a different exception that we don't catch, so re-throw it - throw $e; - } + $keepTrying = $this->savePageQueryException($page, $query, $e, $options); + if(!$keepTrying) throw $e; } - - } while($errorCode == 23000 && (++$tries < $maxTries)); + } while($keepTrying && (++$tries < $maxTries)); if($result && ($isNew || !$page->id)) $page->id = $database->lastInsertId(); if($options['forceID']) $page->id = (int) $options['forceID']; @@ -686,6 +545,47 @@ class PagesEditor extends Wire { return $result; } + /** + * Handle Exception for savePageQuery() + * + * While setupNew() already attempts to uniqify a page name with an incrementing + * number, there is a chance that two processes running at once might end up with + * the same number, so we account for the possibility here by re-trying queries + * that trigger duplicate-entry exceptions. + * + * Example of actual exception text, for reference: + * Integrity constraint violation: 1062 Duplicate entry 'background-3552' for key 'name3894_parent_id' + * + * @param Page $page + * @param \PDOStatement $query + * @param \PDOException|\Exception $exception + * @param array $options + * @return bool True if it should give $query another shot, false if not + * + */ + protected function savePageQueryException(Page $page, $query, $exception, array $options) { + + $errorCode = $exception->getCode(); + if($errorCode != 23000) return false; + + if(!$this->pages->names()->hasAutogenName($page) && !$options['adjustName']) return false; + + // account for the duplicate possibly being a multi-language name field + if($this->wire('languages') && preg_match('/\b(name\d*)_parent_id\b/', $exception->getMessage(), $matches)) { + $nameField = $matches[1]; + } else { + $nameField = 'name'; + } + + // get either 'name' or 'name123' (where 123 is language ID) + $pageName = $page->get($nameField); + $pageName = $this->pages->names()->incrementName($pageName); + $page->set($nameField, $pageName); + $query->bindValue(":$nameField", $this->wire('sanitizer')->pageName($pageName, Sanitizer::toAscii)); + + return true; + } + /** * Save individual Page fields and supporting actions * diff --git a/wire/core/PagesNames.php b/wire/core/PagesNames.php new file mode 100644 index 00000000..969b01d6 --- /dev/null +++ b/wire/core/PagesNames.php @@ -0,0 +1,535 @@ +pages = $pages; + $pages->wire($this); + $untitled = $this->wire('config')->pageNameUntitled; + if($untitled) $this->untitledPageName = $untitled; + parent::__construct(); + } + + /** + * Assign a name to given Page (if it doesn’t already have one) + * + * @param Page $page + * @param string $format + * @return string Returns page name that was assigned + * @throws WireException + * + */ + public function setupNewPageName(Page $page, $format = '') { + + $pageName = $page->name; + + // check if page already has a non-“untitled” name assigned that we should leave alone + if(strlen($pageName) && !$this->isUntitledPageName($pageName)) return ''; + + // determine what format should be used for the generated page name + if(!strlen($format)) $format = $this->defaultPageNameFormat($page); + + // generate a page name from determined format + $pageName = $this->pageNameFromFormat($page, $format); + + // ensure page name is unique + $pageName = $this->uniquePageName($pageName, $page); + + // assign to page + $page->name = $pageName; + + // indicate that page has auto-generated name for savePageQuery (provides adjustName behavior for new pages) + $page->setQuietly('_hasAutogenName', $pageName); + + return $pageName; + } + + /** + * Does the given page have an auto-generated name (during this request)? + * + * @param Page $page + * @return string|bool Returns auto-generated name if present, or boolean false if not + * + */ + public function hasAutogenName(Page $page) { + $name = $page->get('_hasAutogenName'); + if(empty($name)) $name = false; + return $name; + } + + /** + * Is given page name an untitled page name? + * + * @param string $name + * @return bool + * + */ + public function isUntitledPageName($name) { + list($namePrefix,) = $this->nameAndNumber($name); + return $namePrefix === $this->untitledPageName; + } + + /** + * If given name has a numbered suffix, return array with name (excluding suffix) and the numbered suffix + * + * Returns array like `[ 'name', 123 ]` where `name` is name without the suffix, and `123` is the numbered suffix. + * If the name did not have a numbered suffix, then the 123 will be 0 and `name` will be the given `$name`. + * + * @param string $name + * @param string $delimiter Character(s) that separate name and numbered suffix + * @return array + * + */ + public function nameAndNumber($name, $delimiter = '') { + if(empty($delimiter)) $delimiter = $this->delimiter; + $fail = array($name, 0); + if(strpos($name, $delimiter) === false) return $fail; + $parts = explode($delimiter, $name); + $suffix = array_pop($parts); + if(!ctype_digit($suffix)) return $fail; + $suffix = ltrim($suffix, '0'); + return array(implode($delimiter, $parts), (int) $suffix); + } + + /** + * Get the name format string that should be used for given $page if no name was assigned + * + * @param Page $page + * @return string + * + */ + public function defaultPageNameFormat(Page $page) { + + $format = 'untitled-time'; // default fallback format + $parent = $page->parent(); + + if($parent && $parent->id && $parent->template->childNameFormat) { + // if format specified with parent template, use that + $format = $parent->template->childNameFormat; + + } else if(strlen("$page->title")) { + // default format is title (when the page has one) + $format = 'title'; + + } else if($this->wire('languages') && $page->title instanceof LanguagesValueInterface) { + // check for multi-language title + /** @var LanguagesPageFieldValue $pageTitle */ + $pageTitle = $page->title; + if(strlen($pageTitle->getDefaultValue())) $format = 'title'; + } + + return $format; + } + + /** + * Create a page name from the given format + * + * - Returns a generated page name that is not yet assigned to the page. + * - If no format is specified, it first falls back to page parent template `childNameFormat` property (if present). + * - If no format can be determined, it falls back to a randomly generated page name. + * - Does not check if page name is already in use. + * + * Options for $format argument: + * + * - `title` Build name based on “title” field. + * - `field` Build name based on any other field name you choose, replace “field” with any field name. + * - `text` Text already in the right format (that’s not a field name) will be used literally, replace “text” with your text. + * - `random` Randomly generates a name. + * - `untitled` Uses an auto-incremented “untitled” name. + * - `untitled-time` Uses an “untitled” name followed by date/time number string. + * - `a|b|c` Builds name from first matching field name, where a|b|c are your field names. + * - `{field}` Builds name from the given field name. + * - `{a|b|c}` Builds name first matching field name, where a|b|c would be replaced with your field names. + * - `date:Y-m-d-H-i` Builds name from current date - replace “Y-m-d-H-i” with desired wireDate() format. + * - `string with space` A string that does not match one of the above and has space is assumed to be a wireDate() format. + * - `string with /` A string that does not match one of the above and has a “/” slash is assumed to be a wireDate() format. + * + * For formats above that accept a wireDate() format, see `WireDateTime::date()` method for format details. It accepts PHP + * date() format, PHP strftime() format, as well as some other predefined options. + * + * @param Page $page + * @param string $format Optional format. If not specified, pulls from $page’s parent template. + * + * @return string + * + */ + public function pageNameFromFormat(Page $page, $format = '') { + + if(!strlen($format)) $format = $this->defaultPageNameFormat($page); + $format = trim($format); + $name = ''; + + if($format === 'title' && !strlen(trim((string) $page->title))) { + $format = 'untitled-time'; + } + + if($format === 'title') { + // title + $name = trim((string) $page->title); + + } else if($format === 'random') { + // globally unique randomly generated page name + $name = $this->uniqueRandomPageName(); + + } else if($format === 'untitled') { + // just untitled + $name = $this->untitledPageName(); + + } else if($format === 'untitled-time') { + // untitled with datetime, i.e. “untitled-0yymmddhhmmss” (note leading 0 differentiates from increment) + $dateStr = date('ymdHis'); + $name = $this->untitledPageName() . '-0' . $dateStr; + + } else if(strpos($format, '}')) { + // string with {field_name} to text + $name = $page->getText($format, true, false); + + } else if(strpos($format, '|')) { + // field names separated by "|" until one matches + $name = $page->getUnformatted($format); + + } else if(strpos($format, 'date:') === 0) { + // specified date format + list(, $format) = explode('date:', $format); + if(empty($format)) $format = 'Y-m-d H:i:s'; + $name = wireDate(trim($format)); + + } else if(strpos($format, ' ') !== false || strpos($format, '/') !== false) { + // date assumed when spaces or slashes present in format + $name = wireDate($format); + + } else if($this->wire('sanitizer')->fieldName($format) === $format) { + // single field name or predefined string + // this can also return null, which falls back to if() statement below + $name = (string) $page->getUnformatted($format); + } + + if(!strlen($name)) { + // predefined string that is not a field name + $name = $format; + } + + $utf8 = $this->wire('config')->pageNameCharset === 'UTF8'; + $sanitizer = $this->wire('sanitizer'); + $name = $utf8 ? $sanitizer->pageNameUTF8($name) : $sanitizer->pageName($name, Sanitizer::translate); + + return $name; + } + + /** + * Get a unique page name + * + * 1. If given no arguments, it returns a random globally unique page name. + * 2. If given just a $name, it returns that name (if globally unique), or an incremented version of it that is globally unique. + * 3. If given both $page and $name, it returns given name if unique in parent, or incremented version that is. + * 4. If given just a $page, the name is pulled from $page and behavior is the same as #3 above. + * + * The returned value is not yet assigned to the given $page, so if it is something different than what + * is already on $page, you’ll want to assign it manually after this. + * + * @param string|Page $name Name to make unique, or Page to pull it from. + * @param Page||string|null You may optionally specify Page or name in this argument if not in the first. + * + * @return string Returns unique name + * + */ + public function uniquePageName($name = '', $page = null) { + + $options = array(); + + if($name instanceof Page) { + $_name = is_string($page) ? $page : ''; + $page = $name; + $name = $_name; + } + + if($page) { + $parent = $page->parent(); + if(!strlen($name)) $name = $page->name; + $options['parent'] = $parent; + $options['page'] = $page; + } + + if(!strlen($name)) { + $name = $this->uniqueRandomPageName(); + } + + while($this->pageNameExists($name, $options)) { + $name = $this->incrementName($name); + } + + return $name; + } + + /** + * If name exceeds maxLength, truncate it, while keeping any numbered suffixes in place + * + * @param string $name + * @param int $maxLength + * @return string + * + */ + public function adjustNameLength($name, $maxLength = 0) { + + if($maxLength < 1) $maxLength = Pages::nameMaxLength; + if(strlen($name) <= $maxLength) return $name; + + $trims = implode('', $this->delimiters); + $pos = 0; + + list($namePrefix, $numberSuffix) = $this->nameAndNumber($name); + + if($namePrefix !== $name) { + $numberSuffix = $this->delimiter . $numberSuffix; + $maxLength -= strlen($numberSuffix); + } else { + $numberSuffix = ''; + } + + if(strlen($namePrefix) > $maxLength) { + $namePrefix = substr($namePrefix, 0, $maxLength); + } + + // find word delimiter closest to end of string + foreach($this->delimiters as $c) { + $p = strrpos($namePrefix, $c); + if((int) $p > $pos) $pos = $p; + } + + // use word delimiter pos as maxLength when it’s relatively close to the end + if(!$pos || $pos < (strlen($namePrefix) / 1.3)) $pos = $maxLength; + + $name = substr($namePrefix, 0, $pos); + $name = rtrim($name, $trims); + + // append number suffix if there was one + if($numberSuffix) $name .= $numberSuffix; + + return $name; + } + + /** + * Increment the suffix of a page name, or add one if not present + * + * @param string $name + * @param int|null $num Number to use, or omit to determine and increment automatically + * @return string + * + */ + public function incrementName($name, $num = null) { + + list($namePrefix, $n) = $this->nameAndNumber($name); + + if($namePrefix !== $name) { + if($num) { + $num = (int) $num; + $name = $namePrefix . $this->delimiter . $num; + } else { + $zeros = ''; + while(strpos($name, $namePrefix . $this->delimiter . "0$zeros") === 0) $zeros .= '0'; + $name = $namePrefix . $this->delimiter . $zeros . (++$n); + } + } else { + if(!is_int($num)) $num = 1; + $name = $namePrefix . $this->delimiter . $num; + } + + return $this->adjustNameLength($name); + } + + /** + * Is the given name is use by a page? + * + * @param string $name + * @param array $options + * - `page` (Page|int): Ignore this Page or page ID + * - `parent` (Page|int): Limit search to only this parent. + * - `multilang` (bool): Check other languages if multi-language page names supported? (default=false) + * - `language` (Language|int): Limit check to only this language (default=null) + * + * @return int Returns quantity of pages using name, or 0 if name not in use. + * + */ + public function pageNameExists($name, array $options = array()) { + + $defaults = array( + 'page' => null, + 'parent' => null, + 'language' => null, + 'multilang' => false, + ); + + $options = array_merge($defaults, $options); + $languages = $options['multilang'] || $options['language'] ? $this->wire('languages') : null; + if($languages && !$this->wire('modules')->isInstalled('LanguageSupportPageNames')) $languages = null; + + $wheres = array(); + $binds = array(); + $parentID = $options['parent'] === null ? null : (int) "$options[parent]"; + $pageID = $options['page'] === null ? null : (int) "$options[page]"; + + if($languages) { + foreach($languages as $language) { + if($options['language'] && "$options[language]" !== "$language") continue; + $property = $language->isDefault() ? 'name' : 'name' . (int) $language->id; + $wheres[] = "$property=:name$language->id"; + $binds[":name$language->id"] = $name; + } + $wheres = array('(' . implode(' OR ', $wheres) . ')'); + } else { + $wheres[] = 'name=:name'; + $binds[':name'] = $name; + } + + if($parentID) { + $wheres[] = 'parent_id=:parent_id'; + $binds[':parent_id'] = $parentID; + } + if($pageID) { + $wheres[] = 'id!=:id'; + $binds[':id'] = $pageID; + } + + $sql = 'SELECT COUNT(*) FROM pages WHERE ' . implode(' AND ', $wheres); + $query = $this->wire('database')->prepare($sql); + + foreach($binds as $key => $value) { + $query->bindValue($key, $value); + } + + $query->execute(); + $qty = (int) $query->fetchColumn(); + $query->closeCursor(); + + return $qty; + } + + /** + * Get a random, globally unique page name + * + * @param array $options + * - `page` (Page): If name is or should be assigned to a Page, specify it here. (default=null) + * - `length` (int): Required/fixed length, or omit for random length (default=0). + * - `min` (int): Minimum required length, if fixed length not specified (default=6). + * - `max` (int): Maximum allowed length, if fixed length not specified (default=min*2). + * - `alpha` (bool): Include alpha a-z letters? (default=true) + * - `numeric` (bool): Include numeric digits 0-9? (default=true) + * - `confirm` (bool): Confirm that name is globally unique? (default=true) + * - `parent` (Page|int): If specified, name must only be unique for this parent Page or ID (default=0). + * - `prefix` (string): Prepend this prefix to page name (default=''). + * - `suffix` (string): Append this suffix to page name (default=''). + * + * @return string + * + */ + public function uniqueRandomPageName($options = array()) { + + $defaults = array( + 'page' => null, + 'length' => 0, + 'min' => 6, + 'max' => 0, + 'alpha' => true, + 'numeric' => true, + 'confirm' => true, + 'parent' => 0, + 'prefix' => '', + 'suffix' => '', + ); + + if(is_int($options)) $options = array('length' => $options); + $options = array_merge($defaults, $options); + $rand = new WireRandom(); + $this->wire($rand); + + do { + if($options['length'] < 1) { + if($options['min'] < 1) $options['min'] = 6; + if($options['max'] < $options['min']) $options['max'] = $options['min'] * 2; + if($options['min'] == $options['max']) { + $length = $options['max']; + } else { + $length = mt_rand($options['min'], $options['max']); + } + } else { + $length = (int) $options['length']; + } + + if($options['alpha'] && $options['numeric']) { + $name = $rand->alphanumeric($length, array('upper' => false, 'noStart' => '0123456789')); + } else if($options['numeric']) { + $name = $rand->numeric($length); + } else { + $name = $rand->alpha($length); + } + + $name = $options['prefix'] . $name . $options['suffix']; + + if($options['confirm']) { + $qty = $this->pageNameExists($name, array('page' => $options['page'])); + } else { + $qty = 0; + } + + } while($qty); + + if($options['page'] instanceof Page) $options['page']->set('name', $name); + + return $name; + } + + /** + * Return the untitled page name string + * + * @return string + * + */ + public function untitledPageName() { + return $this->untitledPageName; + } + +} diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index ed4dce06..5df0d848 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -2264,6 +2264,8 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $page = $this->masterPage ? $this->masterPage : $this->page; $parent = $page->parent; $parentEditable = ($parent->id && $parent->editable()); + /** @var Config $config */ + $config = $this->wire('config'); // current page template is assumed, otherwise we wouldn't be here $templates[$page->template->id] = $page->template; @@ -2283,9 +2285,9 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if($template->flags & Template::flagSystem) { // if($template->name == 'user' && $parent->id != $this->config->usersPageID) continue; - if(in_array($template->id, $this->config->userTemplateIDs) && !in_array($parent->id, $this->config->usersPageIDs)) continue; - if($template->name == 'role' && $parent->id != $this->config->rolesPageID) continue; - if($template->name == 'permission' && $parent->id != $this->config->permissionsPageID) continue; + if(in_array($template->id, $config->userTemplateIDs) && !in_array($parent->id, $config->usersPageIDs)) continue; + if($template->name == 'role' && $parent->id != $config->rolesPageID) continue; + if($template->name == 'permission' && $parent->id != $config->permissionsPageID) continue; if(strpos($template->name, 'repeater_') === 0 || strpos($template->name, 'fieldset_') === 0) continue; } @@ -2300,17 +2302,16 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if(!in_array($template->id, $parent->template->childTemplates)) continue; } - if($isSuperuser) { - $templates[$template->id] = $template; - - } else if($template->noParents == -1) { + if($template->noParents == -1 && $template->getNumPages() > 0) { // only one of these is allowed to exist - if($template->getNumPages() > 0) continue; + continue; } else if($template->noParents) { // user can't change to a template that has been specified as no more instances allowed - // except for superuser... we'll let them do it continue; + + } else if($isSuperuser) { + $templates[$template->id] = $template; } else if((!$template->useRoles && $parentEditable) || $user->hasPermission('page-edit', $template)) { // determine if the template's assigned roles match up with the users's roles @@ -2568,14 +2569,23 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod */ public function setupHeadline() { + $titlePage = null; + if($this->page && $this->page->id) { $page = $this->page; - $title = $page->get('title|name'); + $title = $page->get('title'); + if(empty($title)) { + if($this->wire('pages')->names()->isUntitledPageName($page->name)) { + $title = $page->template->getLabel(); + } else { + $title = $page->get('name'); + } + } } else if($this->parent && $this->parent->id) { - $page = $this->parent; + $titlePage = $this->parent; $title = rtrim($this->parent->path, '/') . '/[...]'; } else { - $page = new NullPage(); + $titlePage = new NullPage(); $title = '[...]'; } @@ -2592,8 +2602,12 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $headline = implode(', ', $labels); } $browserTitle .= " ($headline)"; + + } else if($titlePage) { + $headline = $titlePage->get('title|name'); + } else { - $headline = $page->get("title|name"); + $headline = $title; } $this->headline($headline);