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);
  }
 
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc