diff --git a/content_lock.module b/content_lock.module index 8cb4068..875f900 100644 --- a/content_lock.module +++ b/content_lock.module @@ -335,32 +335,6 @@ function content_lock_entity_operation(EntityInterface $entity) { return $operations; } -/** - * Implements hook_user_logout(). - */ -function content_lock_user_logout($account) { - /** @var \Drupal\Core\Session\AccountInterface $account */ - // Only do the database check if the original drupal session manager is used. - // Otherwise its not sure if sessions table has correct data. As it would be - // a common practice to extend the Class, instanceof is not used here! - if (get_class(Drupal::service('session_manager')) == \Drupal\Core\Session\SessionManager::class) { - $query = \Drupal::database() - ->select('sessions'); - $query->condition('uid', $account->id()); - $query = $query->countQuery(); - $session_count = (int) $query->execute()->fetchField(); - } - else { - $session_count = FALSE; - } - // Only remove all locks of user if its the last session of the user. - if ($session_count === 1) { - /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */ - $lock_service = \Drupal::service('content_lock'); - $lock_service->releaseAllUserLocks($account->id()); - } -} - /** * Implements hook_theme(). */ diff --git a/modules/content_lock_timeout/content_lock_timeout.module b/modules/content_lock_timeout/content_lock_timeout.module index d6354e7..90fea24 100644 --- a/modules/content_lock_timeout/content_lock_timeout.module +++ b/modules/content_lock_timeout/content_lock_timeout.module @@ -22,7 +22,7 @@ use Drupal\block_content\BlockContentInterface; function content_lock_timeout_cron() { $config = \Drupal::config('content_lock_timeout.settings'); $timeout_minutes = $config->get('content_lock_timeout_minutes'); - $last_valid_time = time() - 60 * $timeout_minutes; + $last_valid_time = Drupal::time()->getCurrentTime() - 60 * $timeout_minutes; /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */ $lock_service = \Drupal::service('content_lock'); @@ -58,7 +58,7 @@ function content_lock_timeout_entity_prepare_form(EntityInterface $entity, $oper return; } $timeout_minutes = $config->get('content_lock_timeout_minutes'); - $last_valid_time = time() - 60 * $timeout_minutes; + $last_valid_time = Drupal::time()->getCurrentTime() - 60 * $timeout_minutes; /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */ $lock_service = \Drupal::service('content_lock'); @@ -69,7 +69,7 @@ function content_lock_timeout_entity_prepare_form(EntityInterface $entity, $oper // A different user owns the lock. // There is already a lock on this entity. if (!empty($entity->id()) - && is_object($lock = $lock_service->fetchLock($entity->id(), $entity->getEntityTypeId())) + && is_object($lock = $lock_service->fetchLock($entity->id(), $entity->getEntityTypeId(), $operation, $entity->getEntityTypeId())) && $lock->uid != $user->id() && $lock->timestamp < $last_valid_time // Now check a subset of the conditions that content_lock_form_alter() @@ -98,3 +98,37 @@ function content_lock_timeout_entity_prepare_form(EntityInterface $entity, $oper } } } + +/** + * Implements hook_user_logout(). + */ +function content_lock_timeout_user_logout($account) { + /** @var \Drupal\Core\Session\AccountInterface $account */ + + // Only remove locks if there is a timeout given. + $config = \Drupal::config('content_lock_timeout.settings'); + $timeout_minutes = $config->get('content_lock_timeout_minutes'); + if ($timeout_minutes == 0) { + return; + } + + // Only do the database check if the original drupal session manager is used. + // Otherwise its not sure if sessions table has correct data. As it would be + // a common practice to extend the Class, instanceof is not used here! + if (get_class(Drupal::service('session_manager')) == \Drupal\Core\Session\SessionManager::class) { + $query = \Drupal::database() + ->select('sessions'); + $query->condition('uid', $account->id()); + $query = $query->countQuery(); + $session_count = (int) $query->execute()->fetchField(); + } + else { + $session_count = FALSE; + } + // Only remove all locks of user if its the last session of the user. + if ($session_count === 1) { + /** @var \Drupal\content_lock\ContentLock\ContentLock $lock_service */ + $lock_service = \Drupal::service('content_lock'); + $lock_service->releaseAllUserLocks($account->id()); + } +} diff --git a/modules/content_lock_timeout/src/Form/SettingsForm.php b/modules/content_lock_timeout/src/Form/SettingsForm.php index 96f467d..194bb47 100644 --- a/modules/content_lock_timeout/src/Form/SettingsForm.php +++ b/modules/content_lock_timeout/src/Form/SettingsForm.php @@ -68,7 +68,6 @@ class SettingsForm extends ConfigFormBase { parent::submitForm($form, $form_state); $this->config('content_lock_timeout.settings') - ->set('content_lock_timeout', $form_state->getValue('content_lock_timeout')) ->set('content_lock_timeout_minutes', $form_state->getValue('content_lock_timeout_minutes')) ->set('content_lock_timeout_on_edit', $form_state->getValue('content_lock_timeout_on_edit')) ->save(); diff --git a/modules/content_lock_timeout/src/Tests/ContentLockTimeoutTest.php b/modules/content_lock_timeout/src/Tests/ContentLockTimeoutTest.php new file mode 100644 index 0000000..aed050b --- /dev/null +++ b/modules/content_lock_timeout/src/Tests/ContentLockTimeoutTest.php @@ -0,0 +1,167 @@ +setNewDatetimeTimeService(); + + $this->drupalLogin($this->adminUser); + $edit = [ + 'content_lock_timeout_minutes' => 10, + 'content_lock_timeout_on_edit' => 1, + ]; + $this->drupalPostForm('/admin/config/content_lock_timeout/settings', $edit, t('Save configuration')); + + $this->lockService = \Drupal::service('content_lock'); + } + + /** + * Change the service.yml to set own datetime.time service. + * + * @see FunctionalTestSetupTrait::setContainerParameter + */ + protected function setNewDatetimeTimeService() { + $filename = $this->siteDirectory . '/services.yml'; + chmod($filename, 0666); + + // @todo Remove preg_replace() once + // https://github.com/symfony/symfony/pull/25787 is in Symfony 3.4. + $content = file_get_contents($filename); + $content = preg_replace('/:$\n^\s+{\s*}$/m', ': {}', $content); + $services = Yaml::decode($content); + $services['services']['datetime.time'] = [ + 'class' => 'Drupal\content_lock_timeout\Tests\TimeChanger', + 'arguments' => ['@request_stack'], + ]; + file_put_contents($filename, Yaml::encode($services)); + + // Ensure that the cache is deleted for the yaml file loader. + $file_cache = FileCacheFactory::get('container_yaml_loader'); + $file_cache->delete($filename); + $this->rebuildContainer(); + } + + protected function testContentLockNode() { + // We protect the bundle created. + $this->drupalLogin($this->adminUser); + $edit = [ + 'node[bundles][article]' => 1, + ]; + $this->drupalPostForm('admin/config/content/contentlocksettings', $edit, t('Save configuration')); + + $this->doTestForEntity($this->article1); + } + + protected function testContentLockBlock() { + // We protect the bundle created. + $this->drupalLogin($this->adminUser); + $edit = [ + 'block_content[bundles][basic]' => 1, + ]; + $this->drupalPostForm('admin/config/content/contentlocksettings', $edit, t('Save configuration')); + + $this->doTestForEntity($this->block1); + } + + protected function testContentLockTerm() { + // We protect the bundle created. + $this->drupalLogin($this->adminUser); + $edit = [ + 'taxonomy_term[bundles][' . $this->term1->bundle() . ']' => 1, + ]; + $this->drupalPostForm('admin/config/content/contentlocksettings', $edit, t('Save configuration')); + + $this->doTestForEntity($this->term1); + } + + /** + * Run the same tests for node, block and term. + * + * @param \Drupal\Core\Entity\Entity $entity + * + * @throws \Drupal\Core\Entity\EntityMalformedException + */ + protected function doTestForEntity(Entity $entity) { + // We lock article1. + $this->drupalLogin($this->user2); + + $this->lockContentByUser1($entity); + + // Content should be locked + $this->drupalGet($entity->toUrl('edit-form')->toString()); + $this->assertText(t('This content is being edited by the user @name and is therefore locked to prevent other users changes.', [ + '@name' => $this->user1->getDisplayName(), + ])); + + // Jump into future to release lock + \Drupal::time()->setCurrentTime(time() + 60 * 60); + $this->cronRun(); + \Drupal::time()->resetCurrentTime(); + + // Content should be unlocked by cron + $this->assertNoLockOnContent($entity); + $this->drupalGet($entity->toUrl('edit-form')->toString()); + $this->assertText(t('This content is now locked against simultaneous editing.')); + + $this->drupalLogout(); + + // There should be no lock on the content after logout. + $this->assertNoLockOnContent($entity); + + $this->lockContentByUser1($entity); + + $this->drupalLogin($this->user2); + + // Content should be locked + $this->drupalGet($entity->toUrl('edit-form')->toString()); + $this->assertText(t('This content is being edited by the user @name and is therefore locked to prevent other users changes.', [ + '@name' => $this->user1->getDisplayName(), + ])); + + // Jump into the future + \Drupal::time()->setCurrentTime(time() + 60 * 60); + // lock should be release by form prepare + $this->drupalGet($entity->toUrl('edit-form')->toString()); + $this->assertText(t('This content is now locked against simultaneous editing.')); + } + + /** + * Create lock from user 1. + * + * As login/logout is always clearing locks, its only possible over service directly. + * + * @param \Drupal\Core\Entity\Entity $entity + */ + protected function lockContentByUser1(Entity $entity) { + $this->lockService->releaseAllUserLocks($this->user2->id()); + $this->lockService->locking($entity->id(), $entity->language()->getId(), 'edit', $this->user1->id(), $entity->getEntityTypeId()); + $lock = $this->lockService->fetchLock($entity->id(), $entity->language()->getId(), 'edit', $entity->getEntityTypeId()); + $this->assertNotNull($lock, 'Lock present'); + $this->assertEqual($this->user1->label(), $lock->name, 'Lock present for correct user.'); + } + + protected function assertNoLockOnContent(Entity $entity) { + $lock = $this->lockService->fetchLock($entity->id(), $entity->language()->getId(), 'edit', $entity->getEntityTypeId()); + $this->assertFalse($lock, 'No lock present'); + } + +} diff --git a/modules/content_lock_timeout/src/Tests/TimeChanger.php b/modules/content_lock_timeout/src/Tests/TimeChanger.php new file mode 100644 index 0000000..5970d4c --- /dev/null +++ b/modules/content_lock_timeout/src/Tests/TimeChanger.php @@ -0,0 +1,47 @@ +get('time', NULL); + if (!empty($time)) { + return $time; + } + return parent::getCurrentTime(); + } + + /** + * Forward current time to the given timestamp. + * + * @param $time + */ + public function setCurrentTime($time) { + \Drupal::keyValue('time')->set('time', $time); + } + + /** + * Reset the current time to the real time. + */ + public function resetCurrentTime() { + \Drupal::keyValue('time')->delete('time'); + } + + public function getRequestTime() { + $time = \Drupal::keyValue('time')->get('time', NULL); + if (!empty($time)) { + return $time; + } + return parent::getRequestTime(); + } +}