From cc53b835b64f4a91e3662f95d77c746865a4bfea Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 24 Aug 2018 10:41:47 -0400 Subject: [PATCH] Refactoring of the PagesTrash class and related, plus some minor additions to PagesNames class --- wire/core/Pages.php | 9 +- wire/core/PagesNames.php | 74 +++- wire/core/PagesTrash.php | 405 +++++++++++++++--- wire/modules/PagePermissions.module | 16 +- .../ProcessPageEdit/ProcessPageEdit.module | 14 +- wire/modules/Process/ProcessPageTrash.module | 31 +- 6 files changed, 439 insertions(+), 110 deletions(-) diff --git a/wire/core/Pages.php b/wire/core/Pages.php index a42ea5ae..f3c9f5f7 100644 --- a/wire/core/Pages.php +++ b/wire/core/Pages.php @@ -29,7 +29,7 @@ * @method bool saveField(Page $page, $field, array $options = array()) Save just the named field from $page. Same as: $page->save('field') #pw-group-manipulation * @method bool trash(Page $page, $save = true) Move a page to the trash. If you have already set the parent to somewhere in the trash, then this method won't attempt to set it again. #pw-group-manipulation * @method bool restore(Page $page, $save = true) Restore a trashed page to its original location. #pw-group-manipulation - * @method int emptyTrash() Empty the trash and return number of pages deleted. #pw-group-manipulation + * @method int|array emptyTrash(array $options = array()) Empty the trash and return number of pages deleted. #pw-group-manipulation * @method bool delete(Page $page, $recursive = false, array $options = array()) Permanently delete a page and it's fields. Unlike trash(), pages deleted here are not restorable. If you attempt to delete a page with children, and don't specifically set the $recursive param to True, then this method will throw an exception. If a recursive delete fails for any reason, an exception will be thrown. #pw-group-manipulation * @method Page|NullPage clone(Page $page, Page $parent = null, $recursive = true, $options = array()) Clone an entire page, it's assets and children and return it. #pw-group-manipulation * @method Page|NullPage add($template, $parent, $name = '', array $values = array()) #pw-group-manipulation @@ -619,13 +619,14 @@ class Pages extends Wire { * * #pw-group-manipulation * - * @return int Returns total number of pages deleted from trash. + * @param array $options See PagesTrash::emptyTrash() for advanced options + * @return int|array Returns total number of pages deleted from trash, or array if verbose option specified. * This number is negative or 0 if not all pages could be deleted and error notices may be present. * @see Pages::trash(), Pages::restore() * */ - public function ___emptyTrash() { - return $this->trasher()->emptyTrash(); + public function ___emptyTrash(array $options = array()) { + return $this->trasher()->emptyTrash($options); } /** diff --git a/wire/core/PagesNames.php b/wire/core/PagesNames.php index 969b01d6..933194ca 100644 --- a/wire/core/PagesNames.php +++ b/wire/core/PagesNames.php @@ -135,17 +135,41 @@ class PagesNames extends Wire { return array(implode($delimiter, $parts), (int) $suffix); } + /** + * Does the given name or Page have a number suffix? Returns the number if yes, or false if not + * + * @param string|Page $name + * @param bool $getNamePrefix Return the name prefix rather than the number suffix? (default=false) + * @return int|bool|string Returns false if no number suffix, or int for number suffix or string for name prefix (if requested) + * + */ + public function hasNumberSuffix($name, $getNamePrefix = false) { + if($name instanceof Page) $name = $name->name; + list($namePrefix, $numberSuffix) = $this->nameAndNumber($name); + if(!$numberSuffix) return false; + return $getNamePrefix ? $namePrefix : $numberSuffix; + } + /** * Get the name format string that should be used for given $page if no name was assigned * * @param Page $page + * @param array $options + * - `fallbackFormat` (string): Fallback format if another cannot be determined (default='untitled-time'). + * - `parent` (Page|null): Optional parent page to use instead of $page->parent (default=null). * @return string * */ - public function defaultPageNameFormat(Page $page) { + public function defaultPageNameFormat(Page $page, array $options = array()) { - $format = 'untitled-time'; // default fallback format - $parent = $page->parent(); + $defaults = array( + 'fallbackFormat' => 'untitled-time', + 'parent' => null, + ); + + $options = array_merge($defaults, $options); + $parent = $options['parent'] ? $options['parent'] : $page->parent(); + $format = ''; if($parent && $parent->id && $parent->template->childNameFormat) { // if format specified with parent template, use that @@ -162,6 +186,14 @@ class PagesNames extends Wire { if(strlen($pageTitle->getDefaultValue())) $format = 'title'; } + if(empty($format)) { + if($page->id && $options['fallbackFormat']) { + $format = $options['fallbackFormat']; + } else { + $format = 'untitled-time'; + } + } + return $format; } @@ -273,29 +305,47 @@ class PagesNames extends Wire { * * @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. - * + * Note that specifying a Page here or in the first argument is important if the page already exists, as it is used + * as the page to exclude when checking for name collisions, and we want to exclude $page from that check. + * @param array $options + * - `parent` (Page|null): Optionally specify a different parent if $page does not currently have the parent you want to use. + * - `language` (Language|int): Get unique for this language (if multi-language page names active). * @return string Returns unique name * */ - public function uniquePageName($name = '', $page = null) { + public function uniquePageName($name = '', $page = null, array $options = array()) { + + $defaults = array( + 'page' => null, + 'parent' => null, + 'language' => null + ); - $options = array(); + $options = array_merge($defaults, $options); if($name instanceof Page) { $_name = is_string($page) ? $page : ''; $page = $name; $name = $_name; } - + if($page) { - $parent = $page->parent(); + if($options['parent'] === null) $options['parent'] = $page->parent(); if(!strlen($name)) $name = $page->name; - $options['parent'] = $parent; $options['page'] = $page; } - + if(!strlen($name)) { - $name = $this->uniqueRandomPageName(); + // no name currently present, so we need to determine what kind of name it should have + if($page) { + $format = $this->defaultPageNameFormat($page, array( + 'fallbackFormat' => $page->id ? 'random' : 'untitled-time', + 'parent' => $options['parent'] + )); + $name = $this->pageNameFromFormat($page, $format); + } else { + $name = $this->uniqueRandomPageName(); + } } while($this->pageNameExists($name, $options)) { @@ -389,7 +439,7 @@ class PagesNames extends Wire { * - `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) + * - `language` (Language|int): Limit check to only this language [also implies multilang option] (default=null) * * @return int Returns quantity of pages using name, or 0 if name not in use. * diff --git a/wire/core/PagesTrash.php b/wire/core/PagesTrash.php index f3f5570b..b35e27ec 100644 --- a/wire/core/PagesTrash.php +++ b/wire/core/PagesTrash.php @@ -5,7 +5,7 @@ * * Implements page trash/restore/empty methods of the $pages API variable * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * */ @@ -17,7 +17,13 @@ class PagesTrash extends Wire { * */ protected $pages; - + + /** + * Construct + * + * @param Pages $pages + * + */ public function __construct(Pages $pages) { $this->pages = $pages; } @@ -34,13 +40,17 @@ class PagesTrash extends Wire { * */ public function trash(Page $page, $save = true) { + if(!$this->pages->isDeleteable($page) || $page->template->noTrash) { throw new WireException("This page may not be placed in the trash"); } + if(!$trash = $this->pages->get($this->config->trashPageID)) { throw new WireException("Unable to load trash page defined by config::trashPageID"); } + $page->addStatus(Page::statusTrash); + if(!$page->parent->isTrash()) { $parentPrevious = $page->parent; $page->parent = $trash; @@ -49,7 +59,10 @@ class PagesTrash extends Wire { } else { $parentPrevious = null; } - if(!preg_match('/^' . $page->id . '(\.\d+\.\d+)?_.+/', $page->name)) { + + $nameInfo = $this->parseTrashPageName($page->name); + + if(!$nameInfo || $nameInfo['id'] != $page->id) { // make the name unique when in trash, to avoid namespace collision and maintain parent restore info $name = $page->id; if($parentPrevious && $parentPrevious->id) { @@ -68,12 +81,13 @@ class PagesTrash extends Wire { $page->set("name$language->id", $name . "_" . $langName); } } - } + if($save) $this->pages->save($page); $this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, false); if($save) $this->pages->trashed($page); $this->pages->debugLog('trash', $page, true); + return true; } @@ -90,55 +104,186 @@ class PagesTrash extends Wire { */ public function restore(Page $page, $save = true) { - if(preg_match('/^(' . $page->id . ')((?:\.\d+\.\d+)?)_(.+)$/', $page->name, $matches)) { - - if($matches[2]) { - /** @noinspection PhpUnusedLocalVariableInspection */ - list($unused, $parentID, $sort) = explode('.', $matches[2]); - $parentID = (int) $parentID; - $sort = (int) $sort; - } else { - $parentID = 0; - $sort = 0; - } - - $prefix = $matches[1] . $matches[2] . '_'; - $name = $matches[3]; - - if($parentID && $page->parent->isTrash() && !$page->parentPrevious) { - // no new parent was defined, so use the one in the page name - $newParent = $this->pages->get($parentID); - if($newParent->id && $newParent->id != $page->id) { - $page->parent = $newParent; - $page->sort = $sort; - } - } - if(!count($page->parent->children("name=$name, include=all"))) { - $page->name = $name; // remove namespace collision info if no collision - // do the same for other languages, when applicable - if($this->wire('languages') && $this->wire('modules')->isInstalled('LanguageSupportPageNames')) { - foreach($this->wire('languages') as $language) { - if($language->isDefault()) continue; - $langName = $page->get("name$language->id"); - if(strpos($langName, $prefix) !== 0) continue; - $langName = str_replace($prefix, '', $langName); - $page->set("name$language->id", $langName); - } - } - } - } - - if(!$page->parent->isTrash()) { + $info = $this->getRestoreInfo($page, true); + if(!$info['restorable']) return false; + + if($page->parent->isTrash()) { + if($save) $page->save(); + } else { $page->removeStatus(Page::statusTrash); if($save) $page->save(); $this->pages->editor()->savePageStatus($page->id, Page::statusTrash, true, true); if($save) $this->pages->restored($page); $this->pages->debugLog('restore', $page, true); + } + + return true; + } + + /** + * Get info needed to restore a Page that is in the trash + * + * Returns array with the following info: + * - `restorable` (bool): Is the page restorable to a previous known/existing parent? + * - `notes` (array): Any additional notes to explain restore info (like reason why not restorable, or why name changed, etc.) + * - `parent` (Page|NullPage): Parent page that it should restore to + * - `parent_id` (int): ID of parent page that it should restore to + * - `sort` (int): Sort order that should be restored to page + * - `name` (string): Name that should be restored to page’s “name” property. + * - `namePrevious` (string): Previous name, if we had to modify the original name to make it restorable. + * - `name{id}` (string): Name that should be restored to language where {id} is language ID (if appliable). + * + * @param Page $page Page to restore + * @param bool $populateToPage Populate this information to given page? (default=false) + * @return array + * + */ + public function getRestoreInfo(Page $page, $populateToPage = false) { + + $info = array( + 'restorable' => false, + 'notes' => array(), + 'parent' => $this->pages->newNullPage(), + 'parent_id' => 0, + 'sort' => 0, + 'name' => '', + 'namePrevious' => '', + ); + + /** @var Languages|array $languages */ + $languages = $this->wire('languages'); + if(!$languages || !$this->wire('modules')->isInstalled('LanguageSupportPageNames')) $languages = array(); + + // initialize name properties in $info for each language + foreach($languages as $language) { + $info["name$language->id"] = ''; + } + + $result = $this->parseTrashPageName($page->name); + + if(!$result || $result['id'] !== $page->id) { + // page does not have restore info + $info['notes'][] = 'Page name does not contain restore information'; + return $info; + } + + $name = $result['name']; + $trashPrefix = $result['prefix']; // pageID.parentID.sort_ prefix for testing other language names later + $newParent = null; + $parentID = $result['parent_id']; + $sort = $result['sort']; + + if($parentID && $parentID != $page->id) { + if($page->rootParent()->isTrash()) { + // no new parent was defined, so use the one in the page name + $newParent = $this->pages->get($parentID); + if(!$newParent->id) { + $newParent = null; + $info['notes'][] = 'Original parent no longer exists'; + } + } else { + $info['notes'][] = 'Page root parent is not trash'; + } + + } else if($parentID) { + $info['notes'][] = "Invalid parent ID: $parentID"; + } else { - if($save) $page->save(); + // page was likely trashed a long time ago, before this info was stored + $info['notes'][] = 'Page name does not contain previous parent or sort info'; } - return true; + $info['parent'] = $newParent ? $newParent : $this->pages->newNullPage(); + $info['parent_id'] = $parentID; + $info['sort'] = $sort; + + // if we have no new parent available we can exit now + if(!$newParent) { + $info['notes'][] = 'Unable to determine parent to restore to'; + return $info; + } + + // check if there is already a page at the restore location with the same name + $namePrevious = $name; + $name = $this->pages->names()->uniquePageName($name, $page, array('parent' => $newParent)); + + if($name !== $namePrevious) { + $info['notes'][] = "Name changed from '$namePrevious' to '$name' to be unique in new parent"; + $info['namePrevious'] = $namePrevious; + } + + $info['name'] = $name; + $info['restorable'] = true; + + if($populateToPage) { + $page->name = $name; + $page->parent = $newParent; + $page->sort = $sort; + } + + // do the same for other languages, when applicable + foreach($languages as $language) { + /** @var Language $language */ + if($language->isDefault()) continue; + $langName = $page->get("name$language->id"); + if(!strlen($langName)) continue; + if(strpos($langName, $trashPrefix) === 0) { + list(,$langName) = explode('_', $langName); + } + $langNamePrevious = $langName; + $langName = $this->pages->names()->uniquePageName($langName, $page, array( + 'parent' => $newParent, + 'language' => $language + )); + if($populateToPage) $page->set("name$language->id", $langName); + $info["name$language->id"] = $langName; + if($langName !== $langNamePrevious) { + $info['notes'][] = $language->get('title|name') . ' ' . + "name changed from '$langNamePrevious' to '$langName' to be unique in new parent"; + } + } + + return $info; + } + + /** + * Parse a trashed page name into an array of its components + * + * @param string $name + * @return array|bool Returns array of info if name is a trash/restore name, or boolean false if not + * + */ + public function parseTrashPageName($name) { + + $info = array( + 'id' => 0, + 'parent_id' => 0, + 'sort' => 0, + 'name' => $name, + 'prefix' => '', + 'note' => '', + ); + + // match "pageID.parentID.sort_name" in page name (1).(2.2)_3 + if(!preg_match('/^(\d+)((?:\.\d+\.\d+)?)_(.+)$/', $name, $matches)) return false; + + $info['id'] = (int) $matches[1]; + $info['name'] = $matches[3]; + + if($matches[2]) { + // matches[2] contains ".parentID.sort" + list(, $parentID, $sort) = explode('.', $matches[2]); + $info['parent_id'] = (int) $parentID; + $info['sort'] = (int) $sort; + } else { + // page was likely trashed a long time ago, before this info was stored + $info['note'] = 'Page name does not contain previous parent or sort info'; + } + + // pageID.parentID.sort_ prefix that can be used with other language names + $info['prefix'] = $matches[1] . $matches[2] . '_'; + + return $info; } /** @@ -146,54 +291,178 @@ class PagesTrash extends Wire { * * Populates error notices when there are errors deleting specific pages. * - * @return int Returns total number of pages deleted from trash. - * This number is negative or 0 if not all pages could be deleted and error notices may be present. + * @param array $options + * - `chunkSize` (int): Pages will be deleted in chunks of this many pages per chunk (default=100). + * - `chunkTimeLimit` (int): Maximum seconds allowed to process deletion of each chunk (default=600). + * - `chunkLimit' (int): Maximum chunks to process in an emptyTrash() call (default=1000); + * - `pageLimit` (int): Maximum pages to delete per emptyTrash() call (default=0, no limit). + * - `timeLimit` (int): Maximum time (in seconds) to allow for trash empty (default=3600). + * - `pass2` (bool): Perform a secondary pass using alternate method as a backup? (default=true) + * Note: pass2 is always disabled when a pageLimit is in use or timeLimit has been exceeded. + * - `verbose` (bool): Return verbose array of information about the trash empty process? For debug/dev purposes (default=false) + * @return int|array Returns integer (default) or array in verbose mode. + * - By default, returns total number of pages deleted from trash. This number is negative or 0 if not + * all pages could be deleted and error notices may be present. + * - Returns associative array with verbose information if verbose option is chosen. * */ - public function emptyTrash() { + public function emptyTrash(array $options = array()) { - $trashPage = $this->pages->get($this->wire('config')->trashPageID); - $selector = "include=all, has_parent=$trashPage, children.count=0, status=" . Page::statusTrash; + $defaults = array( + 'chunkSize' => 100, + 'chunkTimeLimit' => 600, + 'chunkLimit' => 100, + 'pageLimit' => 0, + 'timeLimit' => 3600, + 'pass2' => true, + 'verbose' => false, + ); + + $options = array_merge($defaults, $options); + $trashPageID = $this->wire('config')->trashPageID; + $masterSelector = "include=all, children.count=0, status=" . Page::statusTrash; $totalDeleted = 0; $lastTotalInTrash = 0; - $numBatches = 0; + $chunkCnt = 0; + $errorCnt = 0; + $nonTrashIDs = array(); // page IDs that had trash status but did not have trash parent + $result = array(); + $timer = $options['verbose'] ? Debug::timer() : null; + $startTime = time(); + $stopTime = $options['timeLimit'] ? $startTime + $options['timeLimit'] : false; + $stopNow = false; + $options['stopTime'] = $stopTime; // for pass2 + // Empty trash pass1: + // Operates by finding pages in trash using Page::statusTrash that have no children do { - set_time_limit(60 * 10); + $selector = $masterSelector; + + if($options['chunkTimeLimit']) { + set_time_limit($options['chunkTimeLimit']); + } + + if(count($nonTrashIDs)) { + $selector .= ", id!=" . implode('|', $nonTrashIDs); + } + $totalInTrash = $this->pages->count($selector); if(!$totalInTrash || $totalInTrash == $lastTotalInTrash) break; $lastTotalInTrash = $totalInTrash; - $items = $this->pages->find("$selector, limit=100"); + + if($options['chunkSize'] > 0) $selector .= ", limit=$options[chunkSize]"; + $items = $this->pages->find($selector); $cnt = $items->count(); + foreach($items as $item) { + + // determine if any limits have been reached + if($stopTime && time() > $stopTime) $stopNow = true; + if($options['pageLimit'] && $totalDeleted >= $options['pageLimit']) $stopNow = true; + if($stopNow) break; + + // if page does not have trash as a parent, then this is a page with trash status + // that is somewhere else in the page tree (not likely) + if($item->rootParent()->id !== $trashPageID) { + $nonTrashIDs[$item->id] = $item->id; + $errorCnt++; + continue; + } + + // delete the page try { $totalDeleted += $this->pages->delete($item, true); } catch(\Exception $e) { $this->error($e->getMessage()); + $errorCnt++; } } + $this->pages->uncacheAll(); - $numBatches++; - } while($cnt); + $chunkCnt++; + if($options['chunkLimit'] && $chunkCnt >= $options['chunkLimit']) break; + + } while($cnt && !$stopNow); - // just in case anything left in the trash, use a backup method - $trashPage = $this->pages->get($trashPage->id); // fresh copy - $trashPages = $trashPage->children("include=all"); - foreach($trashPages as $t) { - try { - $totalDeleted += $this->pages->delete($t, true); - } catch(\Exception $e) { - $this->error($e->getMessage()); - } + // if recording verbose info, populate it for pass1 now + if($options['verbose']) { + $result['pass1_cnt'] = $chunkCnt; + $result['pass1_numDeleted'] = $totalDeleted; + $result['pass1_numErrors'] = $errorCnt; + $result['pass1_elapsedTime'] = Debug::timer($timer); } - $this->pages->uncacheAll(); - if($totalDeleted) { - $totalInTrash = $this->pages->count("has_parent=$trashPage, include=all, status=" . Page::statusTrash); - if($totalInTrash) $totalDeleted = $totalDeleted * -1; + // Empty trash pass2: + // Operates by finding pages that are children of the Trash and performing recursive delete upon them + if($options['pass2'] && !$stopNow && !$options['pageLimit']) { + $totalDeleted += $this->emptyTrashPass2($options, $result); + } + + if($totalDeleted || $options['verbose']) { + $numTrashChildren = $this->wire('pages')->count("parent_id=$trashPageID, include=all"); + // return a negative number if pages still remain in trash + if($numTrashChildren && !$options['verbose']) $totalDeleted = $totalDeleted * -1; + } else { + $numTrashChildren = 0; + } + + if($options['verbose']) { + $result['startTime'] = $startTime; + $result['elapsedTime'] = Debug::timer($timer); + $result['numDeleted'] = $totalDeleted; + $result['numRemain'] = $numTrashChildren; + $result['numErrors'] = $errorCnt; + $result['numMispaced'] = count($nonTrashIDs); + $result['idsMisplaced'] = $nonTrashIDs; + $result['options'] = $options; + return $result; } return $totalDeleted; } + /** + * Secondary pass for trash deletion + * + * This works by finding the children of the trash page and performing a recursive delete on them. + * + * @param array $options Options passed to emptyTrash() method + * @param array $result Verbose array, modified directly + * @return int + * + */ + protected function emptyTrashPass2(array $options, &$result) { + + if($options['chunkTimeLimit']) { + set_time_limit($options['chunkTimeLimit']); + } + + $timer = $options['verbose'] ? Debug::timer() : null; + $numErrors = 0; + $numDeleted = 0; + $trashPage = $this->pages->get($this->wire('config')->trashPageID); + $trashPages = $trashPage->children("include=all"); + + foreach($trashPages as $t) { + try { + // perform recursive delete + $numDeleted += $this->pages->delete($t, true); + } catch(\Exception $e) { + $this->error($e->getMessage()); + $numErrors++; + } + if($options['stopTime'] && time() > $options['stopTime']) break; + } + + $this->pages->uncacheAll(); + + if($options['verbose']) { + $result['pass2_numDeleted'] = $numDeleted; + $result['pass2_numErrors'] = $numErrors; + $result['pass2_elapsedTime'] = Debug::timer($timer); + } + + return $numDeleted; + } + } \ No newline at end of file diff --git a/wire/modules/PagePermissions.module b/wire/modules/PagePermissions.module index 3fe359ce..16cb87fe 100644 --- a/wire/modules/PagePermissions.module +++ b/wire/modules/PagePermissions.module @@ -746,25 +746,19 @@ class PagePermissions extends WireData implements Module { * */ public function restorable($event) { - $event->return = true; /** @var Page $page */ $page = $event->object; /** @var User $user */ $user = $this->wire('user'); $event->return = false; if($page->isLocked()) return; - if(!$page->isTrash()) return; + if(!$page->isTrash() && !$page->rootParent()->isTrash()) return; if(!$user->isSuperuser() && !$page->editable()) return; - $parts = explode('.', $page->name, 4); - if(count($parts) < 3) return; - list($pageID, $parentID, $rest) = explode('.', $page->name); - list($sort, $name) = explode('_', $rest, 2); - if($pageID || $sort) {} // ignore - $parent = $this->wire('pages')->get((int) $parentID); + $info = $this->wire('pages')->trasher()->getRestoreInfo($page); + if(!$info['restorable']) return; + $parent = $info['parent']; // check if parent does not allow this user to add pages here - if(!$parent->addable($page)) return; - // check if parent already has a page with the same name - if($parent->numChildren("name=$name, include=all")) return; + if(!$parent->id || !$parent->addable($page)) return; $event->return = true; } diff --git a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module index 5df0d848..c6a50cc0 100644 --- a/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module +++ b/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module @@ -1443,6 +1443,9 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod if(!$this->page->isTrash()) return false; if(!$this->page->restorable()) return false; + $info = $this->wire('pages')->trasher()->getRestoreInfo($this->page); + if(!$info['restorable']) return false; + /** @var InputfieldWrapper $wrapper */ $wrapper = $this->wire(new InputfieldWrapper()); $id = $this->className() . 'Restore'; @@ -1451,13 +1454,8 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $wrapper->attr('id', $id); $wrapper->attr('title', $restoreLabel); $this->addTab($id, $restoreLabel); - - if(!preg_match('/^(\d+)\.(\d+)\.(\d+)_(.+)$/', $this->page->name, $matches)) return false; - $parentID = (int) $matches[2]; - $parent = $parentID ? $this->wire('pages')->get($parentID) : null; + $newPath = $info['parent']->path() . $info['name'] . '/'; - if(!$parent || !$parent->id) return false; - /** @var InputfieldCheckbox $field */ $field = $this->modules->get('InputfieldCheckbox'); $field->attr('id+name', 'restore_page'); @@ -1466,7 +1464,9 @@ class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableMod $field->icon = 'trash-o'; $field->label = $restoreLabel2; $field->description = $this->_('Check the box to confirm that you want to restore this page.'); // Restore page confirmation instruction - $field->notes = sprintf($this->_('The page will be restored into parent: **%s**'), $parent->get('path')); + $field->notes = sprintf($this->_('The page will be restored to: **%s**.'), $newPath); + if($info['namePrevious']) $field->notes .= ' ' . + sprintf($this->_('Original name will be adjusted from **%1$s** to **%2$s** to be unique.'), $info['namePrevious'], $info['name']); $field->label2 = $restoreLabel; $wrapper->append($field); diff --git a/wire/modules/Process/ProcessPageTrash.module b/wire/modules/Process/ProcessPageTrash.module index e2eda5d5..47337977 100644 --- a/wire/modules/Process/ProcessPageTrash.module +++ b/wire/modules/Process/ProcessPageTrash.module @@ -8,12 +8,14 @@ * For more details about how Process modules work, please see: * /wire/core/Process.php * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * */ class ProcessPageTrash extends Process { + + const debug = false; public static function getModuleInfo() { return array( @@ -31,16 +33,23 @@ class ProcessPageTrash extends Process { public function ___execute() { if(!$this->wire('user')->isSuperuser()) throw new WirePermissionException(); + $input = $this->wire('input'); - if(isset($_POST['submit_empty']) && !empty($_POST['confirm_empty'])) { + if($input->post('submit_empty') && $input->post('confirm_empty')) { $this->session->CSRF->validate(); - $totalDeleted = $this->wire('pages')->emptyTrash(); + $result = $this->wire('pages')->emptyTrash(array( + 'verbose' => true + )); + if(self::debug) $this->warning($result); $message = $this->_('Emptied the trash') . ' ' . - sprintf($this->_n('(%d page)', '(%d pages)', abs($totalDeleted)), abs($totalDeleted)); - if($totalDeleted < 0) $message .= ' - ' . $this->_('Not all pages could be deleted'); + sprintf($this->_n('(%d page)', '(%d pages)', $result['numDeleted']), $result['numDeleted']); + if($result['numRemain'] > 0) { + $message .= ' - ' . $this->_('Not all pages could be deleted'); + } $this->session->message($message); // redirect to admin root after emptying trash - $this->session->redirect($this->config->urls->admin); + $this->session->redirect($this->wire('config')->urls('admin')); + return ''; } else { // render a form showing what pages are in the trash and confirming they want to empty it return $this->render(); @@ -55,20 +64,24 @@ class ProcessPageTrash extends Process { $trashPages = $this->pages->get($this->config->trashPageID)->children("limit=2, status<" . Page::statusMax); + /** @var InputfieldForm $form */ $form = $this->modules->get("InputfieldForm"); $form->attr('action', './'); $form->attr('method', 'post'); if(!count($trashPages)) return "

" . $this->_("The trash is empty") . "

"; + /** @var InputfieldMarkup $field */ $field = $this->modules->get("InputfieldMarkup"); $field->label = $this->_("The following pages are in the trash"); + /** @var ProcessPageList $pageList */ $pageList = $this->modules->get('ProcessPageList'); $pageList->set('id', $this->config->trashPageID); $pageList->set('showRootPage', false); $field->value = $pageList->execute(); $form->add($field); + /** @var InputfieldCheckbox $field */ $field = $this->modules->get("InputfieldCheckbox"); $field->attr('name', 'confirm_empty'); $field->attr('value', 1); @@ -77,13 +90,15 @@ class ProcessPageTrash extends Process { $field->notes = $this->_("If there are too many items in the trash, you may have to empty it multiple times."); $form->add($field); + /** @var InputfieldSubmit $field */ $field = $this->modules->get("InputfieldSubmit"); $field->attr('name', 'submit_empty'); $form->add($field); return $form->render(); - - + } + + public function ___executeForce() { } }