diff --git a/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php new file mode 100644 index 0000000..859782f --- /dev/null +++ b/core/lib/Drupal/Core/Revision/RevisionControllerTrait.php @@ -0,0 +1,249 @@ +entityManager()->getStorage($this->getRevisionEntityTypeId())->loadRevision($revision_id); + $view_controller = $this->getEntityViewBuilder($this->entityManager, $this->renderer); + $page = $view_controller->view($entity); + unset($page[$this->getRevisionEntityTypeId() . 's'][$entity->id()]['#cache']); + return $page; + } + + /** + * Page title callback for an entity revision. + * + * @param int $revision_id + * The entity revision ID. + * + * @return string + * The page title. + */ + public function revisionPageTitle($revision_id) { + $entity = $this->entityManager()->getStorage($this->getRevisionEntityTypeId())->loadRevision($revision_id); + if ($entity instanceof TimestampedRevisionInterface) { + return $this->t('Revision of %title from %date', array( + '%title' => $entity->label(), + '%date' => \Drupal::service('date.formatter')->format($entity->getRevisionCreationTime()), + )); + } + else { + return $this->t('Revision of %title', array( + '%title' => $entity->label(), + )); + } + } + + /** + * Determines if the user has permission to revert revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check revert access for. + * + * @return bool + * TRUE if the user has revert access. + */ + abstract protected function hasRevertRevisionPermission(EntityInterface $entity); + + /** + * Determines if the user has permission to delete revisions. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to check delete revision access for. + * + * @return bool + * TRUE if the user has delete revision access. + */ + abstract protected function hasDeleteRevisionPermission(EntityInterface $entity); + + /** + * Builds a link to revert an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to build a revert revision link for. + * @param int $revision_id + * The revision ID of the revert link. + * + * @return array + * A link render array. + */ + abstract protected function buildRevertRevisionLink(EntityInterface $entity, $revision_id); + + /** + * Builds a link to delete an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to build a delete revision link for. + * @param int $revision_id + * The revision ID of the delete link. + * + * @return array + * A link render array. + */ + abstract protected function buildDeleteRevisionLink(EntityInterface $entity, $revision_id); + + /** + * Returns a string providing details of the revision. + * + * E.g. Node describes its revisions using {date} by {username}. For the + * non-current revision, it also provides a link to view that revision. + * + * @param \Drupal\Core\Entity\EntityInterface $revision + * Returns a string to provide the details of the revision. + * @param bool $is_current + * TRUE if the revision is the current revision. + * + * @return string + * Revision description. + */ + abstract protected function getRevisionDescription(EntityInterface $revision, $is_current = FALSE); + + /** + * Returns a string providing the title of the revision. + * + * @param \Drupal\Core\Entity\EntityInterface $revision + * Returns a string to provide the title of the revision. + * + * @return string + * Revision title. + */ + abstract protected function getRevisionTitle(EntityInterface $revision); + + + /** + * Gets the entity type ID for this revision controller. + * + * @return string + * Entity Type ID for this revision controller. + */ + abstract protected function getRevisionEntityTypeId(); + + /** + * Gets the entity's view controller. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * Entity manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * Renderer service. + * + * @return \Drupal\Core\Entity\EntityViewBuilderInterface + * A new entity view builder. + */ + abstract protected function getEntityViewBuilder(EntityManagerInterface $entity_manager, RendererInterface $renderer); + + /** + * Generates an overview table of older revisions of an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * An entity object. + * + * @return array + * An array as expected by drupal_render(). + */ + public function revisionOverview(ContentEntityInterface $entity) { + $langcode = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + $entity_storage = $this->entityManager()->getStorage($this->getRevisionEntityTypeId()); + + $build['#title'] = $this->getRevisionTitle($entity); + $header = array($this->t('Revision'), $this->t('Operations')); + + $rows = []; + + $vids = $entity_storage->revisionIds($entity); + + $latest_revision = TRUE; + + foreach (array_reverse($vids) as $vid) { + $row = []; + /** @var \Drupal\node\NodeInterface $revision */ + $revision = $entity_storage->loadRevision($vid); + if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { + if ($latest_revision) { + $row[] = $this->getRevisionDescription($revision, TRUE); + $row[] = [ + 'data' => [ + '#prefix' => '', + '#markup' => $this->t('Current revision'), + '#suffix' => '', + ], + ]; + foreach ($row as &$current) { + $current['class'] = ['revision-current']; + } + $latest_revision = FALSE; + } + else { + $row[] = $this->getRevisionDescription($revision, FALSE); + $links = $this->getOperationLinks($entity, $vid); + + $row[] = [ + 'data' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + } + + $rows[] = $row; + } + + $build[$this->getRevisionEntityTypeId() . '_revisions_table'] = array( + '#theme' => 'table', + '#rows' => $rows, + '#header' => $header, + ); + + return $build; + } + + /** + * Get the links of the operations for an entity revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to build the revision links for. + * @param int $revision_id + * The revision ID of the delete link. + * + * @return array + * The operation links. + */ + protected function getOperationLinks(EntityInterface $entity, $revision_id) { + $links = []; + $revert_permission = $this->hasRevertRevisionPermission($entity); + $delete_permission = $this->hasDeleteRevisionPermission($entity); + if ($revert_permission) { + $links['revert'] = $this->buildRevertRevisionLink($entity, $revision_id); + } + + if ($delete_permission) { + $links['delete'] = $this->buildDeleteRevisionLink($entity, $revision_id); + } + return $links; + } + +} diff --git a/core/lib/Drupal/Core/Revision/TimestampedRevisionInterface.php b/core/lib/Drupal/Core/Revision/TimestampedRevisionInterface.php new file mode 100644 index 0000000..d4fa536 --- /dev/null +++ b/core/lib/Drupal/Core/Revision/TimestampedRevisionInterface.php @@ -0,0 +1,33 @@ +entityManager()->getStorage('node')->loadRevision($node_revision); - $node_view_controller = new NodeViewController($this->entityManager, $this->renderer); - $page = $node_view_controller->view($node); - unset($page['nodes'][$node->id()]['#cache']); - return $page; + // We call to the parent so we can retain the {node_revision} entry in our + // routing definition. + return $this->showRevisionTrait($node_revision); } /** @@ -147,8 +154,9 @@ public function revisionShow($node_revision) { * The page title. */ public function revisionPageTitle($node_revision) { - $node = $this->entityManager()->getStorage('node')->loadRevision($node_revision); - return $this->t('Revision of %title from %date', array('%title' => $node->label(), '%date' => format_date($node->getRevisionCreationTime()))); + // We call to the parent so we can retain the {node_revision} entry in our + // routing definition. + return $this->revisionTraitTitle($node_revision); } /** @@ -161,111 +169,10 @@ public function revisionPageTitle($node_revision) { * An array as expected by drupal_render(). */ public function revisionOverview(NodeInterface $node) { - $account = $this->currentUser(); - $langcode = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); - $langname = $this->languageManager()->getLanguageName($langcode); - $languages = $node->getTranslationLanguages(); - $has_translations = (count($languages) > 1); - $node_storage = $this->entityManager()->getStorage('node'); - $type = $node->getType(); - - $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $node->label()]) : $this->t('Revisions for %title', ['%title' => $node->label()]); - $header = array($this->t('Revision'), $this->t('Operations')); - - $revert_permission = (($account->hasPermission("revert $type revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $node->access('update')); - $delete_permission = (($account->hasPermission("delete $type revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $node->access('delete')); - - $rows = array(); - - $vids = $node_storage->revisionIds($node); - - $latest_revision = TRUE; - - foreach (array_reverse($vids) as $vid) { - /** @var \Drupal\node\NodeInterface $revision */ - $revision = $node_storage->loadRevision($vid); - if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) { - $username = [ - '#theme' => 'username', - '#account' => $revision->uid->entity, - ]; - - // Use revision link to link to revisions that are not active. - $date = $this->dateFormatter->format($revision->revision_timestamp->value, 'short'); - if ($vid != $node->getRevisionId()) { - $link = $this->l($date, new Url('entity.node.revision', ['node' => $node->id(), 'node_revision' => $vid])); - } - else { - $link = $node->link($date); - } - - $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->value, '#allowed_tags' => Xss::getHtmlTagList()], - ], - ], - ]; - // @todo Simplify once https://www.drupal.org/node/2334319 lands. - $this->renderer->addCacheableDependency($column['data'], $username); - $row[] = $column; - - if ($latest_revision) { - $row[] = [ - 'data' => [ - '#prefix' => '', - '#markup' => $this->t('Current revision'), - '#suffix' => '', - ], - ]; - foreach ($row as &$current) { - $current['class'] = ['revision-current']; - } - $latest_revision = FALSE; - } - else { - $links = []; - if ($revert_permission) { - $links['revert'] = [ - 'title' => $this->t('Revert'), - 'url' => $has_translations ? - Url::fromRoute('node.revision_revert_translation_confirm', ['node' => $node->id(), 'node_revision' => $vid, 'langcode' => $langcode]) : - Url::fromRoute('node.revision_revert_confirm', ['node' => $node->id(), 'node_revision' => $vid]), - ]; - } - - if ($delete_permission) { - $links['delete'] = [ - 'title' => $this->t('Delete'), - 'url' => Url::fromRoute('node.revision_delete_confirm', ['node' => $node->id(), 'node_revision' => $vid]), - ]; - } - - $row[] = [ - 'data' => [ - '#type' => 'operations', - '#links' => $links, - ], - ]; - } - - $rows[] = $row; - } - } - - $build['node_revisions_table'] = array( - '#theme' => 'table', - '#rows' => $rows, - '#header' => $header, - '#attached' => array( - 'library' => array('node/drupal.node.admin'), - ), - ); + $build = $this->revisionTraitOverview($node); + $build['#attached'] = [ + 'library' => ['node/drupal.node.admin'], + ]; return $build; } @@ -283,4 +190,121 @@ public function addPageTitle(NodeTypeInterface $node_type) { return $this->t('Create @name', array('@name' => $node_type->label())); } + /** + * {@inheritdoc} + */ + protected function hasRevertRevisionPermission(EntityInterface $entity) { + $account = $this->currentUser(); + $bundle = $entity->bundle(); + return (($account->hasPermission("revert $bundle revisions") || $account->hasPermission('revert all revisions') || $account->hasPermission('administer nodes')) && $entity->access('update')); + } + + /** + * {@inheritdoc} + */ + protected function hasDeleteRevisionPermission(EntityInterface $entity) { + $account = $this->currentUser(); + $bundle = $entity->bundle(); + return (($account->hasPermission("delete $bundle revisions") || $account->hasPermission('delete all revisions') || $account->hasPermission('administer nodes')) && $entity->access('delete')); + } + + /** + * {@inheritdoc} + */ + protected function buildRevertRevisionLink(EntityInterface $entity, $revision_id) { + $langcode = $this->languageManager() + ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->getId(); + $languages = $entity->getTranslationLanguages(); + $has_translations = (count($languages) > 1); + return [ + 'title' => $this->t('Revert'), + 'url' => $has_translations ? + Url::fromRoute('node.revision_revert_translation_confirm', [ + 'node' => $entity->id(), + 'node_revision' => $revision_id, + 'langcode' => $langcode + ]) : + Url::fromRoute('node.revision_revert_confirm', [ + 'node' => $entity->id(), + 'node_revision' => $revision_id + ]), + ]; + } + + /** + * {@inheritdoc} + */ + protected function buildDeleteRevisionLink(EntityInterface $entity, $revision_id) { + return [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('node.revision_delete_confirm', ['node' => $entity->id(), 'node_revision' => $revision_id]), + ]; + } + + /** + * {@inheritdoc} + */ + protected function getRevisionDescription(EntityInterface $revision, $is_current = FALSE) { + $revision_author = $revision->uid->entity; + $username = array( + '#theme' => 'username', + '#account' => $revision_author, + ); + if ($is_current) { + $date = $revision->link($this->dateFormatter->format($revision->revision_timestamp->value, 'short')); + } + else { + $date = $this->l($this->dateFormatter->format($revision->revision_timestamp->value, 'short'), new Url('entity.node.revision', [ + 'node' => $revision->id(), + 'node_revision' => $revision->getRevisionId(), + ])); + } + + $description = [ + 'data' => [ + '#type' => 'inline_template', + '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}

{{ message }}

{% endif %}', + '#context' => [ + 'date' => $date, + 'username' => $this->renderer->renderPlain($username), + 'message' => [ + '#markup' => $revision->revision_log->value, + '#allowed_tags' => Xss::getHtmlTagList() + ], + ], + ], + ]; + + // @todo Simplify once https://www.drupal.org/node/2334319 lands. + $this->renderer->addCacheableDependency($description['data'], $username); + + return $description; + } + + /** + * {@inheritdoc} + */ + protected function getRevisionEntityTypeId() { + return 'node'; + } + + /** + * {@inheritdoc} + */ + protected function getEntityViewBuilder(EntityManagerInterface $entity_manager, RendererInterface $renderer) { + return new NodeViewController($entity_manager, $renderer); + } + + /** + * {@inheritdoc} + */ + protected function getRevisionTitle(EntityInterface $entity) { + $langcode = $this->languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + $langname = $this->languageManager()->getLanguageName($langcode); + $languages = $entity->getTranslationLanguages(); + $has_translations = (count($languages) > 1); + return $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $entity->label()]) : $this->t('Revisions for %title', ['%title' => $entity->label()]); + } + } diff --git a/core/modules/node/src/NodeInterface.php b/core/modules/node/src/NodeInterface.php index 241fb1a..d237fdd 100644 --- a/core/modules/node/src/NodeInterface.php +++ b/core/modules/node/src/NodeInterface.php @@ -7,6 +7,7 @@ namespace Drupal\node; +use Drupal\Core\Revision\TimestampedRevisionInterface; use Drupal\user\EntityOwnerInterface; use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\ContentEntityInterface; @@ -14,7 +15,7 @@ /** * Provides an interface defining a node entity. */ -interface NodeInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface { +interface NodeInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface, TimestampedRevisionInterface { /** * Gets the node type. @@ -122,25 +123,6 @@ public function isPublished(); public function setPublished($published); /** - * Gets the node revision creation timestamp. - * - * @return int - * The UNIX timestamp of when this revision was created. - */ - public function getRevisionCreationTime(); - - /** - * Sets the node revision creation timestamp. - * - * @param int $timestamp - * The UNIX timestamp of when this revision was created. - * - * @return \Drupal\node\NodeInterface - * The called node entity. - */ - public function setRevisionCreationTime($timestamp); - - /** * Gets the node revision author. * * @return \Drupal\user\UserInterface