diff --git a/core/modules/moderation/moderation.module b/core/modules/moderation/moderation.module index a6a2fb7..2800681 100644 --- a/core/modules/moderation/moderation.module +++ b/core/modules/moderation/moderation.module @@ -1,7 +1,7 @@ setHandlerClass('form', $handlers['form']); } } - -/** - * Determine if the given node has a draft. - * - * Draft is simply defined here as an unpublished revision that is newer than - * the published node. - * - * @param \Drupal\node\NodeInterface $node - * The node object. - * - * @return bool|int - * The revision ID if a draft exists, otherwise FALSE. - * - * @todo: Make this more abstract and move logic to a service. - */ -function moderation_node_has_draft(NodeInterface $node) { - $current_revision = $node->getRevisionId(); - $node_storage = \Drupal::service('entity.manager')->getStorage('node'); - $vids = $node_storage->revisionIds($node); - - // Filter out vids less than or equal to current revision. - asort($vids); - $filtered = array_filter($vids, function ($var) use ($current_revision) { - return $var > $current_revision; - }); - return array_pop($filtered) ?: FALSE; -} diff --git a/core/modules/moderation/moderation.services.yml b/core/modules/moderation/moderation.services.yml index 08193dc..b7b91a4 100644 --- a/core/modules/moderation/moderation.services.yml +++ b/core/modules/moderation/moderation.services.yml @@ -1,6 +1,9 @@ services: access_check.node.draft: class: Drupal\moderation\Access\DraftAccess - arguments: ['@entity.manager'] + arguments: ['@entity.manager','@moderation.node_moderation'] tags: - { name: access_check, applies_to: _access_node_draft } + moderation.node_moderation: + class: Drupal\moderation\NodeModeration + arguments: ['@entity_type.manager'] diff --git a/core/modules/moderation/src/Access/DraftAccess.php b/core/modules/moderation/src/Access/DraftAccess.php index 0ef04e8..ec8978c 100644 --- a/core/modules/moderation/src/Access/DraftAccess.php +++ b/core/modules/moderation/src/Access/DraftAccess.php @@ -6,6 +6,7 @@ namespace Drupal\moderation\Access; +use Drupal\moderation\ModerationInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Routing\Access\AccessInterface; @@ -35,22 +36,37 @@ class DraftAccess implements AccessInterface { protected $nodeAccess; /** + * The moderation service. + * + * @var \Drupal\moderation\ModerationInterface + */ + protected $moderation; + + /** * Constructs a new DraftAccess. * * @param \Drupal\Core\Entity\EntityManagerInterface * The entity manager. */ - public function __construct(EntityManagerInterface $entity_manager) { + public function __construct(EntityManagerInterface $entity_manager, ModerationInterface $moderation) { $this->nodeStorage = $entity_manager->getStorage('node'); $this->nodeAccess = $entity_manager->getAccessControlHandler('node'); + $this->moderation = $moderation; } /** * {@inheritdoc} */ public function access(Route $route, AccountInterface $account, NodeInterface $node = NULL) { + $access_control_handler = \Drupal::service('access_check.node.revision'); + + if ($draft_revision = $this->moderation->getDraftRevisionId($node)) { + $node = $this->nodeStorage->loadRevision($draft_revision); + } + // Check that the user has the ability to update the node, and that the node // has a draft. - return AccessResult::allowedIf($node->access('update', $account) && moderation_node_has_draft($node)); + return AccessResult::allowedIf($access_control_handler->checkAccess($node, $account, 'view') && (boolean) $draft_revision)->addCacheableDependency($node); } + } diff --git a/core/modules/moderation/src/Controller/DraftController.php b/core/modules/moderation/src/Controller/DraftController.php index 63f7111..7e06a72 100644 --- a/core/modules/moderation/src/Controller/DraftController.php +++ b/core/modules/moderation/src/Controller/DraftController.php @@ -6,8 +6,12 @@ namespace Drupal\moderation\Controller; +use Drupal\moderation\ModerationInterface; use Drupal\node\Controller\NodeController; use Drupal\node\NodeInterface; +use Drupal\Core\Datetime\DateFormatterInterface; +use Drupal\Core\Render\RendererInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Page controller for viewing node drafts. @@ -15,20 +19,53 @@ class DraftController extends NodeController { /** + * The moderation service. + * + * @var \Drupal\moderation\ModerationInterface + */ + protected $moderation; + + /** + * Constructs a NodeController object. + * + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\moderation\ModerationInterface. + * The moderation service. + */ + public function __construct(DateFormatterInterface $date_formatter, RendererInterface $renderer, ModerationInterface $moderation) { + parent::__construct($date_formatter, $renderer); + $this->moderation = $moderation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('date.formatter'), + $container->get('renderer'), + $container->get('moderation.node_moderation') + ); + } + + /** * Display current revision denoted as a draft. * * @param \Drupal\node\NodeInterface * The current node. */ public function show(NodeInterface $node) { - return $this->revisionShow(moderation_node_has_draft($node)); + return $this->revisionShow($this->moderation->getDraftRevisionId($node)); } /** * Display the title of the draft. */ public function draftPageTitle(NodeInterface $node) { - return $this->revisionPageTitle(moderation_node_has_draft($node)); + return $this->revisionPageTitle($this->moderation->getDraftRevisionId($node)); } } diff --git a/core/modules/moderation/src/Form/NodeForm.php b/core/modules/moderation/src/Form/NodeForm.php index 05c3b8e..bfdf40a 100644 --- a/core/modules/moderation/src/Form/NodeForm.php +++ b/core/modules/moderation/src/Form/NodeForm.php @@ -6,8 +6,13 @@ namespace Drupal\moderation\Form; +use Drupal\moderation\ModerationInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\user\PrivateTempStoreFactory; use Drupal\Core\Form\FormStateInterface; use Drupal\node\NodeForm as BaseNodeForm; +use Drupal\node\NodeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Override the node form. @@ -15,6 +20,13 @@ class NodeForm extends BaseNodeForm { /** + * The moderation service. + * + * @var \Drupal\moderation\ModerationInterface + */ + protected $moderation; + + /** * Track if this is a draft. * * @var bool @@ -22,14 +34,42 @@ class NodeForm extends BaseNodeForm { protected $isDraft = FALSE; /** + * Constructs a ContentEntityForm object. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory + * The factory for the temp store object. + * @param \Drupal\moderation\ModerationInterface. + * The moderation service. + */ + public function __construct(EntityManagerInterface $entity_manager, PrivateTempStoreFactory $temp_store_factory, ModerationInterface $moderation) { + parent::__construct($entity_manager, $temp_store_factory); + $this->moderation = $moderation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('user.private_tempstore'), + $container->get('moderation.node_moderation') + ); + } + + /** * Ensure proper node revision is used in the node form. * * {@inheritdoc} */ protected function prepareEntity() { + parent::prepareEntity(); + /** @var \Drupal\node\NodeInterface $node */ $node = $this->getEntity(); - if (!$node->isNew() && $node->type->entity->isNewRevision() && $revision_id = moderation_node_has_draft($node)) { + if (!$node->isNew() && $node->type->entity->isNewRevision() && $revision_id = $this->moderation->getDraftRevisionId($node)) { /** @var \Drupal\node\NodeStorage $storage */ $storage = \Drupal::service('entity.manager')->getStorage('node'); $this->entity = $storage->loadRevision($revision_id); @@ -51,10 +91,9 @@ protected function actions(array $form, FormStateInterface $form_state) { $element['draft']['#access'] = TRUE; $element['draft']['#dropbutton'] = 'save'; $element['draft']['#value'] = $this->t('Save as draft'); - // Setting to draft must be called before ::save, while setting the - // redirect must be done after. - array_unshift($element['draft']['#submit'], '::draft'); $element['draft']['#submit'][] = '::setRedirect'; + $element['draft']['#published_status'] = FALSE; + $element['draft']['#is_draft'] = TRUE; // Put the draft button first. $element['draft']['#weight'] = -10; @@ -63,11 +102,24 @@ protected function actions(array $form, FormStateInterface $form_state) { // a published node in a type that defaults to being unpublished, then // only allow new drafts. if (!\Drupal::currentUser()->hasPermission('administer nodes') && $this->nodeTypeUnpublishedDefault()) { - $element['submit']['#access'] = FALSE; + // We can't just set #access to false on submit as it's already hidden + // by parent::actions(). Is there a better way to do this? + if (isset($element['publish'])) { + $element['publish']['#access'] = FALSE; + unset($element['publish']['#dropbutton']); + } + if (isset($element['unpublish'])) { + $element['unpublish']['#access'] = FALSE; + unset($element['unpublish']['#dropbutton']); + } unset($element['draft']['#dropbutton']); } } + if ($this->isDraft && isset($element['unpublish'])) { + $element['unpublish']['#submit'][] = '::setRedirect'; + } + // If this is an existing draft, change the publish button text. if ($this->isDraft && isset($element['publish'])) { $element['publish']['#value'] = t('Save and publish'); @@ -77,23 +129,21 @@ protected function actions(array $form, FormStateInterface $form_state) { } /** - * Save node as a draft. - */ - public function draft(array $form, FormStateInterface $form_state) { - $this->entity->isDefaultRevision(FALSE); - } - - /** * Set default revision if this was previously a draft, and is now being * published. * * {@inheritdoc} */ - public function publish(array $form, FormStateInterface $form_state) { - $node = parent::publish($form, $form_state); - if ($this->isDraft) { - $node->isDefaultRevision(TRUE); - } + function updateStatus($entity_type_id, NodeInterface $node, array $form, FormStateInterface $form_state) { + parent::updateStatus($entity_type_id, $node, $form, $form_state); + + $element = $form_state->getTriggeringElement(); + + $is_published = (boolean) isset($element['#published_status']) ? $element['#published_status'] : FALSE; + $is_draft_revision = (boolean) isset($element['#is_draft']) ? $element['#is_draft'] : $this->isDraft; + $is_default_revision = ($is_published || !$is_draft_revision); + + $node->isDefaultRevision($is_default_revision); } /** diff --git a/core/modules/moderation/src/ModerationInterface.php b/core/modules/moderation/src/ModerationInterface.php new file mode 100644 index 0000000..326fe44 --- /dev/null +++ b/core/modules/moderation/src/ModerationInterface.php @@ -0,0 +1,35 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public function hasDraft(RevisionableInterface $entity) { + return (boolean) $this->getDraftRevisionId($entity); + } + + /** + * {@inheritdoc} + */ + public function getDraftRevisionId(RevisionableInterface $entity) { + $current_revision = $entity->getRevisionId(); + $entity_storage = $this->entityTypeManager->getStorage('node'); + $vids = $entity_storage->revisionIds($entity); + + // Filter out vids less than or equal to current revision. + $filtered = array_filter($vids, function ($vid) use ($current_revision) { + return $vid > $current_revision; + }); + + return array_pop($filtered) ?: 0; + } + +} diff --git a/core/modules/moderation/src/Tests/ModerationNodeTest.php b/core/modules/moderation/src/Tests/ModerationNodeTest.php index f7818a2..7368072 100644 --- a/core/modules/moderation/src/Tests/ModerationNodeTest.php +++ b/core/modules/moderation/src/Tests/ModerationNodeTest.php @@ -33,7 +33,7 @@ class ModerationNodeTest extends NodeTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'moderation']; + public static $modules = ['node', 'moderation', 'block']; /** * {@inheritdoc} @@ -41,6 +41,9 @@ class ModerationNodeTest extends NodeTestBase { function setUp() { parent::setUp(); + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('page_title_block'); + // Use revisions by default. $this->nodeType = NodeType::load('article'); $this->nodeType->setNewRevision(TRUE); @@ -127,13 +130,13 @@ function testBasicForwarRevisions() { $this->drupalGet('node/' . $node->id() . '/edit'); $this->assertRaw($draft_one_values['title[0][value]'], 'The draft title is loaded on the edit form.'); $this->assertRaw($draft_one_values['body[0][value]'], 'The draft body is loaded on the edit form.'); - $this->assertButtons([t('Save as draft'), t('Save and publish'), t('Save and unpublish')]); + $this->assertButtons([t('Save and keep unpublished'), t('Save and publish')]); $this->drupalPostForm('node/' . $node->id() . '/edit', $draft_one_values, t('Save and publish')); // Publish the draft. $this->drupalGet('node/' . $node->id()); $this->assertText($draft_one_values['title[0][value]'], 'Published title found'); - $this->assertText($draft_one_values['body[0][value]'], t('Published body found')); + $this->assertText($draft_one_values['body[0][value]'], 'Published body found'); // For normal users (eg, users without the administer nodes permission), if // a content type is set to be unpublished by default, then on edits, only