mirror of
https://github.com/processwire/processwire.git
synced 2025-08-08 15:57:01 +02:00
Refactoring of the PagesTrash class and related, plus some minor additions to PagesNames class
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user