NodeAccessRebuild.php

Same filename and directory in other branches
  1. 11.x core/modules/node/src/NodeAccessRebuild.php

Namespace

Drupal\node

File

core/modules/node/src/NodeAccessRebuild.php

View source
<?php

declare (strict_types=1);
namespace Drupal\node;

use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Component\Utility\Environment;
use Drupal\Core\StringTranslation\StringTranslationTrait;

/**
 * Provides methods for checking and rebuilding node access permissions.
 */
class NodeAccessRebuild {
  use StringTranslationTrait;
  public const STATE_KEY = 'node.node_access_needs_rebuild';
  public function __construct(protected readonly StateInterface $state, protected readonly ModuleHandlerInterface $moduleHandler, protected readonly EntityTypeManagerInterface $entityTypeManager, protected readonly NodeGrantDatabaseStorageInterface $grantStorage, protected readonly MessengerInterface $messenger) {
  }
  
  /**
   * Reads the value of a flag for rebuilding the node access grants.
   *
   * When the flag is set, a message is displayed to users with 'access
   * administration pages' permission, pointing to the 'rebuild' confirm form.
   * This can be used as an alternative to direct rebuild calls,
   * allowing administrators to decide when they want to perform the actual
   * (possibly time consuming) rebuild.
   *
   * When unsure if the current user is an administrator, self::rebuild()
   * should be used instead.
   *
   * @return bool
   *   The current value of the flag.
   *
   * @see self::rebuild()
   */
  public function needsRebuild() : bool {
    return $this->state
      ->get(self::STATE_KEY, FALSE);
  }
  
  /**
   * Sets the value of a flag for rebuilding the node access grants.
   *
   * @param bool $rebuild
   *   (optional) Sets the value of the state key to TRUE if TRUE, otherwise
   *   deletes the key. Defaults to TRUE.
   *
   * @see self::rebuild()
   */
  public function setNeedsRebuild(bool $rebuild = TRUE) : void {
    if ($rebuild) {
      $this->state
        ->set(self::STATE_KEY, TRUE);
    }
    else {
      $this->state
        ->delete(self::STATE_KEY);
    }
  }
  
  /**
   * Rebuilds the node access database.
   *
   * This rebuild is occasionally needed by modules that make system-wide
   * changes to access levels. When the rebuild is required by an
   * admin-triggered action (e.g module settings form), calling
   * self::setNeedsRebuild(TRUE) instead of self::rebuild() lets the user
   * perform changes and actually rebuild only once done.
   *
   * @param bool $batch_mode
   *   (optional) Set to TRUE to process in 'batch' mode, spawning processing
   *   over several HTTP requests (thus avoiding the risk of PHP timeout if the
   *   site has a large number of nodes). hook_update_N() and any form submit
   *   handler are safe contexts to use the 'batch mode'. Less decidable cases
   *   (such as calls from hook_user(), hook_taxonomy(), etc.) might consider
   *   using the non-batch mode. Defaults to FALSE. Calling this method
   *   multiple times in the same request with $batch_mode set to TRUE will
   *   only result in one batch set being added.
   *
   * @see self::needsRebuild()
   */
  public function rebuild(bool $batch_mode = FALSE) : void {
    $node_storage = $this->entityTypeManager
      ->getStorage('node');
    $access_control_handler = $this->entityTypeManager
      ->getAccessControlHandler('node');
    // If the rebuild fails to complete, and node_access_needs_rebuild is not
    // set to TRUE, the node_access table is left in an incomplete state.
    // Force node_access_needs_rebuild to TRUE once existing grants are deleted,
    // to signal that the node access table still needs to be rebuilt if this
    // function does not finish.
    $this->setNeedsRebuild(TRUE);
    $access_control_handler->deleteGrants();
    // Only recalculate if the site is using a node_access module.
    if ($this->moduleHandler
      ->hasImplementations('node_grants')) {
      if ($batch_mode) {
        if (!BatchBuilder::isSetIdRegistered(__FUNCTION__)) {
          $batch_builder = (new BatchBuilder())->setTitle($this->t('Rebuilding content access permissions'))
            ->addOperation(static::class . ':batchOperation')
            ->setFinishCallback(static::class . ':batchFinished')
            ->registerSetId(__FUNCTION__);
          batch_set($batch_builder->toArray());
        }
      }
      else {
        // Try to allocate enough time to rebuild node grants
        Environment::setTimeLimit(240);
        // Rebuild newest nodes first so that recent content becomes available
        // quickly.
        $nids = $node_storage->getQuery()
          ->sort('nid', 'DESC')
          ->accessCheck(FALSE)
          ->execute();
        foreach ($nids as $nid) {
          $node_storage->resetCache([
            $nid,
          ]);
          $node = $node_storage->load($nid);
          // To preserve database integrity, only write grants if the node
          // loads successfully.
          if ($node instanceof NodeInterface) {
            $grants = $access_control_handler->acquireGrants($node);
            $this->grantStorage
              ->write($node, $grants);
          }
        }
      }
    }
    else {
      // Not using any node_access modules. Add the default grant.
      $access_control_handler->writeDefaultGrant();
    }
    if (!isset($batch_builder)) {
      $this->messenger
        ->addStatus($this->t('Content permissions have been rebuilt.'));
      $this->setNeedsRebuild(FALSE);
    }
  }
  
  /**
   * Implements callback_batch_operation().
   *
   * Performs batch operation for \Drupal\node\NodeAccessRebuild::rebuild().
   *
   * This is a multistep operation: we go through all nodes by packs of 20. The
   * batch processing engine interrupts processing and sends progress feedback
   * after 1 second execution time.
   *
   * @param array $context
   *   An array of contextual key/value information for rebuild batch process.
   */
  public function batchOperation(array &$context) : void {
    $node_storage = $this->entityTypeManager
      ->getStorage('node');
    if (empty($context['sandbox'])) {
      $context['sandbox']['progress'] = 0;
      $context['sandbox']['current_node'] = 0;
      $context['sandbox']['max'] = $node_storage->getQuery()
        ->accessCheck(FALSE)
        ->count()
        ->execute();
    }
    $limit = 20;
    $nids = $node_storage->getQuery()
      ->condition('nid', $context['sandbox']['current_node'], '>')
      ->sort('nid', 'ASC')
      ->accessCheck(FALSE)
      ->range(0, $limit)
      ->execute();
    $node_storage->resetCache($nids);
    $nodes = $node_storage->loadMultiple($nids);
    /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */
    $access_control_handler = $this->entityTypeManager
      ->getAccessControlHandler('node');
    foreach ($nids as $nid) {
      $node = $nodes[$nid] ?? NULL;
      // To preserve database integrity, only write grants if the node
      // loads successfully.
      if ($node instanceof NodeInterface) {
        $grants = $access_control_handler->acquireGrants($node);
        $this->grantStorage
          ->write($node, $grants);
      }
      $context['sandbox']['progress']++;
      $context['sandbox']['current_node'] = $nid;
    }
    if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
    }
  }
  
  /**
   * Implements callback_batch_finished().
   *
   * Performs post-processing for \Drupal\node\NodeAccessRebuild::rebuild().
   *
   * @param bool $success
   *   A boolean indicating whether the re-build process has completed.
   */
  public function batchFinished(bool $success) : void {
    if ($success) {
      $this->messenger
        ->addStatus($this->t('The content access permissions have been rebuilt.'));
      $this->setNeedsRebuild(FALSE);
    }
    else {
      $this->messenger
        ->addError($this->t('The content access permissions have not been properly rebuilt.'));
    }
  }

}

Classes

Title Deprecated Summary
NodeAccessRebuild Provides methods for checking and rebuilding node access permissions.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.