diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php index a4715a8..78b152f 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/Link.php @@ -7,8 +7,9 @@ namespace Drupal\node\Plugin\views\field; -use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\Component\Annotation\PluginID; +use Drupal\node\NodeInterface; +use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\ResultRow; /** @@ -43,6 +44,7 @@ public function buildOptionsForm(&$form, &$form_state) { * {@inheritdoc} */ public function query() { + $this->ensureMyTable(); $this->addAdditionalFields(); } @@ -55,11 +57,21 @@ public function render(ResultRow $values) { } } - protected function renderLink($node, ResultRow $values) { + /** + * Create link and returns link text. + * + * @param \Drupal\node\NodeInterface $node + * Current node object. + * @param \Drupal\views\ResultRow $values + * An object containing all retrieved values. + * @return string + * The link text. + */ + protected function renderLink(NodeInterface $node, ResultRow $values) { if (node_access('view', $node)) { $this->options['alter']['make_link'] = TRUE; $this->options['alter']['path'] = 'node/' . $node->id(); - $text = !empty($this->options['text']) ? $this->options['text'] : t('view'); + $text = !empty($this->options['text']) ? $this->sanitizeValue($this->options['text']) : t('View'); return $text; } } diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php index 8b3d3d7..62f5ffe 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLink.php @@ -12,6 +12,11 @@ use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; use Drupal\Component\Annotation\PluginID; +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityManager; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Field handler to present a link to a node revision. @@ -20,30 +25,110 @@ * * @PluginID("node_revision_link") */ -class RevisionLink extends Link { +class RevisionLink extends Link implements ContainerFactoryPluginInterface { /** - * Overrides Drupal\views\Plugin\views\field\FieldPluginBase::init(). + * Database Service Object. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * The entity manager service. + * + * @var \Drupal\Core\Entity\EntityManager + */ + protected $entityManager; + + /** + * The current active user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * An array of revision count of a node keyed by nids. + * + * @var array + */ + protected $countRevisions; + + /** + * Constructs a Drupal\Component\Plugin\PluginBase object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Database\Connection $database + * The database connection. + * @param \Drupal\Core\Entity\EntityManager $entity_manger + * The entity manager service. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current active user. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, Connection $database, EntityManager $entity_manger, AccountInterface $current_user) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->database = $database; + $this->entityManager = $entity_manger; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('database'), + $container->get('entity.manager'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { parent::init($view, $display, $options); - $this->additional_fields['node_vid'] = array('table' => 'node_field_revision', 'field' => 'vid'); + $this->additional_fields[] = 'vid'; + $this->additional_fields[] = 'nid'; } - public function access() { - return user_access('view revisions') || user_access('administer nodes'); + /** + * {@inheritdoc} + */ + public function preRender(&$values) { + parent::preRender($values); + + // Count the amount of revisions per node. + $nids = array(); + foreach ($values as $row) { + $nids[] = $this->getValue($row, 'nid'); + } + + if (!empty($nids)) { + $this->countRevisions = $this->database->query("SELECT nid, COUNT(vid) as count FROM {node_field_revision} WHERE nid IN (:nids) GROUP BY nid", array(':nids' => array_unique($nids)))->fetchAllKeyed(); + } } + /** + * {@inheritdoc} + */ protected function renderLink($data, ResultRow $values) { - list($node, $vid) = $this->get_revision_entity($values, 'view'); - if (!isset($vid)) { - return; - } + list($node, , $vid) = $this->getRevisionEntity($values, 'view'); // Current revision uses the node view path. - $path = 'node/' . $node->nid; - if (!$node->isDefaultRevision()) { + $uri = $node->uri(); + $path = $uri['path']; + if ($this->countRevisions[$node->id()] > 1 && $node->getRevisionId() != $vid) { $path .= "/revisions/$vid/view"; } @@ -57,25 +142,21 @@ protected function renderLink($data, ResultRow $values) { /** * Returns the revision values of a node. * - * @param object $values + * @param \Drupal\views\ResultRow $values * An object containing all retrieved values. * @param string $op * The operation being performed. * * @return array - * A numerically indexed array containing the current node object and the - * revision ID for this row. + * Containing current node, node revision object and revision ID. */ - function get_revision_entity($values, $op) { - $vid = $this->getValue($values, 'node_vid'); - $node = $this->getEntity($values); - // Unpublished nodes ignore access control. - $node->setPublished(TRUE); - // Ensure user has access to perform the operation on this node. - if (!node_access($op, $node)) { - return array($node, NULL); - } - return array($node, $vid); + public function getRevisionEntity(ResultRow $values, $op) { + $storage_controller = $this->entityManager->getStorageController('node'); + $vid = $this->getValue($values, 'vid'); + $revision_node = $storage_controller->loadRevision($vid); + $node = $storage_controller->load($revision_node->id()); + + return array($node, $revision_node, $vid); } } diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php index 306b5ba..eace77d 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkDelete.php @@ -20,18 +20,19 @@ */ class RevisionLinkDelete extends RevisionLink { - public function access() { - return user_access('delete revisions') || user_access('administer nodes'); - } - + /** + * {@inheritdoc} + */ protected function renderLink($data, ResultRow $values) { - list($node, $vid) = $this->get_revision_entity($values, 'delete'); - if (!isset($vid)) { + list($node, $revision_node, $vid) = $this->getRevisionEntity($values, 'update'); + + $type = $node->getType(); + if (!(($this->currentUser->hasPermission("delete $type revisions") || $this->currentUser->hasPermission('delete all revisions') || $this->currentUser->hasPermission('administer nodes')) && node_access('delete', $revision_node))) { return; } // Current revision cannot be deleted. - if ($node->isDefaultRevision()) { + if ($node->getRevisionId() == $vid) { return; } @@ -39,7 +40,7 @@ protected function renderLink($data, ResultRow $values) { $this->options['alter']['path'] = 'node/' . $node->id() . "/revisions/$vid/delete"; $this->options['alter']['query'] = drupal_get_destination(); - return !empty($this->options['text']) ? $this->options['text'] : t('Delete'); + return !empty($this->options['text']) ? $this->sanitizeValue($this->options['text']) : t('Delete'); } } diff --git a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php index 6318bd4..476efe2 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php +++ b/core/modules/node/lib/Drupal/node/Plugin/views/field/RevisionLinkRevert.php @@ -20,18 +20,19 @@ */ class RevisionLinkRevert extends RevisionLink { - public function access() { - return user_access('revert revisions') || user_access('administer nodes'); - } - + /** + * {@inheritdoc} + */ protected function renderLink($data, ResultRow $values) { - list($node, $vid) = $this->get_revision_entity($values, 'update'); - if (!isset($vid)) { + list($node, , $vid) = $this->getRevisionEntity($values, 'update'); + + $type = $node->bundle(); + if (!($access = ($this->currentUser->hasPermission("revert $type revisions") || $this->currentUser->hasPermission('revert all revisions') || $this->currentUser->hasPermission('administer nodes')) && node_access('update', $node))) { return; } // Current revision cannot be reverted. - if ($node->isDefaultRevision()) { + if ($node->getRevisionId() == $vid) { return; } @@ -39,7 +40,7 @@ protected function renderLink($data, ResultRow $values) { $this->options['alter']['path'] = 'node/' . $node->id() . "/revisions/$vid/revert"; $this->options['alter']['query'] = drupal_get_destination(); - return !empty($this->options['text']) ? $this->options['text'] : t('Revert'); + return !empty($this->options['text']) ? $this->sanitizeValue($this->options['text']) : t('Revert'); } } diff --git a/core/modules/node/lib/Drupal/node/Tests/Views/RevisionLinkTest.php b/core/modules/node/lib/Drupal/node/Tests/Views/RevisionLinkTest.php new file mode 100644 index 0000000..1615a46 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Tests/Views/RevisionLinkTest.php @@ -0,0 +1,106 @@ + 'Node: Revision Links', + 'description' => 'Tests the different revision link handlers.', + 'group' => 'Views module integration', + ); + } + + /** + * Tests revision links. + */ + public function testRevisionLinks() { + // Create one user which can view/revert and delete and one which can only + // one of them. + $this->drupalCreateContentType(array('name' => 'page', 'type' => 'page')); + $account = $this->drupalCreateUser(array('revert all revisions', 'view all revisions', 'delete all revisions', 'edit any page content', 'delete any page content')); + $this->drupalLogin($account); + + // Create two nodes, one without an additional revision and one with a + // revision. + $this->nodes[] = $node = $this->drupalCreateNode(); + $this->revisions[] = $node; + + $this->nodes[] = $node = $this->drupalCreateNode(); + $this->revisions[] = $node; + $node = clone $node; + $node->setNewRevision(); + $this->revisions[] = $node; + $node->save(); + + $this->drupalGet('test-node-revision-links'); + $this->assertResponse(200, 'Test view can be accessed in the path expected'); + + // The first node revision should link to the node directly as you get a + // access denied if you link to the revision. + $uri = $this->nodes[0]->uri(); + $this->assertLinkByHref($uri['path']); + $this->assertNoLinkByHref($uri['path'] . '/revisions/' . $this->revisions[0]->getRevisionId() . '/delete'); + $this->assertNoLinkByHref($uri['path'] . '/revision/' . $this->revisions[0]->getRevisionId() . '/revert'); + + // For the second node the current revision got set to the last revision, so + // the first one should also link to the node page itself. + $uri = $this->revisions[1]->uri(); + $this->assertLinkByHref($uri['path'] . '/revisions/' . $this->revisions[1]->getRevisionId() . '/view'); + $this->assertLinkByHref($uri['path'] . '/revisions/' . $this->revisions[1]->getRevisionId() . '/delete'); + $this->assertLinkByHref($uri['path'] . '/revisions/' . $this->revisions[1]->getRevisionId() . '/revert'); + + $uri = $this->revisions[2]->uri(); + $this->assertLinkByHref($uri['path']); + $this->assertNoLinkByHref($uri['path'] . '/revisions/' . $this->revisions[2]->getRevisionId() . '/delete'); + $this->assertNoLinkByHref($uri['path'] . '/revisions/' . $this->revisions[2]->getRevisionId() . '/revert'); + + $accounts = array(); + $accounts['view'] = $this->drupalCreateUser(array('view all revisions')); + $accounts['revert'] = $this->drupalCreateUser(array('revert all revisions', 'edit any page content')); + $accounts['delete'] = $this->drupalCreateUser(array('delete all revisions', 'delete any page content')); + + $operations = array_keys($accounts); + array_shift($operations); + $uri = $this->revisions[1]->uri(); + // Render the view with users which can only delete/revert revisions. + foreach ($accounts as $op => $account) { + $this->drupalLogin($account); + $this->drupalGet('test-node-revision-links'); + + // Check expected links. + foreach ($operations as $operation) { + if ($operation == $op) { + $this->assertLinkByHref($uri['path'] . '/revisions/' . $this->revisions[1]->getRevisionId() . '/' . $operation); + } + else { + $this->assertNoLinkByHref($uri['path'] . '/revisions/' . $this->revisions[1]->getRevisionId() . '/' . $operation); + } + } + } + } +} diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_links.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_links.yml new file mode 100644 index 0000000..d387c3e --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_revision_links.yml @@ -0,0 +1,221 @@ +base_field: vid +base_table: node_field_revision +core: 8.x +description: '' +display: + default: + display_plugin: default + id: default + display_title: Master + position: '1' + display_options: + cache: + type: none + options: { } + query: + type: views_query + options: + disable_sql_rewrite: '0' + distinct: '0' + slave: '0' + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: '0' + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: '1' + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + items_per_page: '0' + offset: '0' + style: + type: default + row: + type: fields + fields: + link_to_revision: + id: link_to_revision + table: node_field_revision + field: link_to_revision + relationship: none + group_type: group + admin_label: '' + label: 'Link to revision' + exclude: '0' + alter: + alter_text: '0' + text: '' + make_link: '0' + path: '' + absolute: '0' + external: '0' + replace_spaces: '0' + path_case: none + trim_whitespace: '0' + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: '0' + max_length: '' + word_boundary: '1' + ellipsis: '1' + more_link: '0' + more_link_text: '' + more_link_path: '' + strip_tags: '0' + trim: '0' + preserve_tags: '' + html: '0' + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: '1' + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + text: '' + plugin_id: node_revision_link + delete_revision: + id: delete_revision + table: node_field_revision + field: delete_revision + relationship: none + group_type: group + admin_label: '' + label: 'Link to delete revision' + exclude: '0' + alter: + alter_text: '0' + text: '' + make_link: '0' + path: '' + absolute: '0' + external: '0' + replace_spaces: '0' + path_case: none + trim_whitespace: '0' + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: '0' + max_length: '' + word_boundary: '1' + ellipsis: '1' + more_link: '0' + more_link_text: '' + more_link_path: '' + strip_tags: '0' + trim: '0' + preserve_tags: '' + html: '0' + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: '1' + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + text: '' + plugin_id: node_revision_link_delete + revert_revision: + id: revert_revision + table: node_field_revision + field: revert_revision + relationship: none + group_type: group + admin_label: '' + label: 'Link to revert revision' + exclude: '0' + alter: + alter_text: '0' + text: '' + make_link: '0' + path: '' + absolute: '0' + external: '0' + replace_spaces: '0' + path_case: none + trim_whitespace: '0' + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: '0' + max_length: '' + word_boundary: '1' + ellipsis: '1' + more_link: '0' + more_link_text: '' + more_link_path: '' + strip_tags: '0' + trim: '0' + preserve_tags: '' + html: '0' + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: '1' + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: '1' + empty: '' + hide_empty: '0' + empty_zero: '0' + hide_alter_empty: '1' + text: '' + plugin_id: node_revision_link_revert + filters: + status: + value: '1' + table: node_field_revision + field: status + id: status + expose: + operator: '0' + group: '1' + sorts: { } + title: test_node_revision_links + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: '1' + display_options: + path: test-node-revision-links +label: test_node_revision_links +module: views +id: test_node_revision_links +tag: '' +uuid: c89579e2-be88-44e6-81f8-6d5e50b81a05 +langcode: en