content_lock.module in Content locking (anti-concurrent editing) 8
Same filename and directory in other branches
Content lock - Main functions of the module.
File
content_lock.moduleView 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',
];
}
Functions
Name | Description |
---|---|
content_lock_entity_operation | Implements hook_entity_operation(). |
content_lock_entity_predelete | Implements hook_entity_predelete(). |
content_lock_entity_type_build | Implements hook_entity_type_build(). |
content_lock_form_alter | Implements hook_form_FORM_ID_alter(). |
content_lock_form_submit | Submit handler for content_lock. |
content_lock_help | Implements hook_help(). |
content_lock_hook_info | Implements hook_hook_info(). |
content_lock_theme | Implements hook_theme(). |
content_lock_views_data | Implements hook_views_data(). |
template_preprocess_content_lock_settings_entities | Prepares variables for content lock entity settings templates. |