diff -u b/src/Entity/Group.php b/src/Entity/Group.php --- b/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -27,6 +27,7 @@ * ), * bundle_label = @Translation("Group type"), * handlers = { + * "storage" = "Drupal\group\Entity\Storage\GroupStorage", * "view_builder" = "Drupal\group\Entity\ViewBuilder\GroupViewBuilder", * "views_data" = "Drupal\group\Entity\Views\GroupViewsData", * "list_builder" = "Drupal\group\Entity\Controller\GroupListBuilder", @@ -66,6 +67,7 @@ * "add-form" = "/group/add/{group_type}", * "add-page" = "/group/add", * "canonical" = "/group/{group}", + * "revision" = "/group/{group}/revisions/{group_revision}/view", * "collection" = "/admin/group", * "edit-form" = "/group/{group}/edit", * "delete-form" = "/group/{group}/delete" @@ -83,6 +85,7 @@ * Gets the group membership loader. * * @return \Drupal\group\GroupMembershipLoaderInterface + * The group.membership_loader service. */ protected function membershipLoader() { return \Drupal::service('group.membership_loader'); @@ -92,6 +95,7 @@ * Gets the group permission checker. * * @return \Drupal\group\Access\GroupPermissionCheckerInterface + * The group_permission.checker service. */ protected function groupPermissionChecker() { return \Drupal::service('group_permission.checker'); @@ -101,6 +105,7 @@ * Gets the group content storage. * * @return \Drupal\group\Entity\Storage\GroupContentStorageInterface + * The group content storage. */ protected function groupContentStorage() { return $this->entityTypeManager()->getStorage('group_content'); @@ -110,6 +115,7 @@ * Gets the group role storage. * * @return \Drupal\group\Entity\Storage\GroupRoleStorageInterface + * The group role storage. */ protected function groupRoleStorage() { return $this->entityTypeManager()->getStorage('group_role'); only in patch2: unchanged: --- a/group.group.permissions.yml +++ b/group.group.permissions.yml @@ -4,7 +4,22 @@ administer group: description: 'Administer the group, its content and members' restrict access: TRUE view group: - title: 'View group' + title: 'View published group' +view any unpublished group: + title: 'View any unpublished group' +view own unpublished group: + title: 'View own unpublished group' +view group latest version: + title: 'View the latest version' + description: 'Requires the "View any unpublished group" or "View own unpublished group" permission' +view group revisions: + title: 'Revisioning: View revisions' +revert group revisions: + title: 'Revisioning: Revert revisions' + description: 'Requires the "Edit group" permission' +delete group revisions: + title: 'Revisioning: Delete revisions' + description: 'Requires the "Delete group" permission' edit group: title: 'Edit group' description: 'Edit the group information' only in patch2: unchanged: --- a/group.links.task.yml +++ b/group.links.task.yml @@ -35,6 +35,12 @@ group.content: route_name: 'entity.group_content.collection' weight: 15 +group.version_history: + route_name: entity.group.version_history + base_route: entity.group.canonical + title: 'Revisions' + weight: 20 + group_type.edit_form: title: 'Edit' base_route: 'entity.group_type.edit_form' only in patch2: unchanged: --- a/group.post_update.php +++ b/group.post_update.php @@ -8,6 +8,8 @@ use Drupal\group\Entity\GroupType; use Drupal\group\Entity\GroupContentType; use Drupal\user\Entity\Role; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Recalculate group type and group content type dependencies after moving the only in patch2: unchanged: --- a/group.routing.yml +++ b/group.routing.yml @@ -26,6 +26,55 @@ entity.group.leave: _group_permission: 'leave group' _group_member: 'TRUE' +# Revisions routes. +entity.group.version_history: + path: '/group/{group}/revisions' + defaults: + _title: 'Revisions' + _controller: '\Drupal\group\Entity\Controller\GroupController::revisionOverview' + requirements: + _access_group_revision: 'view' + group: \d+ + options: + _group_operation_route: TRUE + +entity.group.revision: + path: '/group/{group}/revisions/{group_revision}/view' + defaults: + _controller: '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision' + _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title' + options: + parameters: + group: + type: entity:group + group_revision: + type: entity_revision:group + requirements: + _access_group_revision: 'view' + group: \d+ + +entity.group.revision_revert_confirm: + path: '/group/{group}/revisions/{group_revision}/revert' + defaults: + _form: '\Drupal\group\Entity\Form\GroupRevisionRevertForm' + _title: 'Revert to earlier revision' + requirements: + _access_group_revision: 'update' + group: \d+ + options: + _group_operation_route: TRUE + +entity.group.revision_delete_confirm: + path: '/group/{group}/revisions/{group_revision}/delete' + defaults: + _form: '\Drupal\group\Entity\Form\GroupRevisionDeleteForm' + _title: 'Delete earlier revision' + requirements: + _access_group_revision: 'delete' + group: \d+ + options: + _group_operation_route: TRUE + # Group type entity routes. # Common entity routes are generated by \Drupal\group\Entity\Routing\GroupTypeRouteProvider. entity.group_type.permissions_form: only in patch2: unchanged: --- a/group.services.yml +++ b/group.services.yml @@ -33,6 +33,16 @@ services: class: 'Drupal\group\Access\GroupContentCreateAnyEntityAccessCheck' tags: - { name: 'access_check', applies_to: '_group_content_create_any_entity_access' } + access_check.group_latest_revision: + class: Drupal\group\Access\GroupLatestRevisionCheck + arguments: ['@content_moderation.moderation_information'] + tags: + - { name: access_check, applies_to: '_group_moderation_latest_version' } + access_check.group.revision: + class: Drupal\group\Access\GroupRevisionAccessCheck + arguments: ['@entity_type.manager'] + tags: + - { name: access_check, applies_to: _access_group_revision } cache_context.group: class: 'Drupal\group\Cache\Context\GroupCacheContext' @@ -85,6 +95,10 @@ services: arguments: ['@config.factory'] tags: - { name: 'event_subscriber' } + group.latest_revision.route_subscriber: + class: Drupal\group\Routing\LatestRevisionRouteSubscriber + tags: + - {name: event_subscriber} group.config_subscriber: class: 'Drupal\group\EventSubscriber\ConfigSubscriber' arguments: ['@entity_type.manager', '@plugin.manager.group_content_enabler'] only in patch2: unchanged: --- /dev/null +++ b/src/Access/GroupLatestRevisionCheck.php @@ -0,0 +1,100 @@ +moderationInfo = $moderation_information; + } + + /** + * Checks that there is a pending revision available. + * + * This checker assumes the presence of an '_entity_access' requirement key + * in the same form as used by EntityAccessCheck. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parametrized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + * + * @see \Drupal\Core\Entity\EntityAccessCheck + */ + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { + // This tab should not show up unless there's a reason to show it. + $entity = $this->loadEntity($route, $route_match); + + if ($this->moderationInfo->hasPendingRevision($entity)) { + // Check the global permissions first. + $access_result = GroupAccessResult::allowedIfHasGroupPermissions($entity, $account, ['view group latest version', 'view any unpublished group']); + if (!$access_result->isAllowed()) { + // Check entity owner access. + $owner_access = GroupAccessResult::allowedIfHasGroupPermissions($entity, $account, ['view group latest version', 'view own unpublished group']);; + $owner_access = $owner_access->andIf((AccessResult::allowedIf($entity instanceof EntityOwnerInterface && ($entity->getOwnerId() == $account->id())))); + $access_result = $access_result->orIf($owner_access); + } + + return $access_result->addCacheableDependency($entity); + } + + return AccessResult::forbidden('No pending revision for moderated entity.')->addCacheableDependency($entity); + } + + /** + * Returns the default revision of the entity this route is for. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parametrized route. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * returns the Entity in question. + * + * @throws \Drupal\Core\Access\AccessException + * An AccessException is thrown if the entity couldn't be loaded. + */ + protected function loadEntity(Route $route, RouteMatchInterface $route_match) { + $entity_type = $route->getOption('_content_moderation_entity_type'); + + if ($entity = $route_match->getParameter($entity_type)) { + if ($entity instanceof EntityInterface) { + return $entity; + } + } + throw new AccessException(sprintf('%s is not a valid entity route. The LatestRevisionCheck access checker may only be used with a route that has a single entity parameter.', $route_match->getRouteName())); + } + +} only in patch2: unchanged: --- /dev/null +++ b/src/Access/GroupRevisionAccessCheck.php @@ -0,0 +1,147 @@ +groupStorage = $entity_type_manager->getStorage('group'); + $this->groupAccess = $entity_type_manager->getAccessControlHandler('group'); + } + + /** + * Checks routing access for the group revision. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * @param int $group_revision + * (optional) The group revision ID. If not specified, but $group is, access + * is checked for that object's revision. + * @param \Drupal\group\Entity\GroupInterface $group + * (optional) A group object. Used for checking access to a group's default + * revision is $group_revision is unspecified. Ignored when $group_revision + * is specified. If neither $group_revision nor $group are specified, then + * access is denied. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(Route $route, AccountInterface $account, $group_revision = NULL, GroupInterface $group = NULL) { + if ($group_revision) { + $group = $this->groupStorage->loadRevision($group_revision); + } + $operation = $route->getRequirement('_access_group_revision'); + return AccessResult::allowedIf($group && $this->checkAccess($group, $account, $operation))->cachePerPermissions()->addCacheableDependency($group); + } + + /** + * Checks group revision access. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group to check. + * @param \Drupal\Core\Session\AccountInterface $account + * A user object representing the user for whom the operation is to be + * performed. + * @param string $op + * (optional) The specific operation being checked. Defaults to 'view'. + * + * @return bool + * TRUE if the operation may be performed, FALSE otherwise. + */ + public function checkAccess(GroupInterface $group, AccountInterface $account, $op = 'view') { + $map = [ + 'view' => 'view group revisions', + 'update' => 'revert group revisions', + 'delete' => 'delete group revisions', + ]; + $bundle = $group->getGroupType()->id(); + + if (!$group || !isset($map[$op])) { + // If there was no group to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + // Statically cache access by revision ID, language code, user account ID, + // and operation. + $langcode = $group->language()->getId(); + $cid = $group->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op; + + if (!isset($this->access[$cid])) { + // Perform basic permission checks first. + if (!$group->hasPermission($map[$op], $account)) { + $this->access[$cid] = FALSE; + return FALSE; + } + + // If the revisions checkbox is selected for the group type, display the + // revisions tab. + $bundle_entity_type = $group->getEntityType()->getBundleEntityType(); + $bundle_entity = \Drupal::entityTypeManager()->getStorage($bundle_entity_type)->load($bundle); + + if ($bundle_entity->shouldCreateNewRevision() && $op === 'view') { + $this->access[$cid] = TRUE; + } + else { + // There should be at least two revisions. If the vid of the given group + // and the vid of the default revision differ, then we already have two + // different revisions so there is no need for a separate database + // check. Also, if you try to revert to or delete the default revision, + // that's not good. + if ($group->isDefaultRevision() && ($this->groupStorage->countDefaultLanguageRevisions($group) == 1 || $op === 'update' || $op === 'delete')) { + $this->access[$cid] = FALSE; + } + else { + // First check the access to the default revision and finally, if the + // group passed in is not the default revision then check access to + // that, too. + $this->access[$cid] = $this->groupAccess->access($this->groupStorage->load($group->id()), $op, $account) && ($group->isDefaultRevision() || $this->groupAccess->access($group, $op, $account)); + } + } + } + + return $this->access[$cid]; + } + +} only in patch2: unchanged: --- a/src/Entity/Access/GroupAccessControlHandler.php +++ b/src/Entity/Access/GroupAccessControlHandler.php @@ -19,9 +19,22 @@ class GroupAccessControlHandler extends EntityAccessControlHandler { * {@inheritdoc} */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + // Fetch information from the group object if possible. + $status = $entity->isPublished(); + $uid = $entity->getOwnerId(); + switch ($operation) { case 'view': - return GroupAccessResult::allowedIfHasGroupPermission($entity, $account, 'view group'); + if (!$status) { + $access_result = GroupAccessResult::allowedIfHasGroupPermission($entity, $account, 'view any unpublished group'); + if (!$access_result->isAllowed() && $account->isAuthenticated() && $account->id() == $uid) { + $access_result = GroupAccessResult::allowedIfHasGroupPermission($entity, $account, 'view own unpublished group'); + } + } + else { + $access_result = GroupAccessResult::allowedIfHasGroupPermission($entity, $account, 'view group'); + } + return $access_result; case 'update': return GroupAccessResult::allowedIfHasGroupPermission($entity, $account, 'edit group'); only in patch2: unchanged: --- a/src/Entity/Controller/GroupController.php +++ b/src/Entity/Controller/GroupController.php @@ -7,7 +7,12 @@ use Drupal\Core\Entity\EntityFormBuilderInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Render\RendererInterface; use Drupal\group\Entity\GroupTypeInterface; +use Drupal\group\Entity\GroupInterface; +use Drupal\group\Entity\Storage\GroupStorageInterface; +use Drupal\Core\Url; use Drupal\user\PrivateTempStoreFactory; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Component\Utility\Xss; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -43,6 +48,13 @@ class GroupController extends ControllerBase { */ protected $renderer; + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + /** * Constructs a new GroupController. * @@ -54,12 +66,15 @@ class GroupController extends ControllerBase { * The entity form builder. * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. */ - public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, RendererInterface $renderer) { + public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, RendererInterface $renderer, DateFormatterInterface $date_formatter) { $this->privateTempStoreFactory = $temp_store_factory; $this->entityTypeManager = $entity_type_manager; $this->entityFormBuilder = $entity_form_builder; $this->renderer = $renderer; + $this->dateFormatter = $date_formatter; } /** @@ -70,7 +85,8 @@ class GroupController extends ControllerBase { $container->get('user.private_tempstore'), $container->get('entity_type.manager'), $container->get('entity.form_builder'), - $container->get('renderer') + $container->get('renderer'), + $container->get('date.formatter') ); } @@ -124,4 +140,159 @@ class GroupController extends ControllerBase { return $this->entityFormBuilder()->getForm($entity, 'add', $extra); } + /** + * Generates an overview table of older revisions of a group. + * + * @param \Drupal\group\Entity\GroupInterface $group + * A group object. + * + * @return array + * An array as expected by \Drupal\Core\Render\RendererInterface::render(). + */ + public function revisionOverview(GroupInterface $group) { + $account = $this->currentUser(); + $langcode = $group->language()->getId(); + $langname = $group->language()->getName(); + $languages = $group->getTranslationLanguages(); + $has_translations = (count($languages) > 1); + $group_storage = $this->entityTypeManager()->getStorage('group'); + $type = $group->getGroupType(); + + $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $group->label()]) : $this->t('Revisions for %title', ['%title' => $group->label()]); + $header = [$this->t('Revision'), $this->t('Operations')]; + + $revert_permission = ($group->hasPermission("revert group revisions", $account) && $group->access('update')); + $delete_permission = ($group->hasPermission("delete group revisions", $account) && $group->access('delete')); + + $rows = []; + $default_revision = $group->getRevisionId(); + $current_revision_displayed = FALSE; + + foreach ($this->getRevisionIds($group, $group_storage) as $vid) { + /** @var \Drupal\group\Entity\GroupInterface $revision */ + $revision = $group_storage->loadRevision($vid); + // Only show revisions that are affected by the language that is being + // displayed. + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { + $username = [ + '#theme' => 'username', + '#account' => $revision->getRevisionUser(), + ]; + + // Use revision link to link to revisions that are not active. + $date = $this->dateFormatter->format($revision->revision_created->value, 'short'); + + // We treat also the latest translation-affecting revision as current + // revision, if it was the default revision, as its values for the + // current language will be the same of the current default revision in + // this case. + $is_current_revision = $vid == $default_revision || (!$current_revision_displayed && $revision->wasDefaultRevision()); + if (!$is_current_revision) { + $link = $this->l($date, new Url('entity.group.revision', ['group' => $group->id(), 'group_revision' => $vid])); + } + else { + $link = $group->toLink($date)->toString(); + $current_revision_displayed = TRUE; + } + + $row = []; + $column = [ + 'data' => [ + '#type' => 'inline_template', + '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}

{{ message }}

{% endif %}', + '#context' => [ + 'date' => $link, + 'username' => $this->renderer->renderPlain($username), + 'message' => ['#markup' => $revision->revision_log_message->value, '#allowed_tags' => Xss::getHtmlTagList()], + ], + ], + ]; + // @todo Simplify once https://www.drupal.org/group/2334319 lands. + $this->renderer->addCacheableDependency($column['data'], $username); + $row[] = $column; + + if ($is_current_revision) { + $row[] = [ + 'data' => [ + '#prefix' => '', + '#markup' => $this->t('Current revision'), + '#suffix' => '', + ], + ]; + + $rows[] = [ + 'data' => $row, + 'class' => ['revision-current'], + ]; + } + else { + $links = []; + if ($revert_permission) { + $links['revert'] = [ + 'title' => $vid < $group->getRevisionId() ? $this->t('Revert') : $this->t('Set as current revision'), + 'url' => $has_translations ? + Url::fromRoute('entity.group.revision_revert_translation_confirm', [ + 'group' => $group->id(), + 'group_revision' => $vid, + 'langcode' => $langcode + ]) : + Url::fromRoute('entity.group.revision_revert_confirm', ['group' => $group->id(), 'group_revision' => $vid]), + ]; + } + + if ($delete_permission) { + $links['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('entity.group.revision_delete_confirm', ['group' => $group->id(), 'group_revision' => $vid]), + ]; + } + + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + + $rows[] = $row; + } + } + } + + $build['group_revisions_table'] = [ + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + '#attached' => [ + 'library' => ['group/drupal.group.admin'], + ], + '#attributes' => ['class' => 'group-revision-table'], + ]; + + $build['pager'] = ['#type' => 'pager']; + + return $build; + } + + /** + * Gets a list of group revision IDs for a specific group. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group entity. + * @param \Drupal\group\Entity\Storage\GroupStorageInterface $group_storage + * The group storage handler. + * + * @return int[] + * Group revision IDs (in descending order). + */ + protected function getRevisionIds(GroupInterface $group, GroupStorageInterface $group_storage) { + $result = $group_storage->getQuery() + ->allRevisions() + ->condition($group->getEntityType()->getKey('id'), $group->id()) + ->sort($group->getEntityType()->getKey('revision'), 'DESC') + ->pager(50) + ->execute(); + return array_keys($result); + } + } only in patch2: unchanged: --- /dev/null +++ b/src/Entity/Form/GroupRevisionDeleteForm.php @@ -0,0 +1,157 @@ +groupStorage = $group_storage; + $this->groupTypeStorage = $group_type_storage; + $this->connection = $connection; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + $entity_manager = $container->get('entity.manager'); + return new static( + $entity_manager->getStorage('group'), + $entity_manager->getStorage('group_type'), + $container->get('database'), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'group_revision_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to delete the revision from %revision-date?', [ + '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime()), + ]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.group.version_history', ['group' => $this->revision->id()]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $group_revision = NULL) { + $this->revision = $this->groupStorage->loadRevision($group_revision); + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->groupStorage->deleteRevision($this->revision->getRevisionId()); + + $this->logger('content')->notice('@type: deleted %title revision %revision.', [ + '@type' => $this->revision->getGroupType()->id(), + '%title' => $this->revision->label(), + '%revision' => $this->revision->getRevisionId(), + ]); + $group_type = $this->groupTypeStorage->load($this->revision->getGroupType()->id())->label(); + $this->messenger() + ->addStatus($this->t('Revision from %revision-date of @type %title has been deleted.', [ + '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime()), + '@type' => $group_type, + '%title' => $this->revision->label(), + ])); + $form_state->setRedirect( + 'entity.group.canonical', + ['group' => $this->revision->id()] + ); + if ($this->connection->query('SELECT COUNT(DISTINCT revision_id) FROM {groups_field_revision} WHERE id = :id', [':id' => $this->revision->id()])->fetchField() > 1) { + $form_state->setRedirect( + 'entity.group.version_history', + ['group' => $this->revision->id()] + ); + } + } + +} only in patch2: unchanged: --- /dev/null +++ b/src/Entity/Form/GroupRevisionRevertForm.php @@ -0,0 +1,242 @@ +groupStorage = $group_storage; + $this->dateFormatter = $date_formatter; + $this->time = $time; + $this->moderationInfo = $moderation_info; + $this->validation = $validation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager')->getStorage('group'), + $container->get('date.formatter'), + $container->get('datetime.time'), + $container->get('content_moderation.moderation_information'), + $container->get('content_moderation.state_transition_validation') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'group_revision_revert_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.group.version_history', ['group' => $this->revision->id()]); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $group_revision = NULL) { + $this->revision = $this->groupStorage->loadRevision($group_revision); + $current_state = $this->revision->moderation_state->value; + $workflow = $this->moderationInfo->getWorkflowForEntity($this->revision); + + /** @var \Drupal\workflows\Transition[] $transitions */ + $transitions = $this->validation->getValidTransitions($this->revision, $this->currentUser()); + + // Exclude self-transitions. + $transitions = array_filter($transitions, function (Transition $transition) use ($current_state) { + return $transition->to()->id() != $current_state; + }); + + $target_states = []; + + foreach ($transitions as $transition) { + $target_states[$transition->to()->id()] = $transition->to()->label(); + } + + if (!count($target_states)) { + return $form; + } + + $form['question'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#value' => $this->getQuestion(), + ]; + + // Persist the entity so we can access it in the submit handler. + $form_state->set('entity', $this->revision); + + $form['new_state'] = [ + '#type' => 'select', + '#title' => $this->t('Change to'), + '#options' => $target_states, + ]; + + $form['revision_log'] = [ + '#type' => 'textfield', + '#title' => $this->t('Log message'), + '#size' => 30, + ]; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Revert'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage($this->revision->getEntityTypeId()); + $this->revision = $storage->createRevision($this->revision, $this->revision->isDefaultRevision()); + + $new_state = $form_state->getValue('new_state'); + + $this->revision->set('moderation_state', $new_state); + + // The revision timestamp will be updated when the revision is saved. Keep + // the original one for the confirmation message. + $original_revision_timestamp = $this->revision->getRevisionCreationTime(); + + $this->revision = $this->prepareRevertedRevision($this->revision, $form_state); + $this->revision->revision_log_message = $form_state->getValue('revision_log'); + $this->revision->setRevisionUserId($this->currentUser()->id()); + $this->revision->setRevisionCreationTime($this->time->getRequestTime()); + $this->revision->setChangedTime($this->time->getRequestTime()); + $this->revision->save(); + + $this->logger('group')->notice('@type: reverted %title revision %revision.', [ + '@type' => $this->revision->getGroupType()->id(), + '%title' => $this->revision->label(), + '%revision' => $this->revision->getRevisionId(), + ]); + $this->messenger() + ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [ + '@type' => $this->revision->getGroupType()->label(), + '%title' => $this->revision->label(), + '%revision-date' => $this->dateFormatter->format($original_revision_timestamp), + ])); + $form_state->setRedirect( + 'entity.group.version_history', + ['group' => $this->revision->id()] + ); + } + + /** + * Prepares a revision to be reverted. + * + * @param \Drupal\group\Entity\GroupInterface $revision + * The revision to be reverted. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\group\Entity\GroupInterface + * The prepared revision ready to be stored. + */ + protected function prepareRevertedRevision(GroupInterface $revision, FormStateInterface $form_state) { + $revision->setNewRevision(); + $revision->isDefaultRevision(TRUE); + + return $revision; + } + +} only in patch2: unchanged: --- /dev/null +++ b/src/Entity/Storage/GroupStorage.php @@ -0,0 +1,55 @@ +database->query( + 'SELECT vid FROM {' . $this->getRevisionTable() . '} WHERE nid=:nid ORDER BY vid', + [':nid' => $group->id()] + )->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function userRevisionIds(AccountInterface $account) { + return $this->database->query( + 'SELECT vid FROM {' . $this->getRevisionDataTable() . '} WHERE uid = :uid ORDER BY vid', + [':uid' => $account->id()] + )->fetchCol(); + } + + /** + * {@inheritdoc} + */ + public function countDefaultLanguageRevisions(GroupInterface $group) { + return $this->database->query('SELECT COUNT(*) FROM {' . $this->getRevisionDataTable() . '} WHERE nid = :nid AND default_langcode = 1', [':nid' => $group->id()])->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function clearRevisionsLanguage(LanguageInterface $language) { + return $this->database->update($this->getRevisionTable()) + ->fields(['langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED]) + ->condition('langcode', $language->getId()) + ->execute(); + } + +} only in patch2: unchanged: --- /dev/null +++ b/src/Entity/Storage/GroupStorageInterface.php @@ -0,0 +1,56 @@ +get('entity.group.latest_version')) { + $requirements = $route->getRequirements(); + unset($requirements['_content_moderation_latest_version']); + $requirements['_group_moderation_latest_version'] = 'TRUE'; + $route->setRequirements($requirements); + } + } + +}