diff --git a/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php b/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php new file mode 100644 index 0000000..aca491d --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Access/NodeRevisionAccessCheck.php @@ -0,0 +1,158 @@ +nodeStorage = $entity_manager->getStorageController('node'); + $this->nodeAccess = $entity_manager->getAccessController('node'); + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function applies(Route $route) { + return array_key_exists('_access_node_revision', $route->getRequirements()); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request) { + $revision = $this->nodeStorage->loadRevision($request->attributes->get('node_revision')); + return $this->checkAccess($revision, $route->getRequirement('_access_node_revision')) ? static::ALLOW : static::DENY; + } + + /** + * Checks node revision access. + * + * @param \Drupal\node\NodeInterface $node + * The node to check. + * @param string $op + * (optional) The specific operation being checked. Defaults to 'view.' + * @param \Drupal\Core\Session\AccountInterface|null $account + * (optional) A user object representing the user for whom the operation is + * to be performed. Determines access for a user other than the current user. + * Defaults to NULL. + * @param string|null $langcode + * (optional) Language code for the variant of the node. Different language + * variants might have different permissions associated. If NULL, the + * original langcode of the node is used. Defaults to NULL. + * + * @return bool + * TRUE if the operation may be performed, FALSE otherwise. + */ + public function checkAccess(NodeInterface $node, $op = 'view', AccountInterface $account = NULL, $langcode = NULL) { + $map = array( + 'view' => 'view all revisions', + 'update' => 'revert all revisions', + 'delete' => 'delete all revisions', + ); + $bundle = $node->bundle(); + $type_map = array( + 'view' => "view $bundle revisions", + 'update' => "revert $bundle revisions", + 'delete' => "delete $bundle revisions", + ); + + if (!$node || !isset($map[$op]) || !isset($type_map[$op])) { + // If there was no node to check against, or the $op was not one of the + // supported ones, we return access denied. + return FALSE; + } + + if (!isset($account)) { + $account = $GLOBALS['user']; + } + + // If no language code was provided, default to the node revision's langcode. + if (empty($langcode)) { + $langcode = $node->language()->id; + } + + // Statically cache access by revision ID, language code, user account ID, + // and operation. + $cid = $node->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op; + + if (!isset($this->access[$cid])) { + // Perform basic permission checks first. + if (!user_access($map[$op], $account) && !user_access($type_map[$op], $account) && !user_access('administer nodes', $account)) { + return $this->access[$cid] = FALSE; + } + + // There should be at least two revisions. If the vid of the given node + // 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 ($node->isDefaultRevision() && ($this->connection->query('SELECT COUNT(*) FROM {node_field_revision} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $node->id()))->fetchField() == 1 || $op == 'update' || $op == 'delete')) { + $this->access[$cid] = FALSE; + } + elseif (user_access('administer nodes', $account)) { + $this->access[$cid] = TRUE; + } + else { + // First check the access to the default revision and finally, if the + // node passed in is not the default revision then access to that, too. + $this->access[$cid] = $this->nodeAccess->access($this->nodeStorage->load($node->id()), $op, $langcode, $account) && ($node->isDefaultRevision() || $this->nodeAccess->access($node, $op, $langcode, $account)); + } + } + + return $this->access[$cid]; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Form/NodeDeleteForm.php b/core/modules/node/lib/Drupal/node/Form/NodeDeleteForm.php new file mode 100644 index 0000000..5a3fdb0 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/NodeDeleteForm.php @@ -0,0 +1,106 @@ +urlGenerator = $url_generator; + $this->nodeTypeStorage = $node_type_storage; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info) { + return new static( + $container->get('module_handler'), + $container->get('url_generator'), + $container->get('plugin.manager.entity')->getStorageController('node_type') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to delete %title?', array('%title' => $this->entity->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelPath() { + $uri = $this->entity->uri(); + return $this->urlGenerator->generateFromPath($uri['path'], $uri['options']); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, array &$form_state) { + // Do not attach fields to the delete form. + return $form; + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + $this->entity->delete(); + watchdog('content', '@type: deleted %title.', array('@type' => $this->entity->bundle(), '%title' => $this->entity->label())); + $node_type = $this->nodeTypeStorage->load($this->entity->bundle())->label(); + drupal_set_message(t('@type %title has been deleted.', array('@type' => $node_type, '%title' => $this->entity->label()))); + $form_state['redirect'] = ''; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Form/NodeRevisionDeleteForm.php b/core/modules/node/lib/Drupal/node/Form/NodeRevisionDeleteForm.php new file mode 100644 index 0000000..8c06d49 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/NodeRevisionDeleteForm.php @@ -0,0 +1,130 @@ +nodeStorage = $node_storage; + $this->nodeTypeStorage = $node_type_storage; + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + $entity_manager = $container->get('plugin.manager.entity'); + return new static( + $entity_manager->getStorageController('node'), + $entity_manager->getStorageController('node_type'), + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'node_revision_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime()))); + } + + /** + * {@inheritdoc} + */ + public function getCancelPath() { + return 'node/' . $this->revision->id() . '/revisions'; + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, Request $request = NULL, $node_revision = NULL) { + $this->revision = $this->nodeStorage->loadRevision($node_revision); + return parent::buildForm($form, $form_state, $request); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $this->nodeStorage->deleteRevision($this->revision->getRevisionId()); + + watchdog('content', '@type: deleted %title revision %revision.', array('@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId())); + $node_type = $this->nodeTypeStorage->load($this->revision->bundle())->label(); + drupal_set_message(t('Revision from %revision-date of @type %title has been deleted.', array('%revision-date' => format_date($this->revision->getRevisionCreationTime()), '@type' => $node_type, '%title' => $this->revision->label()))); + $form_state['redirect'] = 'node/' . $this->revision->id(); + if ($this->connection->query('SELECT COUNT(DISTINCT vid) FROM {node_field_revision} WHERE nid = :nid', array(':nid' => $this->revision->id()))->fetchField() > 1) { + $form_state['redirect'] .= '/revisions'; + } + } + +} diff --git a/core/modules/node/lib/Drupal/node/Form/NodeRevisionRevertForm.php b/core/modules/node/lib/Drupal/node/Form/NodeRevisionRevertForm.php new file mode 100644 index 0000000..d4c69ec --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/NodeRevisionRevertForm.php @@ -0,0 +1,119 @@ +nodeStorage = $node_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.entity')->getStorageController('node') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormID() { + return 'node_revision_revert_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime()))); + } + + /** + * {@inheritdoc} + */ + public function getCancelPath() { + return 'node/' . $this->revision->id() . '/revisions'; + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, array &$form_state, Request $request = NULL, $node_revision = NULL) { + $this->revision = $this->nodeStorage->loadRevision($node_revision); + return parent::buildForm($form, $form_state, $request); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $this->revision->setNewRevision(); + // Make this the new default revision for the node. + $this->revision->isDefaultRevision(TRUE); + + // 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->log = t('Copy of the revision from %date.', array('%date' => format_date($original_revision_timestamp))); + + $this->revision->save(); + + watchdog('content', '@type: reverted %title revision %revision.', array('@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId())); + drupal_set_message(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => node_get_type_label($this->revision), '%title' => $this->revision->label(), '%revision-date' => format_date($original_revision_timestamp)))); + $form_state['redirect'] = 'node/' . $this->revision->id() . '/revisions'; + } + +} diff --git a/core/modules/node/lib/Drupal/node/Form/RebuildPermissionsForm.php b/core/modules/node/lib/Drupal/node/Form/RebuildPermissionsForm.php new file mode 100644 index 0000000..d104fab --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Form/RebuildPermissionsForm.php @@ -0,0 +1,57 @@ + 'view all revisions', - 'update' => 'revert all revisions', - 'delete' => 'delete all revisions', - ); - $bundle = $node->bundle(); - $type_map = array( - 'view' => "view $bundle revisions", - 'update' => "revert $bundle revisions", - 'delete' => "delete $bundle revisions", - ); - - if (!$node || !isset($map[$op]) || !isset($type_map[$op])) { - // If there was no node to check against, or the $op was not one of the - // supported ones, we return access denied. - return FALSE; - } - - if (!isset($account)) { - $account = $GLOBALS['user']; - } - - // If no language code was provided, default to the node revision's langcode. - if (empty($langcode)) { - $langcode = $node->language()->id; - } - - // Statically cache access by revision ID, language code, user account ID, - // and operation. - $cid = $node->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op; - - if (!isset($access[$cid])) { - // Perform basic permission checks first. - if (!user_access($map[$op], $account) && !user_access($type_map[$op], $account) && !user_access('administer nodes', $account)) { - return $access[$cid] = FALSE; - } - - // There should be at least two revisions. If the vid of the given node - // 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 ($node->isDefaultRevision() && (db_query('SELECT COUNT(*) FROM {node_field_revision} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $node->id()))->fetchField() == 1 || $op == 'update' || $op == 'delete')) { - $access[$cid] = FALSE; - } - elseif (user_access('administer nodes', $account)) { - $access[$cid] = TRUE; - } - else { - // First check the access to the default revision and finally, if the - // node passed in is not the default revision then access to that, too. - $access[$cid] = node_access($op, node_load($node->id()), $account, $langcode) && ($node->isDefaultRevision() || node_access($op, $node, $account, $langcode)); - } - } - - return $access[$cid]; + return Drupal::service('access_check.node.revision')->checkAccess($node, $op, $account, $langcode); } /** @@ -1217,17 +1160,6 @@ function node_menu() { 'type' => MENU_DEFAULT_LOCAL_TASK, ); - $items['admin/reports/status/rebuild'] = array( - 'title' => 'Rebuild permissions', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('node_configure_rebuild_confirm'), - // Any user than can potentially trigger a node_access_needs_rebuild(TRUE) - // has to be allowed access to the 'node access rebuild' confirm form. - 'access arguments' => array('access administration pages'), - 'type' => MENU_CALLBACK, - 'file' => 'node.admin.inc', - ); - $items['admin/structure/types'] = array( 'title' => 'Content types', 'description' => 'Manage content types, including default status, front page promotion, comment settings, etc.', @@ -1295,14 +1227,10 @@ function node_menu() { ); $items['node/%node/delete'] = array( 'title' => 'Delete', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('node_delete_confirm', 1), - 'access callback' => 'node_access', - 'access arguments' => array('delete', 1), + 'route_name' => 'node_delete_confirm', 'weight' => 10, 'type' => MENU_LOCAL_TASK, 'context' => MENU_CONTEXT_INLINE, - 'file' => 'node.pages.inc', ); $items['node/%node/revisions'] = array( 'title' => 'Revisions', @@ -1323,19 +1251,11 @@ function node_menu() { ); $items['node/%node/revisions/%node_revision/revert'] = array( 'title' => 'Revert to earlier revision', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('node_revision_revert_confirm', 3), - 'access callback' => '_node_revision_access', - 'access arguments' => array(3, 'update'), - 'file' => 'node.pages.inc', + 'route_name' => 'node_revision_revert_confirm', ); $items['node/%node/revisions/%node_revision/delete'] = array( 'title' => 'Delete earlier revision', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('node_revision_delete_confirm', 3), - 'access callback' => '_node_revision_access', - 'access arguments' => array(3, 'delete'), - 'file' => 'node.pages.inc', + 'route_name' => 'node_revision_delete_confirm', ); return $items; } diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc index af8e1e7..386608b 100644 --- a/core/modules/node/node.pages.inc +++ b/core/modules/node/node.pages.inc @@ -181,44 +181,6 @@ function theme_node_preview($variables) { } /** - * Page callback: Form constructor for node deletion confirmation form. - * - * @param object $node - * A node object. - * - * @return array - * A form array. - * - * @see node_delete_confirm_submit() - * @see node_menu() - */ -function node_delete_confirm($form, &$form_state, $node) { - // Always provide entity id in the same form key as in the entity edit form. - $form['nid'] = array('#type' => 'value', '#value' => $node->nid); - return confirm_form($form, - t('Are you sure you want to delete %title?', array('%title' => $node->label())), - 'node/' . $node->nid, - t('This action cannot be undone.'), - t('Delete'), - t('Cancel') - ); -} - -/** - * Form submission handler for node_delete_confirm(). - */ -function node_delete_confirm_submit($form, &$form_state) { - if ($form_state['values']['confirm']) { - $node = node_load($form_state['values']['nid']); - $node->delete(); - watchdog('content', '@type: deleted %title.', array('@type' => $node->type, '%title' => $node->label())); - drupal_set_message(t('@type %title has been deleted.', array('@type' => node_get_type_label($node), '%title' => $node->label()))); - } - - $form_state['redirect'] = ''; -} - -/** * Page callback: Generates an overview table of older revisions of a node. * * @param object $node @@ -300,80 +262,3 @@ function node_revision_overview($node) { return $build; } - -/** - * Page callback: Form constructor for the reversion confirmation form. - * - * This form prevents against CSRF attacks. - * - * @param int $node_revision - * The node revision ID. - * - * @return array - * An array as expected by drupal_render(). - * - * @see node_menu() - * @see node_revision_revert_confirm_submit() - * @ingroup forms - */ -function node_revision_revert_confirm($form, $form_state, $node_revision) { - $form['#node_revision'] = $node_revision; - return confirm_form($form, t('Are you sure you want to revert to the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', '', t('Revert'), t('Cancel')); -} - -/** - * Form submission handler for node_revision_revert_confirm(). - */ -function node_revision_revert_confirm_submit($form, &$form_state) { - $node_revision = $form['#node_revision']; - $node_revision->setNewRevision(); - // Make this the new default revision for the node. - $node_revision->isDefaultRevision(TRUE); - - // The revision timestamp will be updated when the revision is saved. Keep the - // original one for the confirmation message. - $original_revision_timestamp = $node_revision->revision_timestamp; - - $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($original_revision_timestamp))); - - $node_revision->save(); - - watchdog('content', '@type: reverted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->label(), '%revision' => $node_revision->vid)); - drupal_set_message(t('@type %title has been reverted back to the revision from %revision-date.', array('@type' => node_get_type_label($node_revision), '%title' => $node_revision->label(), '%revision-date' => format_date($original_revision_timestamp)))); - $form_state['redirect'] = 'node/' . $node_revision->nid . '/revisions'; -} - -/** - * Page callback: Form constructor for the revision deletion confirmation form. - * - * This form prevents against CSRF attacks. - * - * @param $node_revision - * The node revision ID. - * - * @return - * An array as expected by drupal_render(). - * - * @see node_menu() - * @see node_revision_delete_confirm_submit() - * @ingroup forms - */ -function node_revision_delete_confirm($form, $form_state, $node_revision) { - $form['#node_revision'] = $node_revision; - return confirm_form($form, t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($node_revision->revision_timestamp))), 'node/' . $node_revision->nid . '/revisions', t('This action cannot be undone.'), t('Delete'), t('Cancel')); -} - -/** - * Form submission handler for node_revision_delete_confirm(). - */ -function node_revision_delete_confirm_submit($form, &$form_state) { - $node_revision = $form['#node_revision']; - node_revision_delete($node_revision->vid); - - watchdog('content', '@type: deleted %title revision %revision.', array('@type' => $node_revision->type, '%title' => $node_revision->label(), '%revision' => $node_revision->vid)); - drupal_set_message(t('Revision from %revision-date of @type %title has been deleted.', array('%revision-date' => format_date($node_revision->revision_timestamp), '@type' => node_get_type_label($node_revision), '%title' => $node_revision->label()))); - $form_state['redirect'] = 'node/' . $node_revision->nid; - if (db_query('SELECT COUNT(DISTINCT vid) FROM {node_field_revision} WHERE nid = :nid', array(':nid' => $node_revision->nid))->fetchField() > 1) { - $form_state['redirect'] .= '/revisions'; - } -} diff --git a/core/modules/node/node.routing.yml b/core/modules/node/node.routing.yml index 9204a2b..dfdc0a6 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -12,6 +12,27 @@ node_page_edit: requirements: _entity_access: 'node.update' +node_delete_confirm: + pattern: '/node/{node}/delete' + defaults: + _entity_form: 'node.delete' + requirements: + _entity_access: 'node.delete' + +node_revision_revert_confirm: + pattern: '/node/{node}/revisions/{node_revision}/revert' + defaults: + _form: '\Drupal\node\Form\NodeRevisionRevertForm' + requirements: + _access_node_revision: 'update' + +node_revision_delete_confirm: + pattern: '/node/{node}/revisions/{node_revision}/delete' + defaults: + _form: '\Drupal\node\Form\NodeRevisionDeleteForm' + requirements: + _access_node_revision: 'delete' + node_overview_types: pattern: '/admin/structure/types' defaults: @@ -47,3 +68,10 @@ node_type_delete_confirm: _entity_form: 'node_type.delete' requirements: _entity_access: 'node_type.delete' + +node_configure_rebuild_confirm: + pattern: '/admin/reports/status/rebuild' + defaults: + _form: 'Drupal\node\Form\RebuildPermissionsForm' + requirements: + _permission: 'access administration pages' diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml index 6f486f7..ab49617 100644 --- a/core/modules/node/node.services.yml +++ b/core/modules/node/node.services.yml @@ -2,3 +2,8 @@ services: node.grant_storage: class: Drupal\node\NodeGrantDatabaseStorage arguments: ['@database', '@module_handler'] + access_check.node.revision: + class: Drupal\node\Access\NodeRevisionAccessCheck + arguments: ['@plugin.manager.entity', '@database'] + tags: + - { name: access_check }