foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationDeleteTrait.php
src/Entity/FolderShareTraits/OperationDeleteTrait.php
<?php
namespace Drupal\foldershare\Entity\FolderShareTraits;
use Drupal\Component\Utility\Environment;
use Drupal\foldershare\ManageLog;
use Drupal\foldershare\Settings;
use Drupal\foldershare\Utilities\CacheUtilities;
use Drupal\foldershare\Utilities\LimitUtilities;
use Drupal\foldershare\FolderShareInterface;
use Drupal\foldershare\Entity\FolderShare;
use Drupal\foldershare\Entity\FolderShareScheduledTask;
use Drupal\foldershare\Entity\Exception\LockException;
use Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException;
use Drupal\foldershare\Entity\Exception\MemoryLimitException;
/**
* Delete FolderShare entities.
*
* <B>Internal trait</B>
* This trait is internal to the FolderShare module and used to define
* features of the FolderShare entity class. It is a mechanism to group
* functionality to improve code management.
*
* @ingroup foldershare
*/
trait OperationDeleteTrait {
/*---------------------------------------------------------------------
*
* Delete.
*
*---------------------------------------------------------------------*/
/**
* {@inheritdoc}
*
* @internal
* Deletion treats files and folders differently.
*
* For a file, image, or media kind, the entity is deleted immediately,
* followed by deleting the underlying File or Media entity.
*
* For a folder, deletion proceeds in phases:
*
* - Phase 0. The folder to delete is marked as hidden. Since hidden items
* are not shown in file/folder lists, this provides quick feedback to the
* user that the item is being deleted.
*
* - Phase 1. A task is queued to sweep through all descendants and mark
* them as hidden. For speed, this is done using direct database updates
* rather than entity load/lock/set/save/unlock. This provides quick
* feedback to all users viewing anything in the folder tree being deleted.
*
* - Phase 2. A task is queued to recurse through all descendants and delete
* them. This takes longer because of the additional work to delete the
* entity out of one or more tables, and service hooks associated with
* deletion.
*
* Phases 0 executes in the requesting process. Phases 1 and 2 execute in
* whatever process gets to the scheduled task. This is unlikely to be the
* original requesting process, or even a process for the same user.
*/
public function delete() {
// ------------------------------------------------------------------
// This item can be:
// - A file or folder.
// - At the root level or in a subfolder.
// - Owned by the current user or another.
//
// Special cases:
// - If the item is owned by another user, it is still deleted. It is up
// to the caller to have checked permissions to insure this is valid.
//
// Errors:
// - The item is new and has no ID yet.
//
// Actions:
// - If the item is a file: Lock its root, delete, update ancestor sizes,
// and unlock.
//
// - If the item is a folder: Lock its root, mark as hidden, clear access
// grants (if it is a root), update ancestor sizes, and schedule "hide"
// task. The "hide" task recursively marks descendants hidden,
// flushes the cache, and schedules the "delete" task. The "delete"
// task recursively deletes, and unlocks the root.
// ------------------------------------------------------------------.
if ($this->isNew() === TRUE) {
// The has not been fully created yet (i.e. it is marked as "new").
// New items don't have an ID yet, and therefore can't have any
// subfolders or files yet, so really there's nothing to delete.
return;
}
//
// Lock root's folder tree.
// ------------------------
// During a delete, all other operations that might interfere with the
// delete must be blocked. And the delete cannot be allowed to interfere
// with any other operation already started.
//
// LOCK ROOT FOLDER TREE.
$rootId = $this->getRootItemId();
if (self::acquireRootOperationLock($rootId) === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('deleted'), $this->getName()));
}
//
// Delete file or empty folder.
// ----------------------------
// When the item is a folder without children or a file, there are no
// children to recurse through and deletion is fairly quick. Delete the
// item immediately and return.
if ($this->findNumberOfChildren() === 0) {
try {
// Passing TRUE as arg1 to deleteInternal causes ancestor sizes to
// be updated after the item is deleted. Deletion automatically
// flushes deleted entities from all caches.
$opCounter = 0;
$this->deleteInternal(TRUE, $opCounter, TRUE, (-1));
}
catch (ExecutionTimeLimitException | MemoryLimitException $e) {
// An execution time or memory limit has been exceeded.
//
// This is VERY unlikely. We're deleting a file or empty folder.
// Only a few entities have been loaded and that doesn't take
// much time or space.
//
// The exception is thrown AFTER the item has been deleted, so
// just continue. All that's left to do is release a lock.
}
catch (\Exception $e) {
// deleteInternal does not throw any known exceptions.
// If it gets one, it has already been logged.
//
// UNLOCK ROOT FOLDER TREE.
self::releaseRootOperationLock($rootId);
throw $e;
}
// UNLOCK ROOT FOLDER TREE.
self::releaseRootOperationLock($rootId);
return;
}
//
// Delete folder with children.
// ----------------------------
// When the item is a folder, there may be children, who may have children,
// and so on in a potentially huge folder tree. Without issuing queries,
// we cannot know right now how big that folder tree is. We must assume
// it is big enough that trying to delete it immediately would cause a
// significant delay before responding to the user's request.
//
// To give quick feedback, we mark the item to be deleted as hidden.
// This removes it from file/folder lists and looks like the item is gone.
// This takes much less time than recursing through the children.
//
// Afterwards, we schedule a task to mark the folder's descendants. This
// takes much less time than deleting those same descendants because it
// does not require entity deletion, field and entity hooks, and logging.
//
// Only after that is done do we schedule a task to actually delete the
// descendants.
$this->setSystemHidden(TRUE);
$this->clearAccessGrants(self::ANY_USER_ID, FALSE);
$this->save();
//
// Update ancestor sizes.
// ----------------------
// If needed, update ancestor sizes.
if ($this->isRootItem() === FALSE && $this->getSize() > 0) {
$parent = $this->getParentFolder();
if ($parent !== NULL) {
$parent->updateSizeAndAncestors();
unset($parent);
}
}
//
// Mark and delete descendants.
// ----------------------------
// First, mark all descendants as hidden. Then delete them in a
// second pass.
//
// If we have time left before we need to respond to the user, start
// the work. Otherwise schedule a task to do the work in the future.
//
// Keep root folder tree locks:
//
// - The root of the folder tree where content is being deleted.
//
// This will be unlocked by a future task when the entire delete is done.
$requestingUid = (int) \Drupal::currentUser()->id();
$parameters = [
'deleteIds' => [(int) $this->id()],
'unlockRootId' => $rootId,
];
$started = time();
$comments = 'Start delete phase 1 (hide)';
$executionTime = 0;
if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskDelete1(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete1',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
}
/**
* Deletes multiple items and their descendants.
*
* For any item that is a file, image, or media item, the underlying File
* or Media entity is deleted along with the item. Items are marked as
* hidden while they are being deleted. This removes them from the
* user interface.
*
* For any item that is a folder, a recursive traversal of the folder's
* children deletes them first, followed by deletion of the folder itself.
* Folders are marked as hidden as they are encountered during recursion. Root
* folders have access grants cleared (including for the owner), which
* blocks all user access to all descendants during deletion.
*
* System hidden and disabled items can be deleted.
*
* <B>Background delete</B>
* File deletion occurs immediately, but folder deletion schedules background
* tasks to traverse the folder tree and delete descendants. This will
* delay completion of the delete to a time in the future that depends upon
* the size of the folder tree being deleted and server load.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_delete" hook as
* items are deleted. When deletion requires scheduled background tasks,
* deletion hooks are called as those tasks are serviced.
*
* <B>Process locks</B>
* This method groups items to delete by their root folder trees, then
* processes each group. Processing locks each group's root folder tree
* for exclusive use during the delete. This will prevent any other edit
* operation from being performed on the same root folder tree until the
* deletion completes. When deletion requires scheduled background tasks,
* unlocking the root folder tree does not occur until the last descendant
* is deleted.
*
* <B>Activity log</B>
* This method posts a log message each time an item is deleted. When
* deletion requires scheduled background tasks, log messages are posted
* as those tasks are serviced.
*
* @param int[] $ids
* An array of integer FolderShare entity IDs to delete. Invalid IDs
* are silently skipped.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* If one or more items are in use and could not be locked and deleted.
*
* @see ::isSystemHidden()
* @see ::delete()
*/
public static function deleteMultiple(array $ids) {
// ------------------------------------------------------------------
// Each item can be:
// - A file or folder.
// - At the root level or in a subfolder.
// - Owned by the current user or another.
//
// Special cases:
// - If an item is owned by another user, it is still deleted. It is up
// to the caller to have checked permissions to insure this is valid.
//
// Errors:
// - An item is new and has no ID yet.
//
// Actions:
// - All items are sorted into groups with a shared root. In typical use,
// there will be just one shared root.
//
// - For all files and folders in the same root group: Lock the shared
// root. For each file, delete and update ancestor sizes. For each
// folder, mark as hidden, clear access grants, and update ancestor sizes.
// If there are no folders in the groupo, unlock the shared root.
//
// - After all groups: Schedule "hide" task. The "hide" task recursivelyw
// marks descendants hidden, flushes the cache, and schedules the
// "delete" task. The "delete" task recursively deletes, and unlocks
// the shared root.
// ------------------------------------------------------------------.
if (empty($ids) === TRUE) {
// Nothing to do.
return;
}
if (count($ids) === 1) {
// Save some work and use the simpler method.
$item = self::load(array_shift($ids));
if ($item === NULL) {
// The item does not exist.
return;
}
$item->delete();
unset($item);
return;
}
//
// Group IDs by root.
// ------------------
// The IDs given could be from scattered locations. Group them by
// their roots so that root folder locks can be done efficiently.
//
// Along the way also note if they are files or folders and ignore new
// items.
$rootGroups = [];
foreach ($ids as $id) {
$item = self::load($id);
if ($item === NULL || $item->isNew() === TRUE) {
// The item does not exist or it is new and has no ID yet.
continue;
}
$rootId = $item->getRootItemId();
$kind = ($item->isFolder() === TRUE) ? 'folder' : 'file';
$rootGroups[$rootId][$kind][] = $id;
unset($item);
}
unset($ids);
if (empty($rootGroups) === TRUE) {
// Nothing to do.
return;
}
//
// Loop over root groups.
// ----------------------
// Each root requires its own root folder lock, followed by deleting files
// immediately and marking folders hidden and scheduling a task to delete
// them and their descendants.
//
// This follows a pattern similar to delete(). See its comments for
// details.
$nLockExceptions = 0;
$nRootGroups = count($rootGroups);
foreach ($rootGroups as $rootId => $kinds) {
// LOCK ROOT FOLDER TREE.
if (self::acquireRootOperationLock($rootId) === FALSE) {
++$nLockExceptions;
continue;
}
$fileIds = [];
$folderIds = [];
if (isset($kinds['file']) === TRUE) {
$fileIds = $kinds['file'];
}
if (isset($kinds['folder']) === TRUE) {
$folderIds = $kinds['folder'];
}
$rootGroups[$rootId] = NULL;
//
// Delete files.
// -------------
// When items are files, there are no children to recurse through
// and deletion is fairly quick. Delete the file immediately and return.
foreach ($fileIds as $id) {
$item = self::load($id);
if ($item === NULL) {
// The item does not exist.
continue;
}
try {
// Passing TRUE to arg1 of deleteInternal causes ancestor sizes to
// be updated after the item is deleted. Deletion automatically
// flushes the item from all caches.
$opCounter = 0;
$item->deleteInternal(TRUE, $opCounter, TRUE, (-1));
unset($item);
}
catch (ExecutionTimeLimitException | MemoryLimitException $e) {
// An execution time or memory limit has been exceeded.
//
// This is VERY unlikely. We're deleting a file.
// Only a few entities have been loaded and this doesn't take
// much time or space.
//
// The exception is thrown AFTER the item has been deleted, so
// just continue and hope for the best.
}
catch (\Exception $e) {
// deleteInternal does not throw any known exceptions.
// If it gets one, it has already been logged.
}
}
unset($fileIds);
if (empty($folderIds) === TRUE) {
// There are no folders to delete. Release the lock and move on.
//
// UNLOCK ROOT FOLDER TREE.
self::releaseRootOperationLock($rootId);
continue;
}
//
// Delete folders.
// ---------------
// When items are folders, there may be children, who may have children,
// and so on in a potentially huge folder tree. Without issuing queries,
// we cannot know right now how big that folder tree is. We must assume
// it is big enough that trying to delete it immediately would cause a
// significant delay before responding to the user's request.
//
// To give quick feedback, mark the item to be deleted as hidden.
// This removes it from file/folder lists and looks like the item is gone.
// This takes much less time than recursing through the children.
//
// Afterwards, schedule a task to mark the folder's descendants. This
// takes much less time than deleting those same descendants because it
// does not require entity deletion, field and entity hooks, and logging.
//
// Only after that is done do we schedule a task to actually delete the
// descendants.
$deleteIds = [];
foreach ($folderIds as $id) {
$item = self::load($id);
if ($item === NULL) {
// The item does not exist.
continue;
}
$item->setSystemHidden(TRUE);
$item->clearAccessGrants(self::ANY_USER_ID, FALSE);
$item->save();
$deleteIds[] = (int) $item->id();
//
// Update ancestor sizes.
// ----------------------
// If needed, update ancestor sizes.
if ($item->isRootItem() === FALSE && $item->getSize() > 0) {
$parent = $item->getParentFolder();
if ($parent !== NULL) {
$parent->updateSizeAndAncestors();
unset($parent);
}
}
unset($item);
}
unset($folderIds);
// Garbage collect. Deletion has loaded and released multiple entities.
// Flush them from memory ASAP.
gc_collect_cycles();
//
// Mark and delete descendants.
// ----------------------------
// First, mark all descendants as hidden. Then delete them in a
// second pass.
//
// If we have one root group only and we have time left before we
// need to respond to the user, start the work. Otherwise schedule
// a task to do the work in the future.
//
// Keep root folder tree locks:
//
// - The root of the folder tree where content is being deleted.
//
// This will be unlocked by a future task when the entire delete is done.
$requestingUid = (int) \Drupal::currentUser()->id();
$parameters = [
'deleteIds' => $deleteIds,
'unlockRootId' => $rootId,
];
$started = time();
$comments = 'Start delete phase 1 (hide)';
$executionTime = 0;
if ($nRootGroups === 1 &&
LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskDelete1(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete1',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
}
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('deleted'), NULL));
}
}
/**
* Implements item deletion.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* The caller MUST have locked the root folder tree.
*
* If this item is a file, image, or media item, it is deleted immediately
* along with its wrapped File or Media entity.
*
* If this item is a folder, recursion loops downward through subfolders.
* Folders are marked as "hidden" as they are encountered.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_delete" hook.
*
* <B>Activity log</B>
* This method posts a log message after each item is deleted.
*
* <B>Process locks</B>
* This method does not lock anything. The caller MUST have locked the
* root folder tree.
*
* @param bool $updateAncestorSizes
* (optional, default = FALSE) When TRUE, the item's ancestor folder
* sizes are updated after an item is deleted. During recursion, this
* is always FALSE.
* @param int $opCounter
* A counter that increments each time a load, save, or delete is done,
* and is reset to zero each time memory and execution time limits are
* checked.
* @param bool $interactive
* (optional, default = FALSE) When TRUE, this task is executing in a
* direct response to a user request that is still in progress, and it
* should therefore return fairly quickly. When FALSE, this task is
* executing as a background task and it can take longer without
* impacting interactivity.
* @param int $requestingUid
* (optional, default = current user) The user ID of the user requesting
* the operation. When interactive, this is the current user. When this
* is a background task, this is the original requesting user.
*
* @throws \Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException
* Throws an exception if the execution time has reached its limit.
* @throws \Drupal\foldershare\Entity\Exception\MemoryLimitException
* Throws an exception if memory use has reached its limit.
*/
private function deleteInternal(
bool $updateAncestorSizes,
int &$opCounter,
bool $interactive = FALSE,
int $requestingUid = (-1)) {
if ($requestingUid < 0) {
$requestingUid = self::getCurrentUserId()[0];
}
if ($this->isFolder() === FALSE) {
$this->deleteInternalFile(
$updateAncestorSizes,
$opCounter,
$requestingUid);
}
else {
$this->deleteInternalFolder(
$updateAncestorSizes,
$opCounter,
$interactive,
$requestingUid);
}
// Garbage collect. Deletion has deleted multiple objects. Flush them
// from memory ASAP.
gc_collect_cycles();
if ($opCounter >= self::USAGE_CHECK_INTERVAL) {
if (($interactive === TRUE &&
LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) ||
LimitUtilities::aboveExecutionTimeLimit() === TRUE) {
// Execution time limit has been reached.
throw new ExecutionTimeLimitException();
}
if (LimitUtilities::aboveMemoryUseLimit() === TRUE) {
// Memory usage limit has been reached.
throw new MemoryLimitException();
}
$opCounter = 0;
}
}
/**
* Implements file deletion.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* The caller MUST have locked the root folder tree.
*
* See deleteInternal() for discussion of deletion, hooks, and logging.
*
* This method only handles deletion of file, image, or media FolderShare
* entities. It is presumed that the caller has already checked that the
* entity is one of these before calling this method.
*
* <B>Process locks</B>
* This method does not lock anything. The caller MUST have locked the
* root folder tree.
*
* @param bool $updateAncestorSizes
* (optional, default = FALSE) When TRUE, the item's ancestor folder
* sizes are updated after an item is deleted. During recursion, this
* is always FALSE.
* @param int $opCounter
* A counter that increments each time a load or delete is done, and is
* reset to zero each time memory and execution time limits are checked.
* @param int $requestingUid
* The user ID of the user requesting the operation.
*/
private function deleteInternalFile(
bool $updateAncestorSizes,
int &$opCounter,
int $requestingUid) {
//
// Setup.
// ------
// Pull out some values we need below.
$thisId = $this->id();
$thisName = $this->getName();
$thisKind = $this->getKind();
$isRoot = $this->isRootItem();
$parentId = $this->getParentFolderId();
$size = $this->getSize();
$thisPath = '';
if (ManageLog::isActivityLoggingEnabled() === TRUE) {
$thisPath = $this->getPath();
}
$file = $this->getFile();
$image = $this->getImage();
$media = $this->getMedia();
try {
//
// Delete.
// -------
// Let the parent class delete the object. Deletion automatically
// removes the item from all caches.
parent::delete();
// Increment to count delete.
++$opCounter;
//
// Delete wrapped file or media.
// -----------------------------
// Force deletion of the file/media since it is supposed to only be
// referenced by the wrapper we just deleted.
//
// These may be redundant if these have already been deleted.
if ($file !== NULL) {
// Delete file. Deletion automatically removes the item from
// all caches.
$file->delete();
unset($file);
// Increment to count delete.
++$opCounter;
}
if ($image !== NULL) {
// Delete file. Deletion automatically removes the item from
// all caches.
$image->delete();
unset($image);
// Increment to count delete.
++$opCounter;
}
if ($media !== NULL) {
// Delete media. Deletion automatically removes the item from
// all caches.
$media->delete();
unset($media);
// Increment to count delete.
++$opCounter;
}
}
catch (\Exception $e) {
// Unknown exception. Continue and assume the item is really deleted.
ManageLog::exception($e, $requestingUid);
}
//
// Hook & log.
// -----------
// Announce the deletion.
self::postOperationHook(
'delete',
[
$thisId,
$requestingUid,
]);
ManageLog::activity(
"Deleted @kind '@name' (# @id).\nPath: @path",
[
'@id' => $thisId,
'@kind' => $thisKind,
'@name' => $thisName,
'@path' => $thisPath,
'uid' => $requestingUid,
]);
//
// Update ancestor sizes.
// ----------------------
// If needed, update ancestor sizes.
if ($updateAncestorSizes === TRUE && $isRoot === FALSE && $size > 0) {
$parent = FolderShare::load($parentId);
if ($parent !== NULL) {
$parent->updateSizeAndAncestors();
unset($parent);
// Increment to count parent load.
//
// This really should be incremented by the number of ancestors
// changed, but we don't know that.
++$opCounter;
}
}
}
/**
* Implements recursive folder deletion.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* The caller MUST have locked the root folder tree.
*
* See deleteInternal() for discussion of deletion, hooks, and logging.
*
* This method only handles deletion of folders. It is presumed that the
* caller has already checked that the entity is a folder before calling
* this method.
*
* <B>Process locks</B>
* This method does not lock anything. The caller MUST have locked the
* root folder tree.
*
* @param bool $updateAncestorSizes
* When TRUE, the item's ancestor folder sizes are updated after an
* item is deleted. During recursion, this is always FALSE.
* @param int $opCounter
* A counter that increments each time a load or copy is done, and is
* reset to zero each time memory and execution time limits are checked.
* @param bool $interactive
* When TRUE, this task is executing in a direct response to a user
* request that is still in progress, and it should therefore return
* fairly quickly. When FALSE, this task is executing as a background
* task and it can take longer without impacting interactivity.
* @param int $requestingUid
* The user ID of the user requesting the operation.
*
* @throws \Drupal\foldershare\Entity\Exception\ExecutionTimeLimitException
* Throws an exception if the execution time has reached its limit.
* @throws \Drupal\foldershare\Entity\Exception\MemoryLimitException
* Throws an exception if memory use has reached its limit.
*/
private function deleteInternalFolder(
bool $updateAncestorSizes,
int &$opCounter,
bool $interactive,
int $requestingUid) {
//
// Mark as hidden and clear access.
// --------------------------------
// Marking the folder hidden makes it invisible to users. When invoked
// from the phase 2 delete task, phase 1 has already recursed through
// folders and marked everything hidden. So check first before doing
// it again.
//
// For root items, clearing access grants (for the owner too) blocks
// all access to the folder tree under the root.
//
// Then SAVE the folder. It is not deleted yet. We can't delete it until
// we've deleted the children.
if ($this->isSystemHidden() === FALSE ||
$this->areAccessGrantsCleared() === FALSE) {
$this->setSystemHidden(TRUE);
$this->clearAccessGrants(self::ANY_USER_ID, FALSE);
$this->save();
// Increment to count save.
++$opCounter;
}
//
// Update ancestor sizes.
// ----------------------
// If needed, update ancestor sizes.
if ($updateAncestorSizes === TRUE &&
$this->isRootItem() === FALSE &&
$this->getSize() > 0) {
$parent = $this->getParentFolder();
if ($parent !== NULL) {
$parent->updateSizeAndAncestors();
unset($parent);
// Increment to count parent load.
//
// This really should be incremented by the number of ancestors
// changed, but we don't know that.
++$opCounter;
}
}
//
// Delete children.
// ----------------
// Recurse to delete children. Find all children, including disabled
// and hidden children (actually, all children during a delete should
// have been marked hidden).
$childIds = $this->findChildrenIds(TRUE, TRUE);
foreach ($childIds as $childId) {
$child = self::load($childId);
if ($child === NULL) {
// The child has already been deleted. This can occur if another
// process did the delete first. Silently skip the child.
continue;
}
// Increment to count child load.
++$opCounter;
// Recurse to delete the child. Deletion automatically removes the
// item from all caches.
//
// Below throws an exception when memory use or execution
// time exceed a limit. Let that exception propagate up to the caller
// through any number of recursion levels.
$child->deleteInternal(FALSE, $opCounter, $interactive, $requestingUid);
unset($child);
}
unset($childIds);
//
// Delete.
// -------
// Let the parent class finish deleting the object. There are no more
// children, so it is now safe to delete the folder.
$thisId = $this->id();
$thisName = $this->getName();
$thisKind = $this->getKind();
$thisPath = '';
if (ManageLog::isActivityLoggingEnabled() === TRUE) {
$thisPath = $this->getPath();
}
try {
// Delete this. Deletion automatically removes the item from all caches.
parent::delete();
// Increment to count delete.
++$opCounter;
}
catch (\Exception $e) {
ManageLog::exception($e, $requestingUid);
}
//
// Hook & log.
// -----------
// Announce the deletion.
self::postOperationHook(
'delete',
[
$thisId,
$requestingUid,
]);
ManageLog::activity(
"Deleted @kind '@name' (# @id).\nPath: @path",
[
'@id' => $thisId,
'@kind' => $thisKind,
'@name' => $thisName,
'@path' => $thisPath,
'uid' => $requestingUid,
]);
// Garbage collect. Deletion has removed multiple objects. Flush them
// from memory ASAP.
gc_collect_cycles();
}
/*---------------------------------------------------------------------
*
* Delete all.
*
*---------------------------------------------------------------------*/
/**
* Deletes all items, or just those owned by a user.
*
* <B>This method is intended for use by site administrators.</B>
* It deletes all items, or a specific user's items, and can be used
* as part of deleting a user's account or prior to uninstalling the module.
*
* With the exception of root-level files, this method does not delete
* items immediately. Instead it schedules a task to traverse through
* folder trees and delete descendants. This is necessary because the
* number of items to delete may be very large and the time required may
* exceed the execution time limits of PHP and the web server. Callers
* should therefore not assume that all of the user's content has been
* deleted when this method returns.
*
* <B>Background delete</B>
* File deletion occurs immediately, but folder deletion schedules background
* tasks to traverse the folder tree and delete descendants. This will
* delay completion of the delete to a time in the future that depends upon
* the size of the folder tree being deleted and server load.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_delete" hook as
* items are deleted. When deletion requires scheduled background tasks,
* deletion hooks are called as those tasks are serviced.
*
* <B>Process locks</B>
* This method groups items to delete by their root folder trees, then
* processes each group. Processing locks each group's root folder tree
* for exclusive use during the delete. This will prevent any other edit
* operation from being performed on the same root folder tree until the
* deletion completes. When deletion requires scheduled background tasks,
* unlocking the root folder tree does not occur until the last descendant
* is deleted.
*
* <B>Activity log</B>
* This method posts a log message each time an item is deleted. When
* deletion requires scheduled background tasks, log messages are posted
* as those tasks are serviced.
*
* @param int $uid
* (optional, default = FolderShareInterface::ANY_USER_ID) The user ID
* of the user for whome to delete all content. If the user ID is
* FolderShareInterface::ANY_USER_ID or negative, all content is
* deleted for all users.
*
* @see ::delete()
*
* @internal
* It is tempting to just truncate the folder table, but this
* doesn't give other modules a chance to clean up in response to hooks.
* The primary relevant module is the File module, which keeps reference
* counts for files. But other modules may have added fields to
* folders, and they too need a graceful way of handling deletion.
* So, this method must recurse and delete each item intentionally.
*/
public static function deleteAll(int $uid = self::ANY_USER_ID) {
// ------------------------------------------------------------------
// Special cases:
// - If the $uid is ANY_USER_ID, then the query of roots below gets
// everything. There is no second step.
//
// Actions:
// - A list of all root items owned by the user (or by all users) is
// queried and one by one passed to delete().
//
// - A list of all non-root items in root folder trees not owned by
// the user is queried and passed to deleteMultiple().
// ------------------------------------------------------------------.
//
// Prepare.
// --------
// Try to push the PHP timeout to "unlimited" so that a long operation
// has a better chance to complete. This may not work if the site has
// PHP timeout changes blocked and it does nothing to stop web server
// timeouts.
Environment::setTimeLimit(0);
//
// Delete roots owned by user (or all users).
// ------------------------------------------
// Get a list of all root items owned by the user (or all users)
// and delete them one by one.
//
// While we could pass all of these to deleteMultiple(), it wouldn't
// save any time. deleteMultiple() would have to load them all, find
// that they are all different roots, and then go through the same
// work as delete(). So we just call delete() and void the extra work.
$rootIds = self::findAllRootItemIds($uid);
$nLockExceptions = 0;
if (empty($rootIds) === FALSE) {
foreach ($rootIds as $rootId) {
$item = self::load($rootId);
if ($item === NULL) {
// The item does not exist.
continue;
}
try {
// Delete it. Deletion automatically flushes the item from
// all caches.
$item->delete();
unset($item);
}
catch (LockException $e) {
++$nLockExceptions;
}
// Garbage collect. We've just deleted multiple entities and their
// objects. Flush them from memory ASAP.
gc_collect_cycles();
}
}
if ($uid === self::ANY_USER_ID) {
// When deleting everything for ANY user, the above root ID list
// already includes everything. There is nothing further to do.
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('deleted'), NULL));
}
return;
}
//
// Delete misc owned by user.
// --------------------------
// Get a list of items that are not roots and not within the folder
// trees of the user's roots. These will be individual files and folders
// buried within the folder trees of other users, such as shared content.
//
// Get a list of all root IDs for roots that are NOT owned by the user.
$otherRootIds = array_diff(
self::findAllRootItemIds(FolderShareInterface::ANY_USER_ID),
$rootIds);
if (empty($otherRootIds) === TRUE) {
// There are no other root folder trees. Nothing more to delete.
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('deleted'), NULL));
}
return;
}
// For each root NOT owned by the user, find anything within the root's
// folder tree that IS owned by the user. Delete it. When it is a folder,
// this will delete its contents too. Not much else we can do.
foreach ($otherRootIds as $rootId) {
$item = self::load($rootId);
if ($item === NULL) {
// The item does not exist.
continue;
}
$descendantIds = $item->findDescendantIdsByOwnerId($uid);
if (empty($descendantIds) === FALSE) {
// Delete all of them. Deletion automatically removes items from
// all caches.
self::deleteMultiple($descendantIds);
}
unset($descendantIds);
unset($item);
// Garbage collect. We've just deleted multiple entities and their
// objects. Flush them from memory ASAP.
gc_collect_cycles();
}
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('deleted'), NULL));
}
}
/*---------------------------------------------------------------------
*
* Background task handling.
*
*---------------------------------------------------------------------*/
/**
* Processes a scheduled phase 1 delete task to mark descendants hidden.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B> This method is public so that it can be called
* from the module's scheduled task handler.
*
* A delete task has a list of IDs for entities to delete. Phase 1,
* implemented here, recurses through all descendants to quickly mark
* them hidden. This is done with direct database updates, rather than
* entity loads, locks, sets, saves, and unlocks in order to mark items
* quickly throughout a potentially large folder tree.
*
* When phase 1 is complete, this method schedules a phase 2 task to
* go back through the descendants and delete them.
*
* <B>Process locks</B>
* This method does not acquire or release any locks.
*
* <B>Post-operation hooks</B>
* This method does not call hooks.
*
* <B>Activity log</B>
* This method does not log activity.
*
* @param int $requestingUid
* The user ID of the user that requested the delete. This is ignored.
* @param array $parameters
* The queued task's parameters. This is an associative array with keys:
* - 'deleteIds': the IDs of items to be deleted, recursively.
* - 'unlockRootId': the ID of the root to unlock upon completion.
* @param int $started
* The timestamp of the start date & time for an operation that causes
* a chain of tasks.
* @param string $comments
* A comment on the current task.
* @param int $executionTime
* The accumulated total execution time of the task chain, in seconds.
* @param bool $interactive
* (optional, default = FALSE) When TRUE, this task is executing in a
* direct response to a user request that is still in progress, and it
* should therefore return fairly quickly. When FALSE, this task is
* executing as a background task and it can take longer without
* impacting interactivity.
*
* @see ::delete()
*/
public static function processTaskDelete1(
int $requestingUid,
array $parameters,
int $started,
string $comments,
int $executionTime,
bool $interactive = FALSE) {
//
// Validate.
// ---------
// The parameters array must contain a list of entity IDs and the root ID
// of the folder tree to unlock when the delete is done.
if (isset($parameters['deleteIds']) === FALSE ||
is_array($parameters['deleteIds']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'deleteIds');
return;
}
if (isset($parameters['unlockRootId']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'unlockRootIds');
return;
}
$deleteIds = $parameters['deleteIds'];
$unlockRootId = (int) $parameters['unlockRootId'];
//
// Reschedule full task.
// ---------------------
// As a safety net, reschedule the entire task immediately. This insures
// that if we get a PHP or web server timeout that interrupts the task,
// it will be run again to try and complete it in the near future.
$safetyNetTask = FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskSafetyNetDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete1',
$requestingUid,
[
'deleteIds' => $deleteIds,
'unlockRootId' => $unlockRootId,
],
$started,
'Safety-net requeue',
$executionTime);
//
// Prepare.
// --------
// Flush all FolderShare entities from caches everywhere. Below we
// modify the database directly in order to set the hidden flag ASAP.
// Once changed, all cached copies of modified items are out of date.
//
// We'd prefer to only flush cached items that are actually changed,
// but getting such a list can be slow if the folder tree to delete
// is large. And we don't know if it is large. The safe quick choice,
// then, is to clear the entire cache.
CacheUtilities::flushAllEntityCaches(self::ENTITY_TYPE_ID);
$beginTime = time();
//
// Mark descendants hidden.
// ------------------------
// Use database updates to mark all descendants as hidden as quickly
// as possible.
//
// For a huge folder tree, this may be interrupted by a PHP or web
// server timeout. The safety net task scheduled above will try again.
// Since the queries used skip subtrees that are already hidden, a
// repeated run will find fewer things to change and go quicker.
// Over repeated runs, this will eventually complete.
foreach ($deleteIds as $id) {
try {
self::setDescendantsSystemHidden($id);
}
catch (\Exception $e) {
ManageLog::exception($e, $requestingUid);
}
}
// Flush all FolderShare entities from caches everywhere. Above we
// modified the database directly in order to set the hidden flag ASAP.
// Once changed, all cached copies are out of date.
CacheUtilities::flushAllEntityCaches(self::ENTITY_TYPE_ID);
//
// Delete descendants.
// -------------------
// Now that all descendants are marked hidden, delete them.
//
// If we have time left before we need to respond to the user, start
// the work. Otherwise schedule a task to do the work in the future.
$parameters = [
'deleteIds' => $deleteIds,
'unlockRootId' => $unlockRootId,
];
$comments = 'Start of delete phase 2 (delete)';
$executionTime += (time() - $beginTime);
if (($interactive === TRUE &&
LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) ||
LimitUtilities::aboveExecutionTimeLimit() === FALSE) {
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
self::processTaskDelete2(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskContinuationDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete2',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
}
}
/**
* Processes a scheduled phase 2 delete task to delete descendants.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B> This method is public so that it can be called
* from the module's scheduled task handler.
*
* A delete task has a list of IDs for entities to delete. Deletion
* recurses through folder entities, deleting child files before deleting
* the parent folder.
*
* <B>Process locks</B>
* This method releases the root folder tree lock acquired when the task
* was started.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_deleted" hook for
* each item deleted.
*
* <B>Activity log</B>
* If the site has enabled logging of operations, this method posts a
* log message for each item deleted.
*
* @param int $requestingUid
* The user ID of the user that requested the delete. This is ignored.
* @param array $parameters
* The queued task's parameters. This is an associative array with keys:
* - 'deleteIds': the IDs of items to be deleted, recursively.
* - 'unlockRootId': the ID of the root to unlock upon completion.
* @param int $started
* The timestamp of the start date & time for an operation that causes
* a chain of tasks.
* @param string $comments
* A comment on the current task.
* @param int $executionTime
* The accumulated total execution time of the task chain, in seconds.
* @param bool $interactive
* (optional, default = FALSE) When TRUE, this task is executing in a
* direct response to a user request that is still in progress, and it
* should therefore return fairly quickly. When FALSE, this task is
* executing as a background task and it can take longer without
* impacting interactivity.
*
* @see ::delete()
*/
public static function processTaskDelete2(
int $requestingUid,
array $parameters,
int $started,
string $comments,
int $executionTime,
bool $interactive = FALSE) {
//
// Validate.
// ---------
// The parameters array must contain a list of entity IDs.
if (isset($parameters['deleteIds']) === FALSE ||
is_array($parameters['deleteIds']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'deleteIds');
return;
}
if (isset($parameters['unlockRootId']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'unlockRootId');
return;
}
$deleteIds = $parameters['deleteIds'];
$unlockRootId = (int) $parameters['unlockRootId'];
//
// Reschedule full task.
// ---------------------
// As a safety net, reschedule the entire task immediately. This insures
// that if we get a PHP or web server timeout that interrupts the task,
// it will be run again to try and complete it in the near future.
$safetyNetTask = FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskSafetyNetDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete2',
$requestingUid,
[
'deleteIds' => $deleteIds,
'unlockRootId' => $unlockRootId,
],
$started,
'Safety-net requeue',
$executionTime);
//
// Prepare.
// --------
// Garbage collect and initialize.
$beginTime = time();
$opCounter = 0;
gc_collect_cycles();
//
// Delete descendants recursively.
// -------------------------------
// Delete each item, if it hasn't already been deleted.
foreach ($deleteIds as $deleteIndex => $id) {
$item = FolderShare::load((int) $id);
if ($item === NULL) {
// The item has already been deleted by another process.
continue;
}
// Increment to count load.
++$opCounter;
try {
// Passing TRUE to below asks that it update parent folder
// sizes. This is only needed for the top-most items being deleted.
// Deletion removes all of the deleted entities from all caches.
$item->deleteInternal(TRUE, $opCounter, $interactive, $requestingUid);
}
catch (ExecutionTimeLimitException | MemoryLimitException $e) {
// An execution time or memory limit has been exceeded.
//
// This is our chance to gracefully handle a condition where
// the execution time or memory use is reaching its configured
// limits. If we do nothing, we will hit that limit and the
// process will crash with a nasty message. The safety net task
// will remain and be serviced by the next process and continue
// the operation. But that nasty crash message will look bad
// and worry admins. It could also have interrupted something
// and left content in a corrupted state.
//
// Instead, when we near a limit, gracefully stop what we are
// doing and return. We'll schedule a continuation task that
// will be serviced by the next process and continue the operation.
//
// DO NOT release the final root lock. The task is not done yet.
//
// Schedule continuation task. Execution has already unset
// entries in the $deleteIds array as they've been deleted.
$reason = ($e instanceof ExecutionTimeLimitException) ?
'time limit' : 'memory use limit';
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskContinuationDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskDelete2',
$requestingUid,
[
'deleteIds' => $deleteIds,
'unlockRootId' => $unlockRootId,
],
$started,
"Continuation due to $reason after $opCounter ops",
$executionTime + (time() - $beginTime));
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
return;
}
unset($item);
unset($deleteIds[$deleteIndex]);
}
// UNLOCK ROOT FOLDER TREE.
self::releaseRootOperationLock($unlockRootId);
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
}
}
