foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationDeleteTrait.php

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

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

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