foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationMoveTrait.php
src/Entity/FolderShareTraits/OperationMoveTrait.php
<?php
namespace Drupal\foldershare\Entity\FolderShareTraits;
use Drupal\foldershare\ManageLog;
use Drupal\foldershare\Settings;
use Drupal\foldershare\Utilities\FormatUtilities;
use Drupal\foldershare\Utilities\LimitUtilities;
use Drupal\foldershare\FolderShareInterface;
use Drupal\foldershare\Entity\FolderShareScheduledTask;
use Drupal\foldershare\Entity\Exception\LockException;
use Drupal\foldershare\Entity\Exception\ValidationException;
/**
* Move FolderShare entities.
*
* This trait includes methods to move FolderShare entities and place
* them in a folder or at the root level.
*
* <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 OperationMoveTrait {
/*---------------------------------------------------------------------
*
* Move to root.
*
*---------------------------------------------------------------------*/
/**
* {@inheritdoc}
*/
public function moveToRoot(
string $newName = '',
bool $allowRename = FALSE) {
// ------------------------------------------------------------------
// 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 already at the root level and no new name is given,
// do nothing.
//
// - If the item is already at the root level and a new name is given,
// redirect to rename().
//
// - If the item is owned by another user, it is moved to the OWNER's
// root list, not the current user's. It is up to the caller to have
// decided if this is valid (admins can do this, while regular users
// should not).
//
// Errors:
// - The new name is illegal.
// - The new name is in use in the root list and renaming is not allowed.
//
// Actions:
// - If the item is a file: Lock item, lock original root, lock root list,
// set name, clear parent and root, unlock root list, update old
// parent size, unlock original root, unlock item.
//
// - If the item is a folder: Lock item, lock original root, lock root list,
// set name, clear parent and root, clear access grants, set disabled,
// unlock root list, update old parent size, unlock original root, &
// schedule task. The task recurses through the item setting root IDs
// then unlock item.
//
// ------------------------------------------------------------------
//
// Validate.
// ---------
// If this item is already a root, then this is really a rename,
// which is handled separately.
if ($this->isRootItem() === TRUE) {
if (empty($newName) === TRUE) {
// Move to same location with same name. Do nothing.
return;
}
$this->rename($newName);
return;
}
//
// Check name legality.
// --------------------
// If there is a new name, throw an exception if it is not suitable.
if (empty($newName) === FALSE) {
// The checkName() function throws an exception if the name is too
// long or uses illegal characters.
$this->checkName($newName);
}
else {
$newName = $this->getName();
}
//
// Lock this item's folder tree AS IF it were a root.
// --------------------------------------------------
// This item is about to be made a root. Before it is, lock it's
// folder tree so that there is no gap between the move and locking it.
//
// Since this lock will be the last one to be released, it is also
// important that it be the first to be acquired so that we don't get
// race conditions.
//
// 1. LOCK THIS ITEM AS ROOT FOLDER TREE.
if (self::acquireRootOperationLock($this->id()) === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), $this->getName()));
}
//
// Lock original root's folder tree.
// ---------------------------------
// The item is currently in another root's folder tree. Lock it so that
// all other operations that might interfere with the move are blocked.
//
// 2. LOCK ORIGINAL ROOT FOLDER TREE.
$originalRootId = $this->getRootItemId();
if (self::acquireRootOperationLock($originalRootId) === FALSE) {
// 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE.
self::releaseRootOperationLock($this->id());
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), $this->getName()));
}
//
// Lock the root list.
// -------------------
// Lock the root list before checking if this item has a name collision
// in the root list.
//
// Everything on a root list is owned by a user. It is not possible for
// an item to be on one user's root list, but owned by another. This
// means a move-to-root can only move the item to the item's OWNER's
// root list. Moving it to the current user's root list, when the owner
// is not the current user, would require changing ownership too. And
// that is not what this method does.
//
// If moving an item owned by another to that owner's root list is not
// what was intended, it should be detected and blocked before calling
// this method. An admin, for instance, could reasonably be allowed to
// move another user's file or folder to that user's root list. But a
// normal user probably shouldn't be able to do that.
//
// 3. LOCK OWNER'S ROOT LIST.
$ownerId = $this->getOwnerId();
if (self::acquireUserRootListLock($ownerId) === FALSE) {
// 2. UNLOCK ORIGINAL ROOT FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
// 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE.
self::releaseRootOperationLock($this->id());
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), $this->getName()));
}
//
// Check name.
// -----------
// If renaming is not allowed, check if the name is already in use in
// the root list and abort if it is.
//
// If renaming is allowed, create a new unique name in the root list.
if ($allowRename === FALSE) {
if (empty(self::findAllRootItemIds($ownerId, $newName)) === FALSE) {
// 3. UNLOCK OWNER'S ROOT LIST.
self::releaseUserRootListLock($ownerId);
// 2. UNLOCK ORIGINAL ROOT FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
// 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE.
self::releaseRootOperationLock($this->id());
throw new ValidationException(
self::getStandardRenameFirstExceptionMessage($newName));
}
}
else {
$rootNames = self::findAllRootItemNames($ownerId);
$newName = self::createUniqueName($rootNames, $newName);
if ($newName === FALSE) {
// This is very very unlikely because creating a unique name tries
// repeatedly to append a number until it gets to something unique.
throw new ValidationException(
self::getStandardCannotCreateUniqueNameExceptionMessage('move'));
}
unset($rootNames);
}
//
// Decide if a task will be needed.
// --------------------------------
// If the item being moved is:
// - A folder.
// - With children.
//
// Then we'll need to schedule a task.
$taskNeeded = ($this->isFolder() === TRUE &&
$this->findNumberOfChildren() > 0);
//
// Update parent and root IDs.
// ---------------------------
// Update this item, clearing the parent and root IDs to make it a root.
// Set the name (which may or may not be new). Clear access grants, but
// leave defaults for the current owner.
//
// These changes, when saved, provide immediate feedback to the user. The
// item will now show up in the user's root list and not in the file/folder
// list of its old parent folder.
$oldParentId = $this->getParentFolderId();
$this->clearParentFolderId();
$this->clearRootItemId();
$this->clearAccessGrants();
$this->setSystemDisabled($taskNeeded);
$this->setName($newName);
$this->save();
// If this item is a file, image, or media object, change the underlying
// item's name too.
$this->renameWrappedFile($newName);
//
// Unlock user's root list.
// ------------------------
// The item is moved so name collisions are no longer an issue.
// Unlock the user's root list.
//
// 3. UNLOCK OWNER'S ROOT LIST.
self::releaseUserRootListLock($ownerId);
//
// Update ancestor sizes.
// ----------------------
// Update parent ancestor sizes.
$oldParent = self::load($oldParentId);
if ($oldParent !== NULL) {
$oldParent->updateSizeAndAncestors();
}
//
// Unlock original root's folder tree.
// -----------------------------------
// Changes are done for the original folder tree so unlock.
//
// 2. UNLOCK ORIGINAL ROOT FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
//
// Hook & log.
// -----------
// Note the change, even though descendants haven't been updated yet.
$requestingUid = self::getCurrentUserId()[0];
self::postOperationHook(
'move',
[
$this,
$oldParent,
NULL,
$requestingUid,
]);
ManageLog::activity(
"Moved @kind '@name' (# @id) to top level.",
[
'@id' => $this->id(),
'@kind' => $this->getKind(),
'@name' => $this->getName(),
'entity' => $this,
'uid' => $requestingUid,
]);
if ($oldParent !== NULL) {
unset($oldParent);
}
// Garbage collect.
gc_collect_cycles();
if ($taskNeeded === FALSE) {
// No scheduled task is needed.
//
// 1. UNLOCK THIS ITEM AS ROOT FOLDER TREE.
self::releaseRootOperationLock($this->id());
return;
}
//
// Update descendants.
// -------------------
// Finishing the move requires updating the root ID of all descendants.
//
// 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 moved item, which is now a root, and its folder tree.
//
// This will be unlocked by the task when the entire move is done.
$parameters = [
'updateIds' => [(int) $this->id()],
];
$started = time();
$comments = 'Start move to root';
$executionTime = 0;
if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskMoveToRoot(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
}
/**
* Moves multiple items to the root.
*
* Each item's current root folder tree, and the item's own folder tree,
* are both locked at the start of the operation. This will prevent any
* other edit operation from being performed on either folder tree. After
* each item is moved to become a root item, the original root folder tree
* is unlocked. The lock on the item's own folder tree remains until all
* descendants have been updated and the move completes.
*
* Each item's parent and root IDs are updated to move it into the root list.
* Each item is then given default access grants that give the user, and
* only the user, access.
*
* If an item is a folder, a background task is scheduled to complete the
* move by recursively traversing through the folder's descendants to set
* each one's root ID. After all descendants have been updated, the root
* folder tree is unlocked. Because the move executes as a background task,
* completion of the move will occur after this method returns and at a
* time in the future that depends upon the size of the folder tree being
* moved and server load.
*
* System hidden and disabled items are also affected.
*
* <B>Background move</B>
* File moves occur immediately, but folder moves schedule background
* tasks to traverse the folder tree and update descendants. This will
* delay completion of the move 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_move" hook after
* each item is moved. The hook is not called for an item's descendants
* since they have not moved and remain descendants of the original item.
*
* <B>Process locks</B>
* This method locks each item's original root folder tree, and the item's
* own folder tree for exclusive use during the move. This will prevent
* any other edit operation from being performed on the same folder trees
* until the move completes. The original root folder tree lock is released
* as soon as each item has moved to the root list. When moves require
* scheduled background tasks, unlocking each item's own root folder tree
* does not occur until the last descendant is updated.
*
* <B>Activity log</B>
* This method posts a log message after each item is moved. Log messages
* are not posted as the item's descendants are updated since they have
* not moved and remain descendants of the original item.
*
* @param int[] $ids
* An array of integer FolderShare entity IDs to move. Invalid IDs
* are silently skipped.
* @param bool $allowRename
* (optional, default = FALSE) When FALSE, each item retains its same
* name as it is moved into the user's root list. If there is already an
* item with the same name there, an exception is thrown. When TRUE, item
* names may be adjusted to make them unique if there is an item with
* the same name in the user's root list.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
* @throws \Drupal\foldershare\Entity\Exception\ValidationException
* Throws an exception if a name is already in use in the user's root list.
*
* @see ::moveToRoot()
*/
public static function moveToRootMultiple(
array $ids,
bool $allowRename = FALSE) {
// ------------------------------------------------------------------
// Each item can be:
// - A file or folder.
// - At the root level or in a subfolder.
// - Owned by the current user or another.
//
// Typical use:
// - All of the items are from a user interface selection. That selection
// constrains them all to be children of the same parent folder. This
// will put them all in the same root folder tree.
//
// Special cases:
// - If the item is already at the root level, do nothing with it.
//
// - If the item is owned by another user, it is moved to the OWNER's
// root list, not the current user's. It is up to the caller to have
// decided if this is valid (admins can do this, while regular users
// should not).
//
// Errors:
// - The name is in use in the root list and renaming is not allowed.
//
// Actions:
// - All items are sorted into groups with a shared root. In typical use,
// there will be just one shared root. The root lists of all owners
// are locked.
//
// - Check all names or create unique names. This is done with the OWNER's
// root list for each item, not the current user's root list (though the
// current user is often the only owner involved).
//
// - For all files and folders in the same root group: Lock the shared
// root, set names, set parents and roots, set disabled (if a folder),
// update old parent size (if any), and unlock shared root.
//
// - After all groups: Unlock all root lists and schedule a task if there
// are any descendants to update. The task recurses through
// the items setting root IDs, enables the item, then unlocks the item's
// root folder tree.
//
// ------------------------------------------------------------------.
if (empty($ids) === TRUE) {
// Nothing to move.
return;
}
$requestingUid = self::getCurrentUserId()[0];
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->moveToRoot('', $allowRename);
unset($item);
return;
}
//
// Group IDs by root and validate.
// -------------------------------
// The IDs given could be from scattered locations. Group them by
// their current roots so that root folder locks can be done efficiently.
//
// Items in a root group may not all be owned by the same user.
//
// Along the way, skip anything that is already a root and collect a
// list of owner IDs. Below we'll have to lock the root list for each
// of these owners.
$items = self::loadMultiple($ids);
$reducedItems = [];
$rootGroups = [];
$ownerIds = [];
foreach ($items as $item) {
if ($item === NULL) {
// The item does not exist.
continue;
}
if ($item->isRootItem() === TRUE) {
// The item is already a root.
unset($item);
continue;
}
$reducedItems[] = $item;
$ownerIds[] = $item->getOwnerId();
$rootId = $item->getRootItemId();
$rootGroups[$rootId][] = $item;
}
if (empty($rootGroups) === TRUE) {
// Nothing to do.
return;
}
$ownerIds = array_unique($ownerIds);
$items = $reducedItems;
//
// Lock owner root lists.
// ----------------------
// For each owner, lock the root list. We need these locked while
// we check for name collisions and optionally renaming items.
//
// 1. LOCK OWNER ROOT LISTS.
foreach ($ownerIds as $index => $ownerId) {
if (self::acquireUserRootListLock($ownerId) === FALSE) {
// Failed to get lock. Back out any prior root list locks.
//
// 1. UNLOCK OWNER ROOT LISTS.
foreach ($ownerIds as $i => $o) {
if ($i >= $index) {
break;
}
self::releaseUserRootListLock($o);
}
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
}
//
// Check names.
// ------------
// If renaming is not allowed, check if the name is already in use in
// the owner's root list and abort if it is.
//
// If renaming is allowed, create a new unique name for each item,
// checking the owner's root list to create each one.
//
// Note that we check the OWNER's root list, not the current user's
// root list (which is often the same as the owner). A move to root of
// an item not owned by the current user goes to the owner's root list,
// so that's where we need to check for name uniqueness.
//
// Start by collecting the root names for each of the owners.
$rootNamesByOwner = [];
foreach ($ownerIds as $ownerId) {
$rootNamesByOwner[$ownerId] = self::findAllRootItemNames($ownerId);
}
$itemNames = [];
if ($allowRename === FALSE) {
// Check that names do not collide.
foreach ($items as $item) {
$ownerId = $item->getOwnerId();
$rootNames = $rootNamesByOwner[$ownerId];
if (in_array($item->getName(), $rootNames) === TRUE) {
// 1. UNLOCK OWNER ROOT LISTS.
foreach ($ownerIds as $ownerId) {
self::releaseUserRootListLock($ownerId);
}
throw new ValidationException(
self::getStandardRenameFirstExceptionMessage(NULL));
}
// Add the item's name to the name list because it too is a collision
// target for the next items.
$rootNamesByOwner[$ownerId][$item->getName()] = (int) $item->id();
$itemNames[$item->id()] = $item->getName();
}
}
else {
// Create non-colliding names.
foreach ($items as $item) {
$ownerId = $item->getOwnerId();
$rootNames = $rootNamesByOwner[$ownerId];
$newName = self::createUniqueName($rootNames, $item->getName());
if ($newName === FALSE) {
// This is very very unlikely because creating a unique name tries
// repeatedly to append a number until it gets to something unique.
//
// 1. UNLOCK OWNER ROOT LISTS.
foreach ($ownerIds as $ownerId) {
self::releaseUserRootListLock($ownerId);
}
throw new ValidationException(
self::getStandardCannotCreateUniqueNameExceptionMessage('move'));
}
// Add the item's name to the name list because it too is a collision
// target for the next items.
$rootNamesByOwner[$ownerId][$newName] = (int) $item->id();
$itemNames[(int) $item->id()] = $newName;
}
}
unset($rootNamesByOwner);
//
// Loop over root groups.
// ----------------------
// Each root requires its own root folder tree lock as items are moved
// out of that root folder and into the owner's root list.
$updateIds = [];
$nLockExceptions = 0;
foreach ($rootGroups as $originalRootId => $items) {
//
// Lock these items' current root folder tree.
// -------------------------------------------
// Lock the root folder tree these items are coming from so that the
// folder tree cannot be changed during the move.
//
// 2. LOCK ORIGINAL ROOT'S FOLDER TREE.
if (self::acquireRootOperationLock($originalRootId) === FALSE) {
++$nLockExceptions;
$rootGroups[$originalRootId] = NULL;
continue;
}
// Clear the root group entry to save memory.
$rootGroups[$originalRootId] = NULL;
//
// Update parent and root IDs.
// ---------------------------
// Update these items, clearing the parent and root IDs (since the item
// is becoming a root). Clear the access grants and set them to defaults
// for a root. Update the name, if needed.
foreach ($items as $index => $item) {
// This item is becoming a root. If it is a folder, we'll have more
// work to do to update descendants, so lock it.
//
// 3. LOCK THIS ITEM AS ROOT FOLDER TREE, if it is a folder.
if ($item->isFolder() === TRUE) {
if (self::acquireRootOperationLock($item->id()) === FALSE) {
++$nLockExceptions;
unset($item);
$items[$index] = NULL;
continue;
}
}
//
// Decide if a task will be needed.
// --------------------------------
// If the item being moved is:
// - A folder.
// - With children.
//
// Then we'll need to schedule a task.
$taskNeeded = ($item->isFolder() === TRUE &&
$item->findNumberOfChildren() > 0);
$items[$index] = NULL;
$oldParentId = $item->getParentFolderId();
$item->clearParentFolderId();
$item->clearRootItemId();
$item->clearAccessGrants();
$item->setSystemDisabled($taskNeeded);
$item->setName($itemNames[(int) $item->id()]);
$item->save();
// If the item is a file, image, or media object change the underlying
// item's name too.
$item->renameWrappedFile($itemNames[(int) $item->id()]);
//
// Update ancestor sizes.
// ----------------------
// If the item was not a root, it has a parent. Update the parent's
// ancestor sizes now that the item has moved.
$oldParent = self::load($oldParentId);
if ($oldParent !== NULL) {
$oldParent->updateSizeAndAncestors();
}
//
// Hook & log.
// -----------
// Note the change, even though descendants haven't been updated yet.
self::postOperationHook(
'move',
[
$item,
$oldParent,
NULL,
$requestingUid,
]);
ManageLog::activity(
"Moved @kind '@name' (# @id) to top level.",
[
'@id' => $item->id(),
'@kind' => $item->getKind(),
'@name' => $item->getName(),
'entity' => $item,
'uid' => $requestingUid,
]);
if ($taskNeeded === TRUE) {
// A scheduled task is needed to update descendant root IDs.
$updateIds[] = (int) $item->id();
}
if ($oldParent !== NULL) {
unset($oldParent);
}
unset($item);
// Garbage collect.
gc_collect_cycles();
}
unset($items);
//
// Unlock original root's folder tree.
// -----------------------------------
// Modifications to the old one are now done. Everything has been
// moved out of it. Unlock it.
//
// 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
}
unset($rootGroups);
// Unlock owner's root lists.
// -------------------------
// Everything has been moved to the owner's root list and, possibly,
// given new names. We no longer need to keep the root lists locked.
//
// 1. UNLOCK OWNER ROOT LISTS.
foreach ($ownerIds as $ownerId) {
self::releaseUserRootListLock($ownerId);
}
if (empty($updateIds) === TRUE) {
// No descendants to process. Done.
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
return;
}
//
// Update descendants.
// -------------------
// Finishing the move requires updating the root ID of all descendants.
//
// 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:
//
// - Every item being updated is a root with a root folder tree lock.
//
// This will be unlocked by the task when the entire move is done.
$parameters = [
'updateIds' => $updateIds,
];
$started = time();
$comments = 'Start move to root';
$executionTime = 0;
if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskMoveToRoot(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
}
/*---------------------------------------------------------------------
*
* Move to folder.
*
*---------------------------------------------------------------------*/
/**
* {@inheritdoc}
*/
public function moveToFolder(
FolderShareInterface $destination = NULL,
string $newName = '',
bool $allowRename = FALSE) {
// ------------------------------------------------------------------
// 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 no destination is given, assume a move to the root and redirect
// to moveToRoot().
//
// - If the item is owned by another user, it remaines owned by them.
// It is up to the caller to insure the current user has permission
// to move the item.
//
// - If the item is not a descendant of the destination, then the item's
// ancestor root needs to be locked along with the destination's.
//
// - If the item is a descendant of the destination, then locking just
// the destination's root is sufficient.
//
// - If the item is at the root level, then no root list lock is needed
// because we don't need to check for root list collisions on a move
// out of the root list. We only need to check on moves/copies/adds
// in to the root list.
//
// Errors:
// - The destination is not a folder.
// - The destination is a descendant of this item (a circular move).
// - The new name is illegal.
// - The new name is in use in the destination folder and renaming is
// not allowed.
//
// Actions:
// - If the item is a file: Lock destination's root, lock item's root (if
// different), set name, set parent and root, update old parent
// size (if any), unlock old item's root (if not same as destination),
// unlock destination's root.
//
// - If the item is a folder: Lock destination's root, lock item's root (if
// different), set name, clear parent and root, set access grants,
// set disabled, update old parent size, unlock old item's root (if not
// same as destination), and schedule task. The task recurses through
// the item setting root IDs then unlocks the destination's root.
//
// ------------------------------------------------------------------
//
// Validate.
// ---------
// Confirm that the destination is a folder and that it is not a
// descendant of this item.
if ($destination === NULL) {
return $this->moveToRoot($newName, $allowRename);
}
if ($destination->isFolder() === FALSE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'@method was called with a move destination that is not a folder.',
[
'@method' => __METHOD__,
])));
}
$destinationId = (int) $destination->id();
if ($this->isRootItem() === FALSE) {
// If the destination is this item's parent, then this is really
// a rename, which is handled separately.
if ($destinationId === $this->getParentFolderId()) {
if (empty($newName) === TRUE) {
// The item is already in the destination.
return;
}
$this->rename($newName);
return;
}
}
// If the destination is a descendant of this item, then the move is
// circular.
if ($destinationId === (int) $this->id() ||
$this->isAncestorOfFolderId($destinationId) === TRUE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'The item "@name" cannot be moved into one of its own descendants.',
[
'@name' => $this->getName(),
])));
}
$requestingUid = self::getCurrentUserId()[0];
//
// Check name legality.
// --------------------
// If there is a new name, make sure it is legal.
if (empty($newName) === FALSE) {
// The checkName() function throws an exception if the name is too
// long or uses illegal characters.
$this->checkName($newName);
}
else {
$newName = $this->getName();
}
//
// Lock the destination root's folder tree.
// ----------------------------------------
// The destination is about to be modified by the addition of this item.
// Get a lock on that root folder so that it cannot change out from under
// this move.
//
// Since this lock will be the last one to be released, it is important
// that it be the first to be acquired so that we don't get race conditions.
//
// 1. LOCK DESTINATION ROOT FOLDER TREE.
$destinationRootId = $destination->getRootItemId();
if (self::acquireRootOperationLock($destinationRootId) === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), $this->getName()));
}
//
// Lock the item root's folder tree, if different from destination's.
// ------------------------------------------------------------------
// If this item is a root item, then this will lock the item itself.
//
// If this item is in the same root folder tree as the destination,
// then that root folder tree is already locked. Do nothing more.
//
// Otherwise this item is in some other root folder tree. Lock it.
$originalRootId = $this->getRootItemId();
if ($originalRootId !== $destinationRootId) {
// 2. LOCK ORIGINAL ROOT'S FOLDER TREE.
if (self::acquireRootOperationLock($originalRootId) === FALSE) {
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), $this->getName()));
}
}
//
// Check name.
// -----------
// If renaming is not allowed, check if the name is already in use in
// the destination folder and abort if it is.
//
// If renaming is allowed, create a new unique name in the destination
// folder.
if ($allowRename === FALSE) {
if (self::findNamedChildId($destinationId, $newName) !== FALSE) {
if ($originalRootId !== $destinationRootId) {
// 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
}
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
throw new ValidationException(
self::getStandardRenameFirstExceptionMessage($newName));
}
}
else {
$siblingNames = $destination->findChildrenNames();
$newName = self::createUniqueName($siblingNames, $newName);
if ($newName === FALSE) {
// This is very very unlikely because creating a unique name tries
// repeatedly to append a number until it gets to something unique.
throw new ValidationException(
self::getStandardCannotCreateUniqueNameExceptionMessage('move'));
}
unset($siblingNames);
}
//
// Decide if a task will be needed.
// --------------------------------
// If the item being moved is:
// - A folder.
// - With children.
// - And the root folder has changed.
//
// Then we'll need to schedule a task.
$taskNeeded = ($this->isFolder() === TRUE &&
$originalRootId !== $destinationRootId &&
$this->findNumberOfChildren() > 0);
//
// Update parent and root IDs.
// ---------------------------
// Update this item, swapping in the destination ID as the new parent,
// and the new root ID (which might not be different).
//
// Since the item is now in a subfolder, clear all access grants.
//
// If the item is a folder and it was not in the same root folder tree
// as the destination (e.g. it was a root or in another folder tree),
// then we'll have to update descendants below. Mark the item disabled
// until that is done.
$oldParentId = $this->getParentFolderId();
$this->setParentFolderId($destinationId);
$this->setRootItemId($destinationRootId);
$this->clearAccessGrants(self::ANY_USER_ID, FALSE);
$this->setSystemDisabled($taskNeeded);
$this->setName($newName);
$this->save();
// If the item is a file, image, or media object change the underlying
// item's name too.
$this->renameWrappedFile($newName);
//
// Update ancestor sizes.
// ----------------------
// Update destination ancestor sizes to include the addition of the
// moved item. Since the moved item already has a size field set,
// the update can be correct even though we haven't finished updating
// descendants.
//
// If the item was not a root, then it had an old parent folder.
// Update ancestor sizes for that old parent to reflect the loss of
// the moved item.
$destination->updateSizeAndAncestors();
if ($oldParentId < 0) {
// No parent. Item was a root.
$oldParent = NULL;
}
else {
$oldParent = self::load($oldParentId);
if ($oldParent !== NULL) {
$oldParent->updateSizeAndAncestors();
}
}
//
// Unlock original root's folder tree, if different from now.
// ----------------------------------------------------------
// If the item moved within the same root folder tree as the destination,
// then do nothing. No additional root folder tree lock was needed.
//
// Otherwise, the item was a root or it was in some other root folder tree.
// Unlock that tree since we are now done with it.
if ($originalRootId !== $destinationRootId) {
// 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
}
//
// Hook & log.
// -----------
// Note the change, even though descendants haven't been updated yet.
self::postOperationHook(
'move',
[
$this,
$oldParent,
$destination,
$requestingUid,
]);
ManageLog::activity(
"Moved @kind '@name' (# @id) to '@destName' (# @destId).",
[
'@id' => $this->id(),
'@kind' => $this->getKind(),
'@name' => $this->getName(),
'@destId' => $destination->id(),
'@destName' => $destination->getName(),
'entity' => $this,
'uid' => $requestingUid,
]);
if ($oldParent !== NULL) {
unset($oldParent);
}
unset($destination);
if ($taskNeeded === FALSE) {
// No scheduled task is needed.
//
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
return;
}
//
// Update descendants.
// -------------------
// Finishing the move requires updating the root ID of all descendants.
//
// 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 destination folder's root folder tree.
//
// This will be unlocked by the task when the entire move is done.
$parameters = [
'updateIds' => [(int) $this->id()],
'unlockRootId' => $destinationRootId,
];
$started = time();
$comments = 'Start move to folder';
$executionTime = 0;
if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskMoveToFolder(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
}
/**
* Moves multiple items to a folder.
*
* Each item's current root folder tree, and the destination root folder tree,
* are both locked at the start of the operation. This will prevent any
* other edit operation from being performed on either folder tree. After
* each item is moved, the original root folder tree is unlocked. The lock
* on the destination root folder tree remains until all descendants have
* been updated and the move completes.
*
* Each item's parent and root IDs are updated to move it into the
* destination. Access grants are cleared.
*
* If an item is a folder, a background task is scheduled to complete the
* move by recursively traversing through the folder's descendants to set
* each one's root ID. After all descendants have been updated, the
* destination root folder tree is unlocked. Because the move executes as
* a background task, completion of the move will occur after this method
* returns and at a time in the future that depends upon the size of the
* folder tree being moved and server load.
*
* System hidden and disabled items are also affected.
*
* <B>Background move</B>
* File moves occur immediately, but folder moves schedule background
* tasks to traverse the folder tree and update descendants. This will
* delay completion of the move 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_move" hook after
* each item is moved. The hook is not called for an item's descendants
* since they have not moved and remain descendants of the original item.
*
* <B>Process locks</B>
* This method locks each item's original root folder tree, and the
* destination root folder tree for exclusive use during the move. This
* will prevent any other edit operation from being performed on the same
* folder trees until the move completes. The original root folder tree
* lock is released as soon as each item has moved to the destination.
* When moves require scheduled background tasks, unlocking the destination
* root folder tree does not occur until the last descendant is updated.
*
* <B>Activity log</B>
* This method posts a log message after each item is moved. Log messages
* are not posted as the item's descendants are updated since they have
* not moved and remain descendants of the original item.
*
* @param int[] $ids
* An array of integer FolderShare entity IDs to move. Invalid IDs
* are silently skipped.
* @param \Drupal\foldershare\FolderShareInterface $destination
* (optional, default = NULL = move to the root list) The destination
* folder for the move. When NULL, the moved items are added to the
* user's root list.
* @param bool $allowRename
* (optional, default = FALSE) When FALSE, each item retains its same
* name as it is moved into the destination. If there is already an item
* with the same name there, an exception is thrown. When TRUE, item
* names may be adjusted to make them unique if there is an item with
* the same name in the destination.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
* @throws \Drupal\foldershare\Entity\Exception\ValidationException
* Throws an exception if a name is already in use in the destination.
*
* @see ::moveToFolder()
*/
public static function moveToFolderMultiple(
array $ids,
FolderShareInterface $destination = NULL,
bool $allowRename = FALSE) {
// ------------------------------------------------------------------
// Each item can be:
// - A file or folder.
// - At the root level or in a subfolder.
// - Owned by the current user or another.
//
// Typical use:
// - All of the items are from a user interface selection. That selection
// constrains them all to be roots or all children of the same parent
// folder.
//
// Special cases:
// - If the item is already in the destination, do nothing with it.
//
// - If the item is owned by another user, it is moved to the OWNER's
// root list, not the current user's. It is up to the caller to have
// decided if this is valid (admins can do this, while regular users
// should not).
//
// Errors:
// - The destination is not a folder.
// - The destination is a descendant of an item (a circular move).
// - An item's name is in use in the destination folder and renaming is
// not allowed.
//
// Actions:
// - All items are sorted into groups with a shared root. In typical use,
// there will be just one shared root. The destination is locked.
//
// - Check all names or create unique names.
//
// - For all files and folders in the same root group: Lock the shared
// root (if different from destination root), set names, set parents
// and roots, set disabled (if a folder), update old parent size
// (if any), and unlock shared root (if different from destination root).
//
// - After all groups: Update destination size and schedule a task if there
// are any descendants to update. The task recurses through the items
// setting root IDs, enables the item, then unlocks the destination root.
//
// ------------------------------------------------------------------.
if (empty($ids) === TRUE) {
// Nothing to move.
return;
}
if ($destination === NULL) {
// If there is no destination, move to root.
self::moveToRootMultiple($ids, $allowRename);
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->moveToFolder($destination, '', $allowRename);
unset($item);
return;
}
$requestingUid = self::getCurrentUserId()[0];
//
// Validate.
// ---------
// The destination must be a folder.
if ($destination->isFolder() === FALSE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'@method was called with a move destination that is not a folder.',
[
'@method' => __METHOD__,
])));
}
//
// Group IDs by root and validate.
// -------------------------------
// The IDs given could be from scattered locations. Group them by
// their current roots so that root folder locks can be done efficiently.
//
// Along the way, check for circular moves.
$items = self::loadMultiple($ids);
$reducedItems = [];
$rootGroups = [];
$destinationId = (int) $destination->id();
foreach ($items as $item) {
if ($item === NULL) {
// The item does not exist.
continue;
}
if ($item->getParentFolderId() === $destinationId) {
// The item is already in the destination.
unset($item);
continue;
}
// If the destination is a descendant of this item, then the move is
// circular.
if ($destinationId === (int) $item->id() ||
$item->isAncestorOfFolderId($destinationId) === TRUE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'The item "@name" cannot be moved into one of its own descendants.',
[
'@name' => $item->getName(),
])));
}
$rootId = $item->getRootItemId();
$rootGroups[$rootId][] = $item;
$reducedItems[] = $item;
}
if (empty($rootGroups) === TRUE) {
// Nothing to move.
return;
}
$items = $reducedItems;
//
// Lock the destination root's folder tree.
// ----------------------------------------
// The destination is about to be modified by the addition of these items.
// Get a lock on that root folder so that it cannot change out from under
// this move.
//
// 1. LOCK DESTINATION ROOT FOLDER TREE.
$destinationRootId = $destination->getRootItemId();
if (self::acquireRootOperationLock($destinationRootId) === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
//
// Check names.
// ------------
// If renaming is not allowed, check if the name is already in use in
// the destination and abort if it is.
//
// If renaming is allowed, create a new unique name for each item,
// checking the destination to create each one.
$itemNames = [];
$siblingNames = $destination->findChildrenNames();
if ($allowRename === FALSE) {
foreach ($items as $item) {
if (in_array($item->getName(), $siblingNames) === TRUE) {
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
throw new ValidationException(
self::getStandardRenameFirstExceptionMessage($item->getName()));
}
// Add the new name to the list of sibling names since it is now
// taken and cannot be reused by the next item.
$siblingNames[$item->getName()] = (int) $item->id();
$itemNames[(int) $item->id()] = $item->getName();
}
}
else {
// Create non-colliding names.
foreach ($items as $item) {
$newName = self::createUniqueName($siblingNames, $item->getName());
if ($newName === FALSE) {
// This is very very unlikely because creating a unique name tries
// repeatedly to append a number until it gets to something unique.
//
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
throw new ValidationException(
self::getStandardCannotCreateUniqueNameExceptionMessage('move'));
}
// Add the new name to the list of sibling names since it is now
// taken and cannot be reused by the next item.
$siblingNames[$newName] = (int) $item->id();
$itemNames[(int) $item->id()] = $newName;
}
}
unset($siblingNames);
//
// Loop over root groups.
// ----------------------
// Each root requires its own root folder tree lock as the item is moved
// out of that root folder and into the destination root's folder tree.
$nLockExceptions = 0;
$updateIds = [];
foreach ($rootGroups as $originalRootId => $items) {
//
// Lock these item's current root folder tree, if changing.
// --------------------------------------------------------
// If these items are moving from one root folder tree to another, lock
// the root folder tree they are coming from so that it cannot be changed
// during the move.
if ($originalRootId !== $destinationRootId) {
// 2. LOCK ORIGINAL ROOT'S FOLDER TREE.
if (self::acquireRootOperationLock($originalRootId) === FALSE) {
++$nLockExceptions;
$rootGroups[$originalRootId] = NULL;
continue;
}
}
// Unset the root group entry to save memory.
$rootGroups[$originalRootId] = NULL;
//
// Update parent and root IDs.
// ---------------------------
// Update these items, swapping in the destination ID as the new parent,
// and the new root ID (which might not be different). Since the items
// are definitely not roots now (though they may have been before), clear
// the access grants because they are no longer relevant.
//
// Disable items that are folders that are not moving within the same
// root folder tree. Such items have descendants that need their root IDs
// updated, so keep the folder disabled until that update is done.
foreach ($items as $item) {
if ($item === NULL) {
continue;
}
//
// Decide if a task will be needed.
// --------------------------------
// If the item being moved is:
// - A folder.
// - With children.
// - And the root folder has changed.
//
// Then we'll need to schedule a task.
$taskNeeded = ($item->isFolder() === TRUE &&
$originalRootId !== $destinationRootId &&
$item->findNumberOfChildren() > 0);
$oldParentId = $item->getParentFolderId();
$item->setParentFolderId($destinationId);
$item->setRootItemId($destinationRootId);
$item->clearAccessGrants(self::ANY_USER_ID, FALSE);
$item->setSystemDisabled($taskNeeded);
$item->setName($itemNames[(int) $item->id()]);
$item->save();
// If the item is a file, image, or media object change the underlying
// item's name too.
$item->renameWrappedFile($itemNames[(int) $item->id()]);
//
// Update ancestor sizes.
// ----------------------
// Update parent (if any) ancestor sizes.
if ($oldParentId < 0) {
$oldParent = NULL;
}
else {
$oldParent = self::load($oldParentId);
if ($oldParent !== NULL) {
$oldParent->updateSizeAndAncestors();
}
}
if ($taskNeeded === TRUE) {
// The item is a folder and it is moving from one root folder tree
// to another (and thus the folder's descendants need their
// root IDs updated). Add the folder's ID to the to-be-updated list.
$updateIds[] = (int) $item->id();
}
//
// Hook & log.
// -----------
// Note the change, even though descendants haven't been updated yet.
self::postOperationHook(
'move',
[
$item,
$oldParent,
$destination,
$requestingUid,
]);
ManageLog::activity(
"Moved @kind '@name' (# @id) to '@destName' (# @destId).",
[
'@id' => $item->id(),
'@kind' => $item->getKind(),
'@name' => $item->getName(),
'@destId' => $destination->id(),
'@destName' => $destination->getName(),
'entity' => $item,
'uid' => $requestingUid,
]);
if ($oldParent !== NULL) {
unset($oldParent);
}
}
unset($items);
// Garbage collect.
gc_collect_cycles();
//
// Unlock original root's folder tree, if different from now.
// ----------------------------------------------------------
// If the item has moved from one root folder tree to another,
// modifications to the old one are now done. Unlock it.
if ($originalRootId !== $destinationRootId) {
// 2. UNLOCK ORIGINAL ROOT'S FOLDER TREE.
self::releaseRootOperationLock($originalRootId);
}
}
unset($rootGroups);
//
// Update ancestor sizes.
// ----------------------
// Update destination ancestor sizes.
$destination->updateSizeAndAncestors();
if (empty($updateIds) === TRUE) {
// No descendants to process. Done.
//
// 1. UNLOCK DESTINATION ROOT FOLDER TREE.
self::releaseRootOperationLock($destinationRootId);
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
return;
}
unset($destination);
//
// Update descendants.
// -------------------
// Finishing the move requires updating the root ID of all descendants.
//
// 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 destination folder's root folder tree.
//
// This will be unlocked by the task when the entire move is done.
$parameters = [
'updateIds' => $updateIds,
'unlockRootId' => $destinationRootId,
];
$started = time();
$comments = 'Start move to folder';
$executionTime = 0;
if (LimitUtilities::aboveResponseExecutionTimeLimit() === FALSE) {
self::processTaskMoveToFolder(
$requestingUid,
$parameters,
$started,
$comments,
$executionTime,
TRUE);
}
else {
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskInitialDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder',
$requestingUid,
$parameters,
$started,
$comments,
$executionTime);
}
if ($nLockExceptions !== 0) {
throw new LockException(
self::getStandardLockExceptionMessage(t('moved'), NULL));
}
}
/*---------------------------------------------------------------------
*
* Background task handling.
*
*---------------------------------------------------------------------*/
/**
* Processes a scheduled move-to-root task to update move 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 move task is provided a list of root IDs for moved entities. For
* each one, the task recurses through folder entities, updating the root
* ID of children before updating the folder itself.
*
* <B>Process locks</B>
* This method releases the root folder tree lock acquired for each item
* when the task was started.
*
* <B>Post-operation hooks</B>
* No hooks are called.
*
* <B>Activity log</B>
* No log messages are posted.
*
* @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:
* - 'updateIds': the IDs of entities to recurse downwards from and
* set their root IDs. All of them are roots and are unlocked after
* the update is done.
* @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 ::setRootItemId()
* @see ::moveToFolder()
* @see ::moveToFolderMultiple()
* @see ::moveToRoot()
* @see ::moveToRootMultiple()
*/
public static function processTaskMoveToRoot(
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['updateIds']) === FALSE ||
is_array($parameters['updateIds']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'updateIds');
return;
}
$updateIds = $parameters['updateIds'];
//
// 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::processTaskMoveToRoot',
$requestingUid,
[
'updateIds' => $updateIds,
],
$started,
'Safety-net requeue',
$executionTime);
//
// Prepare.
// --------
// Garbage collect and initialize.
$beginTime = time();
$opCounter = 0;
gc_collect_cycles();
//
// Update descendants with new root ID.
// ------------------------------------
// Loop over all descendants that DO NOT have the correct root ID
// already and update their root IDs.
//
// 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 already have the right
// root ID, a repeated run will find fewer things to change and go quicker.
// Over repeated runs, this will eventually complete.
foreach ($updateIds as $updateIndex => $id) {
$item = self::load($id);
if ($item === NULL) {
// Item does not exist.
continue;
}
// Increment to count load.
++$opCounter;
// In a move to root, the queued item is now a root and all of its
// descendants need to use it as their root.
$rootId = (int) $item->id();
// Find all descendants that do NOT have the correct root ID.
$descendantIds = $item->findDescendantIdsByRootId($rootId, FALSE);
// Loop over these. Load each one and change it.
foreach ($descendantIds as $descendantId) {
$descendant = self::load($descendantId);
if ($descendant === NULL) {
// Descendant does not exist.
continue;
}
// Increment to count load.
++$opCounter;
// Set and save.
$descendant->setRootItemId($rootId);
$descendant->save();
// Increment to count save.
++$opCounter;
unset($descendant);
// Check memory and execution time usage every so often.
if ($opCounter >= self::USAGE_CHECK_INTERVAL) {
$reschedule = FALSE;
if (($interactive === TRUE &&
LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) ||
LimitUtilities::aboveExecutionTimeLimit() === TRUE) {
$reschedule = TRUE;
$reason = 'time limit';
}
elseif (LimitUtilities::aboveMemoryUseLimit() === TRUE) {
$reschedule = TRUE;
$reason = 'memory use limit';
}
if ($reschedule === TRUE) {
// 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 task's root lock since we aren't done yet.
//
// Schedule continuation task. Execution has already unset
// entries in the $updateIds list as they were finished.
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskContinuationDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToRoot',
$requestingUid,
[
'updateIds' => $updateIds,
],
$started,
"Continuation due to $reason after $opCounter ops",
$executionTime + (time() - $beginTime));
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
return;
}
$opCounter = 0;
}
}
$item->setSystemDisabled(FALSE);
$item->save();
// Increment to count save.
++$opCounter;
unset($item);
unset($descendantIds);
unset($updateIds[$updateIndex]);
// Garbage collect.
gc_collect_cycles();
// UNLOCK ITEM ROOT FOLDER TREE.
self::releaseRootOperationLock($id);
}
unset($updateIds);
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
}
/**
* Processes a scheduled move-to-folder task to update move 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 move task is provided a list of IDs for moved entities. For each one,
* the task recurses through folder entities, updating the root ID of
* children before updating the folder itself.
*
* <B>Process locks</B>
* This method releases the root folder tree lock acquired when the task
* was started.
*
* <B>Post-operation hooks</B>
* No hooks are called.
*
* <B>Activity log</B>
* No log messages are posted.
*
* @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:
* - 'updateIds': the IDs of entities to recurse downwards from and
* set their root IDs.
* - 'unlockRootId': the ID of the root to unlock upon completion. This
* is also the root ID to set entities to use.
* @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 ::setRootItemId()
* @see ::moveToFolder()
* @see ::moveToFolderMultiple()
* @see ::moveToRoot()
* @see ::moveToRootMultiple()
*/
public static function processTaskMoveToFolder(
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 a new
// root ID.
if (isset($parameters['updateIds']) === FALSE ||
is_array($parameters['updateIds']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'updateIds');
return;
}
if (isset($parameters['unlockRootId']) === FALSE) {
ManageLog::missingTaskParameter(__METHOD__, 'unlockRootId');
return;
}
$updateIds = $parameters['updateIds'];
$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::processTaskMoveToFolder',
$requestingUid,
[
'updateIds' => $updateIds,
'unlockRootId' => $unlockRootId,
],
$started,
'Safety-net requeue',
$executionTime);
//
// Prepare.
// --------
// Garbage collect and initialize.
$beginTime = time();
$opCounter = 0;
gc_collect_cycles();
//
// Update descendants with new root ID.
// ------------------------------------
// Loop over all descendants that DO NOT have the correct root ID
// already and update their root IDs.
//
// 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 already have the right
// root ID, a repeated run will find fewer things to change and go quicker.
// Over repeated runs, this will eventually complete.
foreach ($updateIds as $updateIndex => $id) {
$item = self::load($id);
if ($item === NULL) {
// Item does not exist.
continue;
}
// Increment to count load.
++$opCounter;
// Find all descendants that do NOT have the correct root ID.
$descendantIds = $item->findDescendantIdsByRootId($unlockRootId, FALSE);
// Loop over these. Load each one and change it.
foreach ($descendantIds as $descendantId) {
$descendant = self::load($descendantId);
if ($descendant === NULL) {
// Descendant does not exist.
continue;
}
// Increment to count load.
++$opCounter;
// Set and save.
$descendant->setRootItemId($unlockRootId);
$descendant->save();
// Increment to count save.
++$opCounter;
unset($descendant);
// Check memory and execution time usage every so often.
if ($opCounter >= self::USAGE_CHECK_INTERVAL) {
$reschedule = FALSE;
if (($interactive === TRUE &&
LimitUtilities::aboveResponseExecutionTimeLimit() === TRUE) ||
LimitUtilities::aboveExecutionTimeLimit() === TRUE) {
$reschedule = TRUE;
$reason = 'time limit';
}
elseif (LimitUtilities::aboveMemoryUseLimit() === TRUE) {
$reschedule = TRUE;
$reason = 'memory use limit';
}
if ($reschedule === TRUE) {
// 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 task's root lock since we aren't done yet.
//
// Schedule continuation task. Execution has already unset
// entries in the $updateIds list as they were finished.
FolderShareScheduledTask::createTask(
time() + Settings::getScheduledTaskContinuationDelay(),
'\Drupal\foldershare\Entity\FolderShare::processTaskMoveToFolder',
$requestingUid,
[
'updateIds' => $updateIds,
'unlockRootId' => $unlockRootId,
],
$started,
"Continuation due to $reason after $opCounter ops",
$executionTime + (time() - $beginTime));
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
return;
}
$opCounter = 0;
}
}
$item->setSystemDisabled(FALSE);
$item->save();
// Increment to count save.
++$opCounter;
unset($item);
unset($descendantIds);
unset($updateIds[$updateIndex]);
// Garbage collect.
gc_collect_cycles();
}
unset($updateIds);
// UNLOCK ROOT FOLDER TREE.
self::releaseRootOperationLock($unlockRootId);
// Delete the safety net task.
FolderShareScheduledTask::deleteTask($safetyNetTask);
}
}
