You are here

content_lock.module in Content locking (anti-concurrent editing) 8

Content lock - Main functions of the module.

File

content_lock.module
View source
<?php

/**
 * @file
 * Content lock - Main functions of the module.
 */
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\LocalRedirectResponse;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
use Drupal\Component\Utility\Html;
use Drupal\user\Entity\User;

/**
 * Implements hook_help().
 */
function content_lock_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.content_lock':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Prevents multiple users from trying to edit a single node simultaneously to prevent edit conflicts.') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_entity_type_build().
 */
function content_lock_entity_type_build(array &$entity_types) {
  foreach ($entity_types as &$entity_type) {
    if ($entity_type instanceof ContentEntityTypeInterface) {
      if (!$entity_type
        ->hasHandlerClass('break_lock_form')) {
        $entity_type
          ->setHandlerClass('break_lock_form', '\\Drupal\\content_lock\\Form\\EntityBreakLockForm');
      }
    }
  }
}

/**
 * Implements hook_hook_info().
 */
function content_lock_hook_info() {
  $hooks = [
    'content_lock_locked',
    'content_lock_release',
    'content_lock_entity_lockable',
  ];
  return array_fill_keys($hooks, [
    'group' => 'content_lock',
  ]);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function content_lock_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (!$form_state
    ->getFormObject() instanceof EntityFormInterface) {
    return;
  }

  /** @var \Drupal\core\Entity\ContentEntityInterface $entity */
  $entity = $form_state
    ->getFormObject()
    ->getEntity();
  $entity_type = $entity
    ->getEntityTypeId();
  $user = Drupal::currentUser();

  // Check if we must lock this entity.

  /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
  $lock_service = \Drupal::service('content_lock');
  $form_op = $form_state
    ->getFormObject()
    ->getOperation();
  if (!$lock_service
    ->isLockable($entity, $form_op)) {
    return;
  }

  // We act only on edit form, not for a creation of a new entity.
  if (!is_null($entity
    ->id())) {
    foreach ([
      'submit',
      'publish',
    ] as $key) {
      if (isset($form['actions'][$key])) {
        $form['actions'][$key]['#submit'][] = 'content_lock_form_submit';
      }
    }

    // This hook function is called twice, first when the form loads
    // and second when the form is submitted.
    // Only perform set and check for lock on initial form load.
    $userInput = $form_state
      ->getUserInput();
    if (!empty($userInput)) {
      return;
    }
    if ($lock_service
      ->isJsLock($entity_type)) {
      $form['#attached']['library'][] = 'content_lock/drupal.content_lock.lock_form';
      $form['#attached']['drupalSettings']['content_lock'] = [
        Html::cleanCssIdentifier($form_id) => [
          'lockUrl' => Url::fromRoute('content_lock.create_lock.' . $entity_type, [
            'entity' => $entity
              ->id(),
            'langcode' => $entity
              ->language()
              ->getId(),
            'form_op' => $form_op,
          ], [
            'query' => [
              'destination' => Drupal::request()
                ->getRequestUri(),
            ],
          ])
            ->toString(),
        ],
      ];
      $form['actions']['#attributes']['class'][] = 'content-lock-actions';

      // If moderation state is in use also disable corresponding buttons.
      if (isset($form['moderation_state'])) {
        $form['moderation_state']['#attributes']['class'][] = 'content-lock-actions';
      }
      return;
    }

    // We lock the content if it is currently edited by another user.
    if (!$lock_service
      ->locking($entity
      ->id(), $entity
      ->language()
      ->getId(), $form_op, $user
      ->id(), $entity_type)) {
      $form['#disabled'] = TRUE;

      // Do not allow deletion, publishing, or unpublishing if locked.
      foreach ([
        'delete',
        'publish',
        'unpublish',
      ] as $key) {
        if (isset($form['actions'][$key])) {
          unset($form['actions'][$key]);
        }
      }

      // If moderation state is in use also disable corresponding buttons.
      if (isset($form['moderation_state'])) {
        unset($form['moderation_state']);
      }
    }
    else {

      // ContentLock::locking() returns TRUE if the content is locked by the
      // current user. Add an unlock button only for this user.
      $form['actions']['unlock'] = $lock_service
        ->unlockButton($entity_type, $entity
        ->id(), $entity
        ->language()
        ->getId(), $form_op, \Drupal::request()->query
        ->get('destination'));
    }
  }
}

/**
 * Submit handler for content_lock.
 */
function content_lock_form_submit($form, FormStateInterface $form_state) {

  // Signals editing is finished; remove the lock.
  $user = \Drupal::currentUser();

  /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
  $lock_service = \Drupal::service('content_lock');

  /** @var \Drupal\core\Entity\ContentEntityInterface $entity */
  $entity = $form_state
    ->getFormObject()
    ->getEntity();

  // If the user submitting owns the lock, release it.
  $lock_service
    ->release($entity
    ->id(), $entity
    ->language()
    ->getId(), $form_state
    ->getFormObject()
    ->getOperation(), $user
    ->id(), $entity
    ->getEntityTypeId());

  // We need to redirect to the canonical page after saving it. If not, we
  // stay on the edit form and we re-lock the entity.
  if (!$form_state
    ->getRedirect() || $form_state
    ->getRedirect() && $entity
    ->hasLinkTemplate('edit-form') && $entity
    ->toUrl('edit-form')
    ->toString() == $form_state
    ->getRedirect()
    ->toString()) {
    $form_state
      ->setRedirectUrl($entity
      ->toUrl());
  }
}

/**
 * Implements hook_entity_predelete().
 *
 * Check if the entity attempting to be deleted is locked and prevent deletion.
 */
function content_lock_entity_predelete(EntityInterface $entity) {
  $entity_id = $entity
    ->id();
  $entity_type = $entity
    ->getEntityTypeId();

  /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
  $lock_service = \Drupal::service('content_lock');
  if (!$lock_service
    ->isLockable($entity)) {
    return;
  }
  $data = $lock_service
    ->fetchLock($entity_id, NULL, $entity
    ->language()
    ->getId(), $entity_type);
  if ($data !== FALSE) {
    $current_user = \Drupal::currentUser();

    // If the entity is locked, and current user is not the lock's owner,
    // set a message and stop deletion.
    if ($current_user
      ->id() !== $data->uid) {
      $lock_user = User::load($data->uid);
      $message = t('@entity cannot be deleted because it was locked by @user since @time.', [
        '@entity' => $entity
          ->label(),
        '@user' => $lock_user
          ->getDisplayName(),
        '@time' => \Drupal::service('date.formatter')
          ->formatInterval(REQUEST_TIME - $data->timestamp),
      ]);
      \Drupal::messenger()
        ->addWarning($message);
      $url = Url::fromRoute('entity.' . $entity_type . '.canonical', [
        $entity_type => $entity_id,
      ])
        ->toString();
      $redirect = new LocalRedirectResponse($url);
      $redirect
        ->send();
      exit(0);
    }
  }
}

/**
 * Implements hook_views_data().
 */
function content_lock_views_data() {

  // Define the return array.
  $data = [];
  $data['content_lock']['table']['group'] = t('Content lock');
  $data['content_lock']['table']['provider'] = 'content_lock';
  $data['content_lock']['table']['join'] = [
    'users_field_data' => [
      'left_field' => 'uid',
      'field' => 'uid',
    ],
  ];
  $types = \Drupal::configFactory()
    ->get('content_lock.settings')
    ->get("types");
  $content_lock = \Drupal::service('content_lock');
  foreach (array_filter($types) as $type => $value) {
    $definition = \Drupal::entityTypeManager()
      ->getDefinition($type);
    $data['content_lock']['table']['join'][$definition
      ->getDataTable()] = [
      'left_field' => $definition
        ->getKey('id'),
      'field' => 'entity_id',
      'extra' => [
        [
          'field' => 'entity_type',
          'value' => $type,
        ],
      ],
    ];
    if ($content_lock
      ->isTranslationLockEnabled($type)) {
      $data['content_lock']['table']['join'][$definition
        ->getDataTable()]['extra'][] = [
        'left_field' => $definition
          ->getKey('langcode'),
        'field' => 'langcode',
      ];
    }
    $data['content_lock'][$definition
      ->getKey('id')] = [
      'title' => t('@type locked', [
        '@type' => $definition
          ->getLabel(),
      ]),
      'help' => t('The @type being locked.', [
        '@type' => $definition
          ->getLabel(),
      ]),
      'relationship' => [
        'base' => $definition
          ->getDataTable(),
        'base field' => $definition
          ->getKey('id'),
        'id' => 'standard',
        'label' => t('@type locked', [
          '@type' => $definition
            ->getLabel(),
        ]),
      ],
    ];
  }
  $data['content_lock']['uid'] = [
    'title' => t('Lock owner'),
    'help' => t('The user locking the node.'),
    'relationship' => [
      'base' => 'users_field_data',
      'base field' => 'uid',
      'id' => 'standard',
      'label' => t('Lock owner'),
    ],
  ];
  $data['content_lock']['timestamp'] = [
    'title' => t('Lock Date/Time'),
    'help' => t('Timestamp of the lock'),
    'field' => [
      'id' => 'date',
      'click sortable' => TRUE,
    ],
    'sort' => [
      'id' => 'date',
    ],
    'filter' => [
      'id' => 'date',
    ],
  ];
  $data['content_lock']['langcode'] = [
    'title' => t('Lock Language'),
    'help' => t('Language of the lock'),
    'field' => [
      'id' => 'language',
    ],
    'sort' => [
      'id' => 'standard',
    ],
    'filter' => [
      'id' => 'language',
    ],
    'argument' => [
      'id' => 'language',
    ],
    'entity field' => 'langcode',
  ];
  $data['content_lock']['form_op'] = [
    'title' => t('Lock Form Operation'),
    'help' => t('Form operation of the lock'),
    'field' => [
      'id' => 'standard',
    ],
    'sort' => [
      'id' => 'standard',
    ],
    'filter' => [
      'id' => 'string',
    ],
    'argument' => [
      'id' => 'string',
    ],
  ];
  $data['content_lock']['is_locked'] = [
    'real field' => 'timestamp',
    'title' => t('Is Locked'),
    'help' => t('Whether the node is currently locked'),
    'field' => [
      'id' => 'boolean',
      'click sortable' => TRUE,
    ],
    'sort' => [
      'id' => 'content_lock_sort',
    ],
    'filter' => [
      'id' => 'content_lock_filter',
    ],
  ];

  // Break link.
  $data['content_lock']['break'] = [
    'title' => t('Break link'),
    'help' => t('Link to break the content lock.'),
    'field' => [
      'id' => 'content_lock_break_link',
      'real field' => 'entity_id',
    ],
  ];
  return $data;
}

/**
 * Implements hook_entity_operation().
 */
function content_lock_entity_operation(EntityInterface $entity) {
  $operations = [];

  /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */
  $lock_service = \Drupal::service('content_lock');
  if ($lock_service
    ->isLockable($entity)) {
    $lock = $lock_service
      ->fetchLock($entity
      ->id(), NULL, NULL, $entity
      ->getEntityTypeId());
    $user = \Drupal::currentUser();
    if ($lock && $user
      ->hasPermission('break content lock')) {
      $entity_type = $entity
        ->getEntityTypeId();
      $route_parameters = [
        'entity' => $entity
          ->id(),
        'langcode' => $lock_service
          ->isTranslationLockEnabled($entity_type) ? $entity
          ->language()
          ->getId() : LanguageInterface::LANGCODE_NOT_SPECIFIED,
        'form_op' => '*',
      ];
      $url = 'content_lock.break_lock.' . $entity
        ->getEntityTypeId();
      $operations['break_lock'] = [
        'title' => t('Break lock'),
        'url' => Url::fromRoute($url, $route_parameters),
        'weight' => 50,
      ];
    }
  }
  return $operations;
}

/**
 * Implements hook_theme().
 */
function content_lock_theme() {
  return [
    'content_lock_settings_entities' => array(
      'render element' => 'element',
    ),
  ];
}

/**
 * Prepares variables for content lock entity settings templates.
 *
 * Default template: content-lock-settings-entities.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: An associative array containing the properties of the element.
 *     Properties used: #title.
 */
function template_preprocess_content_lock_settings_entities(&$variables) {
  $element = $variables['element'];
  $header = [
    [
      'data' => $element['bundles']['#title'],
      'class' => [
        'bundle',
      ],
    ],
    [
      'data' => t('Configuration'),
      'class' => [
        'operations',
      ],
    ],
  ];
  $rows = [];
  foreach (Element::children($element['bundles']) as $bundle) {
    $rows[$bundle] = [
      'data' => [
        [
          'data' => $element['bundles'][$bundle],
          'class' => [
            'bundle',
          ],
        ],
      ],
      'class' => [],
    ];
    if ($bundle == '*') {
      $rows[$bundle]['data'][] = [
        'data' => $element['settings'],
        'class' => [
          'operations',
        ],
      ];
    }
    else {
      $rows[$bundle]['data'][] = [
        'data' => t('Uses "all" settings'),
        'class' => [
          'operations',
        ],
      ];
      $rows[$bundle]['class'][] = 'bundle-settings';
    }
  }
  $variables['title'] = $element['#title'];
  $variables['build'] = [
    '#header' => $header,
    '#rows' => $rows,
    '#type' => 'table',
  ];
}