diff --git a/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php b/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
index 871aceb365..59fddd5d0e 100644
--- a/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
+++ b/core/lib/Drupal/Core/Entity/EntityPublishedTrait.php
@@ -33,8 +33,7 @@ public static function publishedBaseFieldDefinitions(EntityTypeInterface $entity
}
return [$entity_type->getKey('published') => BaseFieldDefinition::create('boolean')
- ->setLabel(new TranslatableMarkup('Publishing status'))
- ->setDescription(new TranslatableMarkup('A boolean indicating the published state.'))
+ ->setLabel(new TranslatableMarkup('Published'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE)];
diff --git a/core/modules/book/config/optional/core.entity_form_display.node.book.default.yml b/core/modules/book/config/optional/core.entity_form_display.node.book.default.yml
index 58aba45d36..b1db6d18a7 100644
--- a/core/modules/book/config/optional/core.entity_form_display.node.book.default.yml
+++ b/core/modules/book/config/optional/core.entity_form_display.node.book.default.yml
@@ -33,6 +33,13 @@ content:
weight: 15
region: content
third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 120
+ region: content
+ third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
diff --git a/core/modules/book/tests/src/Functional/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php
index 66edec91e0..e52e484e73 100644
--- a/core/modules/book/tests/src/Functional/BookTest.php
+++ b/core/modules/book/tests/src/Functional/BookTest.php
@@ -741,8 +741,8 @@ public function testBookNavigationBlockOnUnpublishedBook() {
$this->drupalPlaceBlock('book_navigation', ['block_mode' => 'book pages']);
// Unpublish book node.
- $edit = [];
- $this->drupalPostForm('node/' . $this->book->id() . '/edit', $edit, t('Save and unpublish'));
+ $edit = ['status[value]' => FALSE];
+ $this->drupalPostForm('node/' . $this->book->id() . '/edit', $edit, t('Save'));
// Test node page.
$this->drupalGet('node/' . $this->book->id());
diff --git a/core/modules/comment/tests/src/Functional/CommentStatusFieldAccessTest.php b/core/modules/comment/tests/src/Functional/CommentStatusFieldAccessTest.php
index 504b3bacf6..e208c7125d 100644
--- a/core/modules/comment/tests/src/Functional/CommentStatusFieldAccessTest.php
+++ b/core/modules/comment/tests/src/Functional/CommentStatusFieldAccessTest.php
@@ -86,14 +86,14 @@ public function testCommentStatusFieldAccessStatus() {
$assert->fieldNotExists('comment[0][status]');
$this->submitForm([
'title[0][value]' => 'Node 1',
- ], t('Save and publish'));
+ ], t('Save'));
$assert->fieldExists('subject[0][value]');
$this->drupalLogin($this->commentAdmin);
$this->drupalGet('node/add/article');
$assert->fieldExists('comment[0][status]');
$this->submitForm([
'title[0][value]' => 'Node 2',
- ], t('Save and publish'));
+ ], t('Save'));
$assert->fieldExists('subject[0][value]');
}
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index 77d822be28..39ffc94c0a 100644
--- a/core/modules/content_moderation/content_moderation.module
+++ b/core/modules/content_moderation/content_moderation.module
@@ -14,6 +14,8 @@
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
@@ -176,6 +178,27 @@ function content_moderation_node_access(NodeInterface $node, $operation, Account
}
/**
+ * Implements hook_entity_field_access().
+ */
+function content_moderation_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
+ if ($items && $operation === 'edit') {
+ /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
+ $moderation_info = Drupal::service('content_moderation.moderation_information');
+
+ $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId());
+
+ $entity = $items->getEntity();
+
+ // Deny edit access to the published field if the entity is being moderated.
+ if ($entity_type->hasKey('published') && $moderation_info->isModeratedEntity($entity) && $entity->moderation_state && $field_definition->getName() == $entity_type->getKey('published')) {
+ return AccessResult::forbidden();
+ }
+ }
+
+ return AccessResult::neutral();
+}
+
+/**
* Implements hook_theme().
*/
function content_moderation_theme() {
diff --git a/core/modules/content_moderation/content_moderation.module.orig b/core/modules/content_moderation/content_moderation.module.orig
new file mode 100644
index 0000000000..77d822be28
--- /dev/null
+++ b/core/modules/content_moderation/content_moderation.module.orig
@@ -0,0 +1,236 @@
+' . t('About') . '';
+ $output .= '
' . t('The Content Moderation module provides moderation for content by applying workflows to content. For more information, see the online documentation for the Content Moderation module.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '
';
+ $output .= '' . t('Uses') . '
';
+ $output .= '';
+ $output .= '- ' . t('Configuring workflows') . '
';
+ $output .= '- ' . t('Enable the Workflow UI module to create, edit and delete content moderation workflows.') . '';
+ $output .= '
- ' . t('Configure Content Moderation permissions') . '
';
+ $output .= '- ' . t('Each transition is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '';
+ $output .= '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->entityBaseFieldInfo($entity_type);
+}
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function content_moderation_entity_type_alter(array &$entity_types) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->entityTypeAlter($entity_types);
+}
+
+/**
+ * Implements hook_entity_operation().
+ */
+function content_moderation_entity_operation(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->entityOperation($entity);
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function content_moderation_entity_presave(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityPresave($entity);
+}
+
+/**
+ * Implements hook_entity_insert().
+ */
+function content_moderation_entity_insert(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityInsert($entity);
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function content_moderation_entity_update(EntityInterface $entity) {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityUpdate($entity);
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->formAlter($form, $form_state, $form_id);
+}
+
+/**
+ * Implements hook_preprocess_HOOK().
+ *
+ * Many default node templates rely on $page to determine whether to output the
+ * node title as part of the node content.
+ */
+function content_moderation_preprocess_node(&$variables) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(ContentPreprocess::class)
+ ->preprocessNode($variables);
+}
+
+/**
+ * Implements hook_entity_extra_field_info().
+ */
+function content_moderation_entity_extra_field_info() {
+ return \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityTypeInfo::class)
+ ->entityExtraFieldInfo();
+}
+
+/**
+ * Implements hook_entity_view().
+ */
+function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
+ \Drupal::service('class_resolver')
+ ->getInstanceFromDefinition(EntityOperations::class)
+ ->entityView($build, $entity, $display, $view_mode);
+}
+
+/**
+ * Implements hook_node_access().
+ *
+ * Nodes in particular should be viewable if unpublished and the user has
+ * the appropriate permission. This permission is therefore effectively
+ * mandatory for any user that wants to moderate things.
+ */
+function content_moderation_node_access(NodeInterface $node, $operation, AccountInterface $account) {
+ /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
+ $moderation_info = Drupal::service('content_moderation.moderation_information');
+
+ $access_result = NULL;
+ if ($operation === 'view') {
+ $access_result = (!$node->isPublished())
+ ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
+ : AccessResult::neutral();
+
+ $access_result->addCacheableDependency($node);
+ }
+ elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state) {
+ /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
+ $transition_validation = \Drupal::service('content_moderation.state_transition_validation');
+
+ $valid_transition_targets = $transition_validation->getValidTransitions($node, $account);
+ $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
+
+ $access_result->addCacheableDependency($node);
+ $access_result->addCacheableDependency($account);
+ $workflow = \Drupal::service('content_moderation.moderation_information')->getWorkflowForEntity($node);
+ $access_result->addCacheableDependency($workflow);
+ foreach ($valid_transition_targets as $valid_transition_target) {
+ $access_result->addCacheableDependency($valid_transition_target);
+ }
+ }
+
+ return $access_result;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function content_moderation_theme() {
+ return ['entity_moderation_form' => ['render element' => 'form']];
+}
+
+/**
+ * Implements hook_action_info_alter().
+ */
+function content_moderation_action_info_alter(&$definitions) {
+
+ // The publish/unpublish actions are not valid on moderated entities. So swap
+ // their implementations out for alternates that will become a no-op on a
+ // moderated node. If another module has already swapped out those classes,
+ // though, we'll be polite and do nothing.
+ if (isset($definitions['node_publish_action']['class']) && $definitions['node_publish_action']['class'] == PublishNode::class) {
+ $definitions['node_publish_action']['class'] = ModerationOptOutPublishNode::class;
+ }
+ if (isset($definitions['node_unpublish_action']['class']) && $definitions['node_unpublish_action']['class'] == UnpublishNode::class) {
+ $definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class;
+ }
+}
+
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function content_moderation_entity_bundle_info_alter(&$bundles) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
+ /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
+ $plugin = $workflow->getTypePlugin();
+ foreach ($plugin->getEntityTypes() as $entity_type_id) {
+ foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
+ if (isset($bundles[$entity_type_id][$bundle_id])) {
+ $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert().
+ */
+function content_moderation_workflow_insert(WorkflowInterface $entity) {
+ // Clear bundle cache so workflow gets added or removed from the bundle
+ // information.
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+ // Clear field cache so extra field is added or removed.
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function content_moderation_workflow_update(WorkflowInterface $entity) {
+ content_moderation_workflow_insert($entity);
+}
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
index 7579afa4e6..f70226ebc9 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php
@@ -63,7 +63,7 @@ public function testCreatingContent() {
// Create a new node.
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'non-moderated content',
- ], t('Save and publish'));
+ ], t('Save'));
$node = $this->getNodeByTitle('non-moderated content');
if (!$node) {
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php.orig b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php.orig
new file mode 100644
index 0000000000..7579afa4e6
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php.orig
@@ -0,0 +1,141 @@
+drupalLogin($this->adminUser);
+ $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
+ $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
+ }
+
+ /**
+ * Tests creating and deleting content.
+ */
+ public function testCreatingContent() {
+ $this->drupalPostForm('node/add/moderated_content', [
+ 'title[0][value]' => 'moderated content',
+ ], t('Save and Create New Draft'));
+ $node = $this->getNodeByTitle('moderated content');
+ if (!$node) {
+ $this->fail('Test node was not saved correctly.');
+ }
+ $this->assertEqual('draft', $node->moderation_state->value);
+
+ $path = 'node/' . $node->id() . '/edit';
+ // Set up published revision.
+ $this->drupalPostForm($path, [], t('Save and Publish'));
+ \Drupal::entityTypeManager()->getStorage('node')->resetCache([$node->id()]);
+ /* @var \Drupal\node\NodeInterface $node */
+ $node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
+ $this->assertTrue($node->isPublished());
+ $this->assertEqual('published', $node->moderation_state->value);
+
+ // Verify that the state field is not shown.
+ $this->assertNoText('Published');
+
+ // Delete the node.
+ $this->drupalPostForm('node/' . $node->id() . '/delete', [], t('Delete'));
+ $this->assertText(t('The Moderated content moderated content has been deleted.'));
+
+ // Disable content moderation.
+ $this->drupalPostForm('admin/structure/types/manage/moderated_content/moderation', ['workflow' => ''], t('Save'));
+ $this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
+ $this->assertOptionSelected('edit-workflow', '');
+ // Ensure the parent environment is up-to-date.
+ // @see content_moderation_workflow_insert()
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Create a new node.
+ $this->drupalPostForm('node/add/moderated_content', [
+ 'title[0][value]' => 'non-moderated content',
+ ], t('Save and publish'));
+
+ $node = $this->getNodeByTitle('non-moderated content');
+ if (!$node) {
+ $this->fail('Non-moderated test node was not saved correctly.');
+ }
+ $this->assertEqual(NULL, $node->moderation_state->value);
+ }
+
+ /**
+ * Tests edit form destinations.
+ */
+ public function testFormSaveDestination() {
+ // Create new moderated content in draft.
+ $this->drupalPostForm('node/add/moderated_content', [
+ 'title[0][value]' => 'Some moderated content',
+ 'body[0][value]' => 'First version of the content.',
+ ], t('Save and Create New Draft'));
+
+ $node = $this->drupalGetNodeByTitle('Some moderated content');
+ $edit_path = sprintf('node/%d/edit', $node->id());
+
+ // After saving, we should be at the canonical URL and viewing the first
+ // revision.
+ $this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
+ $this->assertText('First version of the content.');
+
+ // Create a new draft; after saving, we should still be on the canonical
+ // URL, but viewing the second revision.
+ $this->drupalPostForm($edit_path, [
+ 'body[0][value]' => 'Second version of the content.',
+ ], t('Save and Create New Draft'));
+ $this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
+ $this->assertText('Second version of the content.');
+
+ // Make a new published revision; after saving, we should be at the
+ // canonical URL.
+ $this->drupalPostForm($edit_path, [
+ 'body[0][value]' => 'Third version of the content.',
+ ], t('Save and Publish'));
+ $this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
+ $this->assertText('Third version of the content.');
+
+ // Make a new forward revision; after saving, we should be on the "Latest
+ // version" tab.
+ $this->drupalPostForm($edit_path, [
+ 'body[0][value]' => 'Fourth version of the content.',
+ ], t('Save and Create New Draft'));
+ $this->assertUrl(Url::fromRoute('entity.node.latest_version', ['node' => $node->id()]));
+ $this->assertText('Fourth version of the content.');
+ }
+
+ /**
+ * Tests pagers aren't broken by content_moderation.
+ */
+ public function testPagers() {
+ // Create 51 nodes to force the pager.
+ foreach (range(1, 51) as $delta) {
+ Node::create([
+ 'type' => 'moderated_content',
+ 'uid' => $this->adminUser->id(),
+ 'title' => 'Node ' . $delta,
+ 'status' => 1,
+ 'moderation_state' => 'published',
+ ])->save();
+ }
+ $this->drupalLogin($this->adminUser);
+ $this->drupalGet('admin/content');
+ $element = $this->cssSelect('nav.pager li.is-active a');
+ $url = $element[0]->getAttribute('href');
+ $query = [];
+ parse_str(parse_url($url, PHP_URL_QUERY), $query);
+ $this->assertEqual(0, $query['page']);
+ }
+
+}
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php
index 8391dd0aae..6b5e167403 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php
@@ -18,10 +18,10 @@ public function testNotModerated() {
$this->assertText('The content type Not moderated has been added.');
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
$this->drupalGet('node/add/not_moderated');
- $this->assertRaw('Save as unpublished');
+ $this->assertRaw('Save');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Test',
- ], t('Save and publish'));
+ ], t('Save'));
$this->assertText('Not moderated Test has been created.');
}
@@ -53,7 +53,7 @@ public function testEnablingOnExistingContent() {
$this->drupalGet('node/add/not_moderated');
$this->drupalPostForm(NULL, [
'title[0][value]' => 'Test',
- ], t('Save and publish'));
+ ], t('Save'));
$this->assertText('Not moderated Test has been created.');
// Now enable moderation state, ensuring all the expected links and tabs are
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php.orig b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php.orig
new file mode 100644
index 0000000000..8391dd0aae
--- /dev/null
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTypeTest.php.orig
@@ -0,0 +1,94 @@
+drupalLogin($this->adminUser);
+ $this->createContentTypeFromUi('Not moderated', 'not_moderated');
+ $this->assertText('The content type Not moderated has been added.');
+ $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
+ $this->drupalGet('node/add/not_moderated');
+ $this->assertRaw('Save as unpublished');
+ $this->drupalPostForm(NULL, [
+ 'title[0][value]' => 'Test',
+ ], t('Save and publish'));
+ $this->assertText('Not moderated Test has been created.');
+ }
+
+ /**
+ * Tests enabling moderation on an existing node-type, with content.
+ */
+ public function testEnablingOnExistingContent() {
+ $editor_permissions = [
+ 'administer content moderation',
+ 'access administration pages',
+ 'administer content types',
+ 'administer nodes',
+ 'view latest version',
+ 'view any unpublished content',
+ 'access content overview',
+ 'use editorial transition create_new_draft',
+ ];
+ $publish_permissions = array_merge($editor_permissions, ['use editorial transition publish']);
+ $editor = $this->drupalCreateUser($editor_permissions);
+ $editor_with_publish = $this->drupalCreateUser($publish_permissions);
+
+ // Create a node type that is not moderated.
+ $this->drupalLogin($editor);
+ $this->createContentTypeFromUi('Not moderated', 'not_moderated');
+ $this->grantUserPermissionToCreateContentOfType($editor, 'not_moderated');
+ $this->grantUserPermissionToCreateContentOfType($editor_with_publish, 'not_moderated');
+
+ // Create content.
+ $this->drupalGet('node/add/not_moderated');
+ $this->drupalPostForm(NULL, [
+ 'title[0][value]' => 'Test',
+ ], t('Save and publish'));
+ $this->assertText('Not moderated Test has been created.');
+
+ // Now enable moderation state, ensuring all the expected links and tabs are
+ // present.
+ $this->drupalGet('admin/structure/types');
+ $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
+ $this->drupalGet('admin/structure/types/manage/not_moderated');
+ $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
+ $this->drupalGet('admin/structure/types/manage/not_moderated/moderation');
+ $this->assertOptionSelected('edit-workflow', '');
+ $this->assertNoLink('Delete');
+ $edit['workflow'] = 'editorial';
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+
+ // And make sure it works.
+ $nodes = \Drupal::entityTypeManager()->getStorage('node')
+ ->loadByProperties(['title' => 'Test']);
+ if (empty($nodes)) {
+ $this->fail('Could not load node with title Test');
+ return;
+ }
+ $node = reset($nodes);
+ $this->drupalGet('node/' . $node->id());
+ $this->assertResponse(200);
+ $this->assertLinkByHref('node/' . $node->id() . '/edit');
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertResponse(200);
+ $this->assertRaw('Save and Create New Draft');
+ $this->assertNoRaw('Save and Publish');
+
+ $this->drupalLogin($editor_with_publish);
+ $this->drupalGet('node/' . $node->id() . '/edit');
+ $this->assertResponse(200);
+ $this->assertRaw('Save and Create New Draft');
+ $this->assertRaw('Save and Publish');
+ }
+
+}
diff --git a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
index f3c27146d4..c939c52c2d 100644
--- a/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/NodeAccessTest.php
@@ -46,7 +46,7 @@ class NodeAccessTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
- $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
+ $this->createContentTypeFromUi('Moderated content', 'moderated_content', FALSE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
// Rebuild permissions because hook_node_grants() is implemented by the
@@ -60,8 +60,20 @@ protected function setUp() {
public function testPageAccess() {
$this->drupalLogin($this->adminUser);
+ // Access the node form before moderation is enabled, the publication state
+ // should now be visible.
+ $this->drupalGet('node/add/moderated_content');
+ $this->assertSession()->fieldExists('Published');
+
+ // Now enable the workflow.
+ $this->enableModerationThroughUi('moderated_content', 'editorial');
+
+ // Access that the status field is no longer visible.
+ $this->drupalGet('node/add/moderated_content');
+ $this->assertSession()->fieldNotExists('Published');
+
// Create a node to test with.
- $this->drupalPostForm('node/add/moderated_content', [
+ $this->drupalPostForm(NULL, [
'title[0][value]' => 'moderated content',
], t('Save and Create New Draft'));
$node = $this->getNodeByTitle('moderated content');
diff --git a/core/modules/content_translation/tests/src/Functional/ContentTranslationLanguageChangeTest.php b/core/modules/content_translation/tests/src/Functional/ContentTranslationLanguageChangeTest.php
index 3236a592c3..ae326ef67b 100644
--- a/core/modules/content_translation/tests/src/Functional/ContentTranslationLanguageChangeTest.php
+++ b/core/modules/content_translation/tests/src/Functional/ContentTranslationLanguageChangeTest.php
@@ -73,12 +73,12 @@ public function testLanguageChange() {
$edit = [
'title[0][value]' => 'english_title',
];
- $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
// Create a translation in French.
$this->clickLink('Translate');
$this->clickLink('Add');
- $this->drupalPostForm(NULL, [], t('Save and keep published (this translation)'));
+ $this->drupalPostForm(NULL, [], t('Save (this translation)'));
$this->clickLink('Translate');
// Edit English translation.
@@ -90,7 +90,7 @@ public function testLanguageChange() {
'files[field_image_field_0]' => $images->uri,
];
$this->drupalPostForm(NULL, $edit, t('Upload'));
- $this->drupalPostForm(NULL, ['field_image_field[0][alt]' => 'alternative_text'], t('Save and keep published (this translation)'));
+ $this->drupalPostForm(NULL, ['field_image_field[0][alt]' => 'alternative_text'], t('Save (this translation)'));
// Check that the translation languages are correct.
$node = $this->getNodeByTitle('english_title');
@@ -109,13 +109,13 @@ public function testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjax
'title[0][value]' => 'english_title',
'test_field_only_en_fr' => 'node created',
];
- $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertEqual('node created', \Drupal::state()->get('test_field_only_en_fr'));
// Create a translation in French.
$this->clickLink('Translate');
$this->clickLink('Add');
- $this->drupalPostForm(NULL, [], t('Save and keep published (this translation)'));
+ $this->drupalPostForm(NULL, [], t('Save (this translation)'));
$this->clickLink('Translate');
// Edit English translation.
@@ -137,7 +137,7 @@ public function testTitleDoesNotChangesOnChangingLanguageWidgetAndTriggeringAjax
'langcode[0][value]' => 'en',
'field_image_field[0][alt]' => 'alternative_text'
];
- $this->drupalPostForm(NULL, $edit, t('Save and keep published (this translation)'));
+ $this->drupalPostForm(NULL, $edit, t('Save (this translation)'));
// Check that the translation languages are correct.
$node = $this->getNodeByTitle('english_title');
diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php
index 46986ec474..b161ed66d8 100644
--- a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php
+++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceXSSTest.php
@@ -62,7 +62,7 @@ public function testEntityReferenceXSS() {
'title[0][value]' => $this->randomString(),
'entity_reference_test' => $referenced_node->id()
];
- $this->drupalPostForm(NULL, $edit, 'Save and publish');
+ $this->drupalPostForm(NULL, $edit, 'Save');
$this->assertEscaped($referenced_node->getTitle());
// Test the options_buttons type.
diff --git a/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php b/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php
index b59b1bafd3..66d87cd7e8 100644
--- a/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php
+++ b/core/modules/file/src/Tests/FileFieldAnonymousSubmissionTest.php
@@ -138,7 +138,7 @@ protected function doTestNodeWithFileWithoutTitle() {
$label = 'Save';
}
else {
- $label = 'Save and publish';
+ $label = 'Save';
}
$this->drupalPostForm(NULL, $edit, $label);
$this->assertResponse(200);
diff --git a/core/modules/file/src/Tests/FileFieldDisplayTest.php b/core/modules/file/src/Tests/FileFieldDisplayTest.php
index 1b7e534211..db9b7dc91b 100644
--- a/core/modules/file/src/Tests/FileFieldDisplayTest.php
+++ b/core/modules/file/src/Tests/FileFieldDisplayTest.php
@@ -77,7 +77,7 @@ public function testNodeDisplay() {
// Turn the "display" option off and check that the file is no longer displayed.
$edit = [$field_name . '[0][display]' => FALSE];
- $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save'));
$this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.');
@@ -87,7 +87,7 @@ public function testNodeDisplay() {
$field_name . '[0][description]' => $description,
$field_name . '[0][display]' => TRUE,
];
- $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save'));
$this->assertText($description);
// Ensure the filename in the link's title attribute is escaped.
@@ -167,7 +167,7 @@ public function testDescToggle() {
'title[0][value]' => $title,
'files[field_' . $field_name . '_0]' => drupal_realpath($file->uri),
];
- $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertText(t('The description may be used as the label of the link to the file.'));
diff --git a/core/modules/file/src/Tests/FileFieldRevisionTest.php b/core/modules/file/src/Tests/FileFieldRevisionTest.php
index 7de496ff8a..7b7b4d3c7a 100644
--- a/core/modules/file/src/Tests/FileFieldRevisionTest.php
+++ b/core/modules/file/src/Tests/FileFieldRevisionTest.php
@@ -63,7 +63,7 @@ public function testRevisions() {
// Save a new version of the node without any changes.
// Check that the file is still the same as the previous revision.
- $this->drupalPostForm('node/' . $nid . '/edit', ['revision' => '1'], t('Save and keep published'));
+ $this->drupalPostForm('node/' . $nid . '/edit', ['revision' => '1'], t('Save'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$node_file_r3 = File::load($node->{$field_name}->target_id);
diff --git a/core/modules/file/src/Tests/FileFieldTestBase.php b/core/modules/file/src/Tests/FileFieldTestBase.php
index 73cc96ad50..b7b90d7d37 100644
--- a/core/modules/file/src/Tests/FileFieldTestBase.php
+++ b/core/modules/file/src/Tests/FileFieldTestBase.php
@@ -227,7 +227,7 @@ public function uploadNodeFiles(array $files, $field_name, $nid_or_type, $new_re
$edit[$name][] = $file_path;
}
}
- $this->drupalPostForm("node/$nid/edit", $edit, t('Save and keep published'));
+ $this->drupalPostForm("node/$nid/edit", $edit, t('Save'));
return $nid;
}
@@ -243,7 +243,7 @@ public function removeNodeFile($nid, $new_revision = TRUE) {
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
}
/**
@@ -256,7 +256,7 @@ public function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE)
];
$this->drupalPostForm('node/' . $nid . '/edit', [], t('Remove'));
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
}
/**
diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php
index 5ddcdde3f8..422bf68282 100644
--- a/core/modules/file/src/Tests/FileFieldValidateTest.php
+++ b/core/modules/file/src/Tests/FileFieldValidateTest.php
@@ -29,7 +29,7 @@ public function testRequired() {
// Try to post a new node without uploading a file.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
- $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save'));
$this->assertRaw(t('@title field is required.', ['@title' => $field->getLabel()]), 'Node save failed when required file field was empty.');
// Create a new node with the uploaded file.
@@ -50,7 +50,7 @@ public function testRequired() {
// Try to post a new node without uploading a file in the multivalue field.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
- $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save'));
$this->assertRaw(t('@title field is required.', ['@title' => $field->getLabel()]), 'Node save failed when required multiple value file field was empty.');
// Create a new node with the uploaded file into the multivalue field.
diff --git a/core/modules/file/src/Tests/FileFieldWidgetTest.php b/core/modules/file/src/Tests/FileFieldWidgetTest.php
index ddfd8f4e79..ad47d77841 100644
--- a/core/modules/file/src/Tests/FileFieldWidgetTest.php
+++ b/core/modules/file/src/Tests/FileFieldWidgetTest.php
@@ -121,7 +121,7 @@ public function testSingleValuedWidget() {
$this->assertTrue(isset($label[0]), 'Label for upload found.');
// Save the node and ensure it does not have the file.
- $this->drupalPostForm(NULL, [], t('Save and keep published'));
+ $this->drupalPostForm(NULL, [], t('Save'));
$node_storage->resetCache([$nid]);
$node = $node_storage->load($nid);
$this->assertTrue(empty($node->{$field_name}->target_id), 'File was successfully removed from the node.');
@@ -238,8 +238,7 @@ public function testMultiValuedWidget() {
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', ['%type' => $type]));
// Save the node and ensure it does not have any files.
- $this->drupalPostForm(NULL, ['title[0][value]' => $this->randomMachineName()], t('Save and publish'));
- $matches = [];
+ $this->drupalPostForm(NULL, ['title[0][value]' => $this->randomMachineName()], t('Save'));
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
$nid = $matches[1];
$node_storage->resetCache([$nid]);
@@ -368,7 +367,7 @@ public function testPrivateFileComment() {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
- $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/article', $edit, t('Save'));
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
// Add a comment with a file.
@@ -402,7 +401,8 @@ public function testPrivateFileComment() {
// Unpublishes node.
$this->drupalLogin($this->adminUser);
- $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and unpublish'));
+ $edit = ['status[value]' => FALSE];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Ensures normal user can no longer download the file.
$this->drupalLogin($user);
diff --git a/core/modules/file/src/Tests/FilePrivateTest.php b/core/modules/file/src/Tests/FilePrivateTest.php
index c5771c6aed..6808a4782b 100644
--- a/core/modules/file/src/Tests/FilePrivateTest.php
+++ b/core/modules/file/src/Tests/FilePrivateTest.php
@@ -73,10 +73,10 @@ public function testPrivateFile() {
// Attempt to reuse the file when editing a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
- $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$edit[$field_name . '[0][fids]'] = $node_file->id();
- $this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save'));
// Make sure the form submit failed - we stayed on the edit form.
$this->assertUrl('node/' . $new_node->id() . '/edit');
// Check that we got the expected constraint form error.
@@ -87,7 +87,7 @@ public function testPrivateFile() {
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName();
$edit[$field_name . '[0][fids]'] = $node_file->id();
- $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save'));
$new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertTrue(empty($new_node), 'Node was not created.');
$this->assertUrl('node/add/' . $type_name);
diff --git a/core/modules/file/src/Tests/FilePrivateTest.php.orig b/core/modules/file/src/Tests/FilePrivateTest.php.orig
new file mode 100644
index 0000000000..c5771c6aed
--- /dev/null
+++ b/core/modules/file/src/Tests/FilePrivateTest.php.orig
@@ -0,0 +1,228 @@
+set('node_access_test.private', TRUE);
+ }
+
+ /**
+ * Tests file access for file uploaded to a private node.
+ */
+ public function testPrivateFile() {
+ $node_storage = $this->container->get('entity.manager')->getStorage('node');
+ $type_name = 'article';
+ $field_name = strtolower($this->randomMachineName());
+ $this->createFileField($field_name, 'node', $type_name, ['uri_scheme' => 'private']);
+
+ $test_file = $this->getTestFile('text');
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, ['private' => TRUE]);
+ \Drupal::entityManager()->getStorage('node')->resetCache([$nid]);
+ /* @var \Drupal\node\NodeInterface $node */
+ $node = $node_storage->load($nid);
+ $node_file = File::load($node->{$field_name}->target_id);
+ // Ensure the file can be viewed.
+ $this->drupalGet('node/' . $node->id());
+ $this->assertRaw($node_file->getFilename(), 'File reference is displayed after attaching it');
+ // Ensure the file can be downloaded.
+ $this->drupalGet(file_create_url($node_file->getFileUri()));
+ $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
+ $this->drupalLogOut();
+ $this->drupalGet(file_create_url($node_file->getFileUri()));
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
+
+ // Create a field with no view access. See
+ // field_test_entity_field_access().
+ $no_access_field_name = 'field_no_view_access';
+ $this->createFileField($no_access_field_name, 'node', $type_name, ['uri_scheme' => 'private']);
+ // Test with the field that should deny access through field access.
+ $this->drupalLogin($this->adminUser);
+ $nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, ['private' => TRUE]);
+ \Drupal::entityManager()->getStorage('node')->resetCache([$nid]);
+ $node = $node_storage->load($nid);
+ $node_file = File::load($node->{$no_access_field_name}->target_id);
+
+ // Ensure the file cannot be downloaded.
+ $file_url = file_create_url($node_file->getFileUri());
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.');
+
+ // Attempt to reuse the file when editing a node.
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName();
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $edit[$field_name . '[0][fids]'] = $node_file->id();
+ $this->drupalPostForm('node/' . $new_node->id() . '/edit', $edit, t('Save and keep published'));
+ // Make sure the form submit failed - we stayed on the edit form.
+ $this->assertUrl('node/' . $new_node->id() . '/edit');
+ // Check that we got the expected constraint form error.
+ $constraint = new ReferenceAccessConstraint();
+ $this->assertRaw(SafeMarkup::format($constraint->message, ['%type' => 'file', '%id' => $node_file->id()]));
+ // Attempt to reuse the existing file when creating a new node, and confirm
+ // that access is still denied.
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName();
+ $edit[$field_name . '[0][fids]'] = $node_file->id();
+ $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish'));
+ $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $this->assertTrue(empty($new_node), 'Node was not created.');
+ $this->assertUrl('node/add/' . $type_name);
+ $this->assertRaw(SafeMarkup::format($constraint->message, ['%type' => 'file', '%id' => $node_file->id()]));
+
+ // Now make file_test_file_download() return everything.
+ \Drupal::state()->set('file_test.allow_all', TRUE);
+ // Delete the node.
+ $node->delete();
+ // Ensure the file can still be downloaded by the owner.
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Confirmed that the owner still has access to the temporary file.');
+
+ // Ensure the file cannot be downloaded by an anonymous user.
+ $this->drupalLogout();
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that access is denied for an anonymous user to the temporary file.');
+
+ // Ensure the file cannot be downloaded by another user.
+ $account = $this->drupalCreateUser();
+ $this->drupalLogin($account);
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that access is denied for another user to the temporary file.');
+
+ // As an anonymous user, create a temporary file with no references and
+ // confirm that only the session that uploaded it may view it.
+ $this->drupalLogout();
+ user_role_change_permissions(
+ RoleInterface::ANONYMOUS_ID,
+ [
+ "create $type_name content" => TRUE,
+ 'access content' => TRUE,
+ ]
+ );
+ $test_file = $this->getTestFile('text');
+ $this->drupalGet('node/add/' . $type_name);
+ $edit = ['files[' . $field_name . '_0]' => drupal_realpath($test_file->getFileUri())];
+ $this->drupalPostForm(NULL, $edit, t('Upload'));
+ /** @var \Drupal\file\FileStorageInterface $file_storage */
+ $file_storage = $this->container->get('entity.manager')->getStorage('file');
+ $files = $file_storage->loadByProperties(['uid' => 0]);
+ $this->assertEqual(1, count($files), 'Loaded one anonymous file.');
+ $file = end($files);
+ $this->assertTrue($file->isTemporary(), 'File is temporary.');
+ $usage = $this->container->get('file.usage')->listUsage($file);
+ $this->assertFalse($usage, 'No file usage found.');
+ $file_url = file_create_url($file->getFileUri());
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the temporary file.');
+ // Close the prior connection and remove the session cookie.
+ $this->curlClose();
+ $this->curlCookies = [];
+ $this->cookies = [];
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the temporary file.');
+
+ // As an anonymous user, create a permanent file, then remove all
+ // references to the file (so that it becomes temporary again) and confirm
+ // that only the session that uploaded it may view it.
+ $test_file = $this->getTestFile('text');
+ $this->drupalGet('node/add/' . $type_name);
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName();
+ $edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $file_id = $new_node->{$field_name}->target_id;
+ $file = File::load($file_id);
+ $this->assertTrue($file->isPermanent(), 'File is permanent.');
+ // Remove the reference to this file.
+ $new_node->{$field_name} = [];
+ $new_node->save();
+ $file = File::load($file_id);
+ $this->assertTrue($file->isTemporary(), 'File is temporary.');
+ $usage = $this->container->get('file.usage')->listUsage($file);
+ $this->assertFalse($usage, 'No file usage found.');
+ $file_url = file_create_url($file->getFileUri());
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the file whose references were removed.');
+ // Close the prior connection and remove the session cookie.
+ $this->curlClose();
+ $this->curlCookies = [];
+ $this->cookies = [];
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the file whose references were removed.');
+
+ // As an anonymous user, create a permanent file that is referenced by a
+ // published node and confirm that all anonymous users may view it.
+ $test_file = $this->getTestFile('text');
+ $this->drupalGet('node/add/' . $type_name);
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName();
+ $edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $file = File::load($new_node->{$field_name}->target_id);
+ $this->assertTrue($file->isPermanent(), 'File is permanent.');
+ $usage = $this->container->get('file.usage')->listUsage($file);
+ $this->assertTrue($usage, 'File usage found.');
+ $file_url = file_create_url($file->getFileUri());
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Confirmed that the anonymous uploader has access to the permanent file that is referenced by a published node.');
+ // Close the prior connection and remove the session cookie.
+ $this->curlClose();
+ $this->curlCookies = [];
+ $this->cookies = [];
+ $this->drupalGet($file_url);
+ $this->assertResponse(200, 'Confirmed that another anonymous user also has access to the permanent file that is referenced by a published node.');
+
+ // As an anonymous user, create a permanent file that is referenced by an
+ // unpublished node and confirm that no anonymous users may view it (even
+ // the session that uploaded the file) because they cannot view the
+ // unpublished node.
+ $test_file = $this->getTestFile('text');
+ $this->drupalGet('node/add/' . $type_name);
+ $edit = [];
+ $edit['title[0][value]'] = $this->randomMachineName();
+ $edit['files[' . $field_name . '_0]'] = drupal_realpath($test_file->getFileUri());
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $new_node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
+ $new_node->setPublished(FALSE);
+ $new_node->save();
+ $file = File::load($new_node->{$field_name}->target_id);
+ $this->assertTrue($file->isPermanent(), 'File is permanent.');
+ $usage = $this->container->get('file.usage')->listUsage($file);
+ $this->assertTrue($usage, 'File usage found.');
+ $file_url = file_create_url($file->getFileUri());
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that the anonymous uploader cannot access the permanent file when it is referenced by an unpublished node.');
+ // Close the prior connection and remove the session cookie.
+ $this->curlClose();
+ $this->curlCookies = [];
+ $this->cookies = [];
+ $this->drupalGet($file_url);
+ $this->assertResponse(403, 'Confirmed that another anonymous user cannot access the permanent file when it is referenced by an unpublished node.');
+ }
+
+}
diff --git a/core/modules/filter/tests/src/Functional/FilterHooksTest.php b/core/modules/filter/tests/src/Functional/FilterHooksTest.php
index c799460988..5ff4a8a487 100644
--- a/core/modules/filter/tests/src/Functional/FilterHooksTest.php
+++ b/core/modules/filter/tests/src/Functional/FilterHooksTest.php
@@ -60,7 +60,7 @@ public function testFilterHooks() {
$edit['title[0][value]'] = $title;
$edit['body[0][value]'] = $this->randomMachineName(32);
$edit['body[0][format]'] = $format_id;
- $this->drupalPostForm("node/add/{$type->id()}", $edit, t('Save and publish'));
+ $this->drupalPostForm("node/add/{$type->id()}", $edit, t('Save'));
$this->assertText(t('@type @title has been created.', ['@type' => $type_name, '@title' => $title]));
// Disable the text format.
diff --git a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
index 6773d32d23..965a691c9e 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
@@ -42,6 +42,13 @@ content:
weight: 15
region: content
third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 120
+ region: content
+ third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
diff --git a/core/modules/forum/tests/src/Functional/ForumBlockTest.php b/core/modules/forum/tests/src/Functional/ForumBlockTest.php
index 6c412b71cd..2914e94e8a 100644
--- a/core/modules/forum/tests/src/Functional/ForumBlockTest.php
+++ b/core/modules/forum/tests/src/Functional/ForumBlockTest.php
@@ -170,7 +170,7 @@ protected function createForumTopics($count = 5) {
];
// Create the forum topic, preselecting the forum ID via a URL parameter.
- $this->drupalPostForm('node/add/forum', $edit, t('Save and publish'), ['query' => ['forum_id' => 1]]);
+ $this->drupalPostForm('node/add/forum', $edit, t('Save'), ['query' => ['forum_id' => 1]]);
$topics[] = $title;
}
diff --git a/core/modules/forum/tests/src/Functional/ForumIndexTest.php b/core/modules/forum/tests/src/Functional/ForumIndexTest.php
index 53c8364db0..38adb72ea8 100644
--- a/core/modules/forum/tests/src/Functional/ForumIndexTest.php
+++ b/core/modules/forum/tests/src/Functional/ForumIndexTest.php
@@ -44,7 +44,7 @@ public function testForumIndexStatus() {
$this->drupalGet("forum/$tid");
$this->clickLink(t('Add new @node_type', ['@node_type' => 'Forum topic']));
$this->assertUrl('node/add/forum', ['query' => ['forum_id' => $tid]]);
- $this->drupalPostForm(NULL, $edit, t('Save and publish'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($title);
@@ -71,7 +71,8 @@ public function testForumIndexStatus() {
// Unpublish the node.
- $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and unpublish'));
+ $edit = ['status[value]' => FALSE];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
$this->drupalGet('node/' . $node->id());
$this->assertText(t('Access denied'), 'Unpublished node is no longer accessible.');
diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
index 9c105c1434..8061526da2 100644
--- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
@@ -282,7 +282,7 @@ public function testImageFieldSettings() {
$field_name . '[0][alt]' => $image['#alt'],
$field_name . '[0][title]' => $image['#title'],
];
- $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save'));
$default_output = str_replace("\n", NULL, $renderer->renderRoot($image));
$this->assertRaw($default_output, 'Image displayed using user supplied alt and title attributes.');
@@ -292,7 +292,7 @@ public function testImageFieldSettings() {
$field_name . '[0][alt]' => $this->randomMachineName($test_size),
$field_name . '[0][title]' => $this->randomMachineName($test_size),
];
- $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save'));
$schema = $field->getFieldStorageDefinition()->getSchema();
$this->assertRaw(t('Alternative text cannot be longer than %max characters but is currently %length characters long.', [
'%max' => $schema['columns']['alt']['length'],
@@ -314,9 +314,9 @@ public function testImageFieldSettings() {
$edit = [
'files[' . $field_name . '_1][]' => drupal_realpath($test_image->uri),
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Add the required alt text.
- $this->drupalPostForm(NULL, [$field_name . '[1][alt]' => $alt], t('Save and keep published'));
+ $this->drupalPostForm(NULL, [$field_name . '[1][alt]' => $alt], t('Save'));
$this->assertText(format_string('Article @title has been updated.', ['@title' => $node->getTitle()]));
// Assert ImageWidget::process() calls FieldWidget::process().
diff --git a/core/modules/image/src/Tests/ImageFieldTestBase.php b/core/modules/image/src/Tests/ImageFieldTestBase.php
index eec7f7bba9..c023917e36 100644
--- a/core/modules/image/src/Tests/ImageFieldTestBase.php
+++ b/core/modules/image/src/Tests/ImageFieldTestBase.php
@@ -90,10 +90,10 @@ public function uploadNodeImage($image, $field_name, $type, $alt = '') {
'title[0][value]' => $this->randomMachineName(),
];
$edit['files[' . $field_name . '_0]'] = drupal_realpath($image->uri);
- $this->drupalPostForm('node/add/' . $type, $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/' . $type, $edit, t('Save'));
if ($alt) {
// Add alt text.
- $this->drupalPostForm(NULL, [$field_name . '[0][alt]' => $alt], t('Save and publish'));
+ $this->drupalPostForm(NULL, [$field_name . '[0][alt]' => $alt], t('Save'));
}
// Retrieve ID of the newly created node from the current URL.
diff --git a/core/modules/image/src/Tests/ImageFieldValidateTest.php b/core/modules/image/src/Tests/ImageFieldValidateTest.php
index 7e5ee9bae7..e429d0bb20 100644
--- a/core/modules/image/src/Tests/ImageFieldValidateTest.php
+++ b/core/modules/image/src/Tests/ImageFieldValidateTest.php
@@ -119,7 +119,7 @@ public function testRequiredAttributes() {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
- $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/article', $edit, t('Save'));
$this->assertNoText(t('Alternative text field is required.'));
$this->assertNoText(t('Title field is required.'));
@@ -132,7 +132,7 @@ public function testRequiredAttributes() {
$edit = [
'title[0][value]' => $this->randomMachineName(),
];
- $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/article', $edit, t('Save'));
$this->assertNoText(t('Alternative text field is required.'));
$this->assertNoText(t('Title field is required.'));
diff --git a/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php
index bce4ff8bd4..2007e1e266 100644
--- a/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php
+++ b/core/modules/inline_form_errors/tests/src/FunctionalJavascript/FormErrorHandlerCKEditorTest.php
@@ -85,7 +85,7 @@ public function testFragmentLink() {
// Only enter a title in the node add form and leave the body field empty.
$edit = ['edit-title-0-value' => 'Test inline form error with CKEditor'];
- $this->submitForm($edit, 'Save and publish');
+ $this->submitForm($edit, 'Save');
// Add a bottom margin to the title field to be sure the body field is not
// visible. PhantomJS runs with a resolution of 1024x768px.
diff --git a/core/modules/language/tests/src/Functional/LanguageNegotiationUrlTest.php b/core/modules/language/tests/src/Functional/LanguageNegotiationUrlTest.php
index 8472ff77f5..07ae85d433 100644
--- a/core/modules/language/tests/src/Functional/LanguageNegotiationUrlTest.php
+++ b/core/modules/language/tests/src/Functional/LanguageNegotiationUrlTest.php
@@ -70,7 +70,7 @@ public function testDomain() {
'title[0][value]' => 'Test',
'path[0][alias]' => '/eng/test',
];
- $this->drupalPostForm('node/add/article', $nodeValues, $this->t('Save and publish'));
+ $this->drupalPostForm('node/add/article', $nodeValues, $this->t('Save'));
$this->assertSession()->statusCodeEquals(200);
}
diff --git a/core/modules/menu_ui/src/Tests/MenuNodeTest.php b/core/modules/menu_ui/src/Tests/MenuNodeTest.php
index 3f1bba06ee..99900955fb 100644
--- a/core/modules/menu_ui/src/Tests/MenuNodeTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuNodeTest.php
@@ -134,8 +134,7 @@ public function testMenuNodeFormWidget() {
$this->drupalGet('test-page');
$this->assertNoLink($node_title);
- // Use not only the save button, but also the two special buttons:
- // 'Save and publish' as well as 'Save and keep published'.
+ // Make sure the menu links only appear when the node is published.
// These buttons just appear for 'administer nodes' users.
$admin_user = $this->drupalCreateUser([
'access administration pages',
@@ -146,21 +145,20 @@ public function testMenuNodeFormWidget() {
'edit any page content',
]);
$this->drupalLogin($admin_user);
- foreach (['Save and unpublish' => FALSE, 'Save and keep unpublished' => FALSE, 'Save and publish' => TRUE, 'Save and keep published' => TRUE] as $submit => $visible) {
- $edit = [
- 'menu[enabled]' => 1,
- 'menu[title]' => $node_title,
- ];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, $submit);
- // Assert that the link exists.
- $this->drupalGet('test-page');
- if ($visible) {
- $this->assertLink($node_title, 0, 'Found a menu link after submitted with ' . $submit);
- }
- else {
- $this->assertNoLink($node_title, 'Found no menu link after submitted with ' . $submit);
- }
- }
+ // Assert that the link does not exist if unpublished.
+ $edit = [
+ 'menu[enabled]' => 1,
+ 'menu[title]' => $node_title,
+ 'status[value]' => FALSE,
+ ];
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ $this->drupalGet('test-page');
+ $this->assertNoLink($node_title, 'Found no menu link with the node unpublished');
+ // Assert that the link exists if published.
+ $edit['status[value]'] = TRUE;
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
+ $this->drupalGet('test-page');
+ $this->assertLink($node_title, 0, 'Found a menu link with the node published');
// Log back in as normal user.
$this->drupalLogin($this->editor);
diff --git a/core/modules/node/node.post_update.php b/core/modules/node/node.post_update.php
new file mode 100644
index 0000000000..43e3cd6acc
--- /dev/null
+++ b/core/modules/node/node.post_update.php
@@ -0,0 +1,29 @@
+condition('targetEntityType', 'node');
+ $ids = $query->execute();
+ $form_displays = EntityFormDisplay::loadMultiple($ids);
+
+ // Assign status settings for each 'node' target entity types with 'default'
+ // form mode.
+ foreach ($form_displays as $id => $form_display) {
+ /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $form_display */
+ $form_display->setComponent('status', [
+ 'type' => 'boolean_checkbox',
+ 'settings' => [
+ 'display_label' => TRUE,
+ ],
+ ])->save();
+ }
+}
diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index 4f8efe3c53..d4c95732f5 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -400,6 +400,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
])
->setDisplayConfigurable('form', TRUE);
+ $fields['status']
+ ->setDisplayOptions('form', [
+ 'type' => 'boolean_checkbox',
+ 'settings' => [
+ 'display_label' => TRUE,
+ ],
+ 'weight' => 120,
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Authored on'))
->setDescription(t('The time that the node was created.'))
diff --git a/core/modules/node/src/Entity/Node.php.orig b/core/modules/node/src/Entity/Node.php.orig
new file mode 100644
index 0000000000..4f8efe3c53
--- /dev/null
+++ b/core/modules/node/src/Entity/Node.php.orig
@@ -0,0 +1,501 @@
+getTranslationLanguages()) as $langcode) {
+ $translation = $this->getTranslation($langcode);
+
+ // If no owner has been set explicitly, make the anonymous user the owner.
+ if (!$translation->getOwner()) {
+ $translation->setOwnerId(0);
+ }
+ }
+
+ // If no revision author has been set explicitly, make the node owner the
+ // revision author.
+ if (!$this->getRevisionUser()) {
+ $this->setRevisionUserId($this->getOwnerId());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
+ parent::preSaveRevision($storage, $record);
+
+ if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) {
+ // If we are updating an existing node without adding a new revision, we
+ // need to make sure $entity->revision_log is reset whenever it is empty.
+ // Therefore, this code allows us to avoid clobbering an existing log
+ // entry with an empty one.
+ $record->revision_log = $this->original->revision_log->value;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ parent::postSave($storage, $update);
+
+ // Update the node access table for this node, but only if it is the
+ // default revision. There's no need to delete existing records if the node
+ // is new.
+ if ($this->isDefaultRevision()) {
+ /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */
+ $access_control_handler = \Drupal::entityManager()->getAccessControlHandler('node');
+ $grants = $access_control_handler->acquireGrants($this);
+ \Drupal::service('node.grant_storage')->write($this, $grants, NULL, $update);
+ }
+
+ // Reindex the node when it is updated. The node is automatically indexed
+ // when it is added, simply by being added to the node table.
+ if ($update) {
+ node_reindex_node_search($this->id());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preDelete(EntityStorageInterface $storage, array $entities) {
+ parent::preDelete($storage, $entities);
+
+ // Ensure that all nodes deleted are removed from the search index.
+ if (\Drupal::moduleHandler()->moduleExists('search')) {
+ foreach ($entities as $entity) {
+ search_index_clear('node_search', $entity->nid->value);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function postDelete(EntityStorageInterface $storage, array $nodes) {
+ parent::postDelete($storage, $nodes);
+ \Drupal::service('node.grant_storage')->deleteNodeRecords(array_keys($nodes));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType() {
+ return $this->bundle();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE) {
+ // This override exists to set the operation to the default value "view".
+ return parent::access($operation, $account, $return_as_object);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ return $this->get('title')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTitle($title) {
+ $this->set('title', $title);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCreatedTime() {
+ return $this->get('created')->value;
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCreatedTime($timestamp) {
+ $this->set('created', $timestamp);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isPromoted() {
+ return (bool) $this->get('promote')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setPromoted($promoted) {
+ $this->set('promote', $promoted ? NodeInterface::PROMOTED : NodeInterface::NOT_PROMOTED);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSticky() {
+ return (bool) $this->get('sticky')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSticky($sticky) {
+ $this->set('sticky', $sticky ? NodeInterface::STICKY : NodeInterface::NOT_STICKY);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOwner() {
+ return $this->get('uid')->entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOwnerId() {
+ return $this->getEntityKey('uid');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOwnerId($uid) {
+ $this->set('uid', $uid);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOwner(UserInterface $account) {
+ $this->set('uid', $account->id());
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRevisionCreationTime() {
+ return $this->get('revision_timestamp')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionCreationTime($timestamp) {
+ $this->set('revision_timestamp', $timestamp);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRevisionAuthor() {
+ return $this->getRevisionUser();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRevisionUser() {
+ return $this->get('revision_uid')->entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionAuthorId($uid) {
+ $this->setRevisionUserId($uid);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionUser(UserInterface $user) {
+ $this->set('revision_uid', $user);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRevisionUserId() {
+ return $this->get('revision_uid')->entity->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionUserId($user_id) {
+ $this->set('revision_uid', $user_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRevisionLogMessage() {
+ return $this->get('revision_log')->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setRevisionLogMessage($revision_log_message) {
+ $this->set('revision_log', $revision_log_message);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ $fields = parent::baseFieldDefinitions($entity_type);
+ $fields += static::publishedBaseFieldDefinitions($entity_type);
+
+ $fields['title'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Title'))
+ ->setRequired(TRUE)
+ ->setTranslatable(TRUE)
+ ->setRevisionable(TRUE)
+ ->setSetting('max_length', 255)
+ ->setDisplayOptions('view', [
+ 'label' => 'hidden',
+ 'type' => 'string',
+ 'weight' => -5,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'string_textfield',
+ 'weight' => -5,
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['uid'] = BaseFieldDefinition::create('entity_reference')
+ ->setLabel(t('Authored by'))
+ ->setDescription(t('The username of the content author.'))
+ ->setRevisionable(TRUE)
+ ->setSetting('target_type', 'user')
+ ->setDefaultValueCallback('Drupal\node\Entity\Node::getCurrentUserId')
+ ->setTranslatable(TRUE)
+ ->setDisplayOptions('view', [
+ 'label' => 'hidden',
+ 'type' => 'author',
+ 'weight' => 0,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'entity_reference_autocomplete',
+ 'weight' => 5,
+ 'settings' => [
+ 'match_operator' => 'CONTAINS',
+ 'size' => '60',
+ 'placeholder' => '',
+ ],
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['created'] = BaseFieldDefinition::create('created')
+ ->setLabel(t('Authored on'))
+ ->setDescription(t('The time that the node was created.'))
+ ->setRevisionable(TRUE)
+ ->setTranslatable(TRUE)
+ ->setDisplayOptions('view', [
+ 'label' => 'hidden',
+ 'type' => 'timestamp',
+ 'weight' => 0,
+ ])
+ ->setDisplayOptions('form', [
+ 'type' => 'datetime_timestamp',
+ 'weight' => 10,
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['changed'] = BaseFieldDefinition::create('changed')
+ ->setLabel(t('Changed'))
+ ->setDescription(t('The time that the node was last edited.'))
+ ->setRevisionable(TRUE)
+ ->setTranslatable(TRUE);
+
+ $fields['promote'] = BaseFieldDefinition::create('boolean')
+ ->setLabel(t('Promoted to front page'))
+ ->setRevisionable(TRUE)
+ ->setTranslatable(TRUE)
+ ->setDefaultValue(TRUE)
+ ->setDisplayOptions('form', [
+ 'type' => 'boolean_checkbox',
+ 'settings' => [
+ 'display_label' => TRUE,
+ ],
+ 'weight' => 15,
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['sticky'] = BaseFieldDefinition::create('boolean')
+ ->setLabel(t('Sticky at top of lists'))
+ ->setRevisionable(TRUE)
+ ->setTranslatable(TRUE)
+ ->setDefaultValue(FALSE)
+ ->setDisplayOptions('form', [
+ 'type' => 'boolean_checkbox',
+ 'settings' => [
+ 'display_label' => TRUE,
+ ],
+ 'weight' => 16,
+ ])
+ ->setDisplayConfigurable('form', TRUE);
+
+ $fields['revision_timestamp'] = BaseFieldDefinition::create('created')
+ ->setLabel(t('Revision timestamp'))
+ ->setDescription(t('The time that the current revision was created.'))
+ ->setQueryable(FALSE)
+ ->setRevisionable(TRUE);
+
+ $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference')
+ ->setLabel(t('Revision user ID'))
+ ->setDescription(t('The user ID of the author of the current revision.'))
+ ->setSetting('target_type', 'user')
+ ->setQueryable(FALSE)
+ ->setRevisionable(TRUE);
+
+ $fields['revision_log'] = BaseFieldDefinition::create('string_long')
+ ->setLabel(t('Revision log message'))
+ ->setDescription(t('Briefly describe the changes you have made.'))
+ ->setRevisionable(TRUE)
+ ->setDefaultValue('')
+ ->setDisplayOptions('form', [
+ 'type' => 'string_textarea',
+ 'weight' => 25,
+ 'settings' => [
+ 'rows' => 4,
+ ],
+ ]);
+
+ $fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
+ ->setLabel(t('Revision translation affected'))
+ ->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
+ ->setReadOnly(TRUE)
+ ->setRevisionable(TRUE)
+ ->setTranslatable(TRUE);
+
+ return $fields;
+ }
+
+ /**
+ * Default value callback for 'uid' base field definition.
+ *
+ * @see ::baseFieldDefinitions()
+ *
+ * @return array
+ * An array of default values.
+ */
+ public static function getCurrentUserId() {
+ return [\Drupal::currentUser()->id()];
+ }
+
+}
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index 203c2e6331..007b7c640b 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -86,7 +86,10 @@ public function form(array $form, FormStateInterface $form_state) {
$node = $this->entity;
if ($this->operation == 'edit') {
- $form['#title'] = $this->t('Edit @type @title', ['@type' => node_get_type_label($node), '@title' => $node->label()]);
+ $form['#title'] = $this->t('Edit @type @title', [
+ '@type' => node_get_type_label($node),
+ '@title' => $node->label()
+ ]);
}
// Changed must be sent to the client, for later overwrite error checking.
@@ -99,6 +102,15 @@ public function form(array $form, FormStateInterface $form_state) {
$form['advanced']['#attributes']['class'][] = 'entity-meta';
+ $form['footer'] = [
+ '#type' => 'container',
+ '#weight' => 99,
+ '#attributes' => [
+ 'class' => ['node-form-footer']
+ ]
+ ];
+ $form['status']['#group'] = 'footer';
+
// Node author information for administrators.
$form['author'] = [
'#type' => 'details',
@@ -147,8 +159,6 @@ public function form(array $form, FormStateInterface $form_state) {
$form['#attached']['library'][] = 'node/form';
- $form['#entity_builders']['update_status'] = '::updateStatus';
-
return $form;
}
@@ -165,6 +175,9 @@ public function form(array $form, FormStateInterface $form_state) {
* The current state of the form.
*
* @see \Drupal\node\NodeForm::form()
+ *
+ * @deprecated in Drupal 8.4.x, will be removed before Drupal 9.0.0.
+ * The "Publish" button was removed.
*/
public function updateStatus($entity_type_id, NodeInterface $node, array $form, FormStateInterface $form_state) {
$element = $form_state->getTriggeringElement();
@@ -183,59 +196,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
$element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || $form_state->get('has_been_previewed');
- // If saving is an option, privileged users get dedicated form submit
- // buttons to adjust the publishing status while saving in one go.
- // @todo This adjustment makes it close to impossible for contributed
- // modules to integrate with "the Save operation" of this form. Modules
- // need a way to plug themselves into 1) the ::submit() step, and
- // 2) the ::save() step, both decoupled from the pressed form button.
- if ($element['submit']['#access'] && \Drupal::currentUser()->hasPermission('administer nodes')) {
- // isNew | prev status » default & publish label & unpublish label
- // 1 | 1 » publish & Save and publish & Save as unpublished
- // 1 | 0 » unpublish & Save and publish & Save as unpublished
- // 0 | 1 » publish & Save and keep published & Save and unpublish
- // 0 | 0 » unpublish & Save and keep unpublished & Save and publish
-
- // Add a "Publish" button.
- $element['publish'] = $element['submit'];
- // If the "Publish" button is clicked, we want to update the status to "published".
- $element['publish']['#published_status'] = TRUE;
- $element['publish']['#dropbutton'] = 'save';
- if ($node->isNew()) {
- $element['publish']['#value'] = t('Save and publish');
- }
- else {
- $element['publish']['#value'] = $node->isPublished() ? t('Save and keep published') : t('Save and publish');
- }
- $element['publish']['#weight'] = 0;
-
- // Add a "Unpublish" button.
- $element['unpublish'] = $element['submit'];
- // If the "Unpublish" button is clicked, we want to update the status to "unpublished".
- $element['unpublish']['#published_status'] = FALSE;
- $element['unpublish']['#dropbutton'] = 'save';
- if ($node->isNew()) {
- $element['unpublish']['#value'] = t('Save as unpublished');
- }
- else {
- $element['unpublish']['#value'] = !$node->isPublished() ? t('Save and keep unpublished') : t('Save and unpublish');
- }
- $element['unpublish']['#weight'] = 10;
-
- // If already published, the 'publish' button is primary.
- if ($node->isPublished()) {
- unset($element['unpublish']['#button_type']);
- }
- // Otherwise, the 'unpublish' button is primary and should come first.
- else {
- unset($element['publish']['#button_type']);
- $element['unpublish']['#weight'] = -10;
- }
-
- // Remove the "Save" button.
- $element['submit']['#access'] = FALSE;
- }
-
$element['preview'] = [
'#type' => 'submit',
'#access' => $preview_mode != DRUPAL_DISABLED && ($node->access('create') || $node->access('update')),
diff --git a/core/modules/node/src/Tests/Update/NodeUpdateTest.php b/core/modules/node/src/Tests/Update/NodeUpdateTest.php
index b8b30be4f1..b193ea8d21 100644
--- a/core/modules/node/src/Tests/Update/NodeUpdateTest.php
+++ b/core/modules/node/src/Tests/Update/NodeUpdateTest.php
@@ -2,6 +2,7 @@
namespace Drupal\node\Tests\Update;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
@@ -38,4 +39,29 @@ public function testPublishedEntityKey() {
$this->assertEqual('status', $entity_type->getKey('published'));
}
+ /**
+ * Tests that the node entity form has the status checkbox.
+ *
+ * @see node_post_update_configure_status_field_widget()
+ */
+ public function testStatusCheckbox() {
+ // Run updates.
+ $this->runUpdates();
+
+ $query = \Drupal::entityQuery('entity_form_display')
+ ->condition('targetEntityType', 'node');
+ $ids = $query->execute();
+ $form_displays = EntityFormDisplay::loadMultiple($ids);
+
+ /**
+ * @var string $id
+ * @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
+ */
+ foreach ($form_displays as $id => $form_display) {
+ $component = $form_display->getComponent('status');
+ $this->assertEqual('boolean_checkbox', $component['type']);
+ $this->assertEqual(['display_label' => TRUE], $component['settings']);
+ }
+ }
+
}
diff --git a/core/modules/node/tests/src/Functional/NodeEditFormTest.php b/core/modules/node/tests/src/Functional/NodeEditFormTest.php
index ea2a168635..ce032e5909 100644
--- a/core/modules/node/tests/src/Functional/NodeEditFormTest.php
+++ b/core/modules/node/tests/src/Functional/NodeEditFormTest.php
@@ -98,7 +98,7 @@ public function testNodeEdit() {
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$edit['revision'] = TRUE;
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
// Ensure that the node revision has been created.
$revised_node = $this->drupalGetNodeByTitle($edit['title[0][value]'], TRUE);
@@ -124,12 +124,21 @@ public function testNodeEdit() {
$edit['created[0][value][date]'] = $this->randomMachineName(8);
// Get the current amount of open details elements.
$open_details_elements = count($this->cssSelect('details[open="open"]'));
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
// The node author details must be open.
$this->assertRaw('');
// Only one extra details element should now be open.
$open_details_elements++;
$this->assertEqual(count($this->cssSelect('details[open="open"]')), $open_details_elements, 'Exactly one extra open <details> element found.');
+
+ // Edit the same node, save it and verify it's unpublished after unchecking
+ // the 'Published' boolean_checkbox and clicking 'Save'.
+ $this->drupalGet("node/" . $node->id() . "/edit");
+ $edit = ['status[value]' => FALSE];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->nodeStorage->resetCache([$node->id()]);
+ $node = $this->nodeStorage->load($node->id());
+ $this->assertFalse($node->isPublished(), 'Node is unpublished');
}
/**
@@ -143,7 +152,7 @@ public function testNodeEditAuthoredBy() {
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
- $this->drupalPostForm('node/add/page', $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/page', $edit, t('Save'));
// Check that the node was authored by the currently logged in user.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
@@ -172,7 +181,7 @@ public function testNodeEditAuthoredBy() {
$this->drupalLogin($this->adminUser);
// Save the node without making any changes.
- $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save'));
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertIdentical($this->webUser->id(), $node->getOwner()->id());
@@ -184,7 +193,7 @@ public function testNodeEditAuthoredBy() {
// Check that saving the node without making any changes keeps the proper
// author ID.
- $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', [], t('Save'));
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertIdentical($this->webUser->id(), $node->getOwner()->id());
@@ -203,13 +212,13 @@ protected function checkVariousAuthoredByValues(NodeInterface $node, $form_eleme
$edit = [
$form_element_name => 'invalid-name',
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
$this->assertRaw(t('There are no entities matching "%name".', ['%name' => 'invalid-name']));
// Change the authored by field to an empty string, which should assign
// authorship to the anonymous user (uid 0).
$edit[$form_element_name] = '';
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$uid = $node->getOwnerId();
@@ -228,7 +237,7 @@ protected function checkVariousAuthoredByValues(NodeInterface $node, $form_eleme
// Change the authored by field to another user's name (that is not
// logged in).
$edit[$form_element_name] = $this->webUser->getUsername();
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
+ $this->drupalPostForm(NULL, $edit, t('Save'));
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertIdentical($node->getOwnerId(), $this->webUser->id(), 'Node authored by normal user.');
diff --git a/core/modules/node/tests/src/Functional/NodeFormButtonsTest.php b/core/modules/node/tests/src/Functional/NodeFormButtonsTest.php
deleted file mode 100644
index 546a6334ad..0000000000
--- a/core/modules/node/tests/src/Functional/NodeFormButtonsTest.php
+++ /dev/null
@@ -1,135 +0,0 @@
-webUser = $this->drupalCreateUser(['create article content', 'edit own article content']);
- // Create a user that has access to change the state of the node.
- $this->adminUser = $this->drupalCreateUser(['administer nodes', 'bypass node access']);
- }
-
- /**
- * Tests that the right buttons are displayed for saving nodes.
- */
- public function testNodeFormButtons() {
- $node_storage = $this->container->get('entity.manager')->getStorage('node');
- // Log in as administrative user.
- $this->drupalLogin($this->adminUser);
-
- // Verify the buttons on a node add form.
- $this->drupalGet('node/add/article');
- $this->assertButtons([t('Save and publish'), t('Save as unpublished')]);
-
- // Save the node and assert it's published after clicking
- // 'Save and publish'.
- $edit = ['title[0][value]' => $this->randomString()];
- $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
-
- // Get the node.
- $node_1 = $node_storage->load(1);
- $this->assertTrue($node_1->isPublished(), 'Node is published');
-
- // Verify the buttons on a node edit form.
- $this->drupalGet('node/' . $node_1->id() . '/edit');
- $this->assertButtons([t('Save and keep published'), t('Save and unpublish')]);
-
- // Save the node and verify it's still published after clicking
- // 'Save and keep published'.
- $this->drupalPostForm(NULL, $edit, t('Save and keep published'));
- $node_storage->resetCache([1]);
- $node_1 = $node_storage->load(1);
- $this->assertTrue($node_1->isPublished(), 'Node is published');
-
- // Save the node and verify it's unpublished after clicking
- // 'Save and unpublish'.
- $this->drupalPostForm('node/' . $node_1->id() . '/edit', $edit, t('Save and unpublish'));
- $node_storage->resetCache([1]);
- $node_1 = $node_storage->load(1);
- $this->assertFalse($node_1->isPublished(), 'Node is unpublished');
-
- // Verify the buttons on an unpublished node edit screen.
- $this->drupalGet('node/' . $node_1->id() . '/edit');
- $this->assertButtons([t('Save and keep unpublished'), t('Save and publish')]);
-
- // Create a node as a normal user.
- $this->drupalLogout();
- $this->drupalLogin($this->webUser);
-
- // Verify the buttons for a normal user.
- $this->drupalGet('node/add/article');
- $this->assertButtons([t('Save')], FALSE);
-
- // Create the node.
- $edit = ['title[0][value]' => $this->randomString()];
- $this->drupalPostForm('node/add/article', $edit, t('Save'));
- $node_2 = $node_storage->load(2);
- $this->assertTrue($node_2->isPublished(), 'Node is published');
-
- // Log in as an administrator and unpublish the node that just
- // was created by the normal user.
- $this->drupalLogout();
- $this->drupalLogin($this->adminUser);
- $this->drupalPostForm('node/' . $node_2->id() . '/edit', [], t('Save and unpublish'));
- $node_storage->resetCache([2]);
- $node_2 = $node_storage->load(2);
- $this->assertFalse($node_2->isPublished(), 'Node is unpublished');
-
- // Log in again as the normal user, save the node and verify
- // it's still unpublished.
- $this->drupalLogout();
- $this->drupalLogin($this->webUser);
- $this->drupalPostForm('node/' . $node_2->id() . '/edit', [], t('Save'));
- $node_storage->resetCache([2]);
- $node_2 = $node_storage->load(2);
- $this->assertFalse($node_2->isPublished(), 'Node is still unpublished');
- $this->drupalLogout();
-
- // Set article content type default to unpublished. This will change the
- // the initial order of buttons and/or status of the node when creating
- // a node.
- $fields = \Drupal::entityManager()->getFieldDefinitions('node', 'article');
- $fields['status']->getConfig('article')
- ->setDefaultValue(FALSE)
- ->save();
-
- // Verify the buttons on a node add form for an administrator.
- $this->drupalLogin($this->adminUser);
- $this->drupalGet('node/add/article');
- $this->assertButtons([t('Save as unpublished'), t('Save and publish')]);
-
- // Verify the node is unpublished by default for a normal user.
- $this->drupalLogout();
- $this->drupalLogin($this->webUser);
- $edit = ['title[0][value]' => $this->randomString()];
- $this->drupalPostForm('node/add/article', $edit, t('Save'));
- $node_3 = $node_storage->load(3);
- $this->assertFalse($node_3->isPublished(), 'Node is unpublished');
- }
-
-}
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsUiBypassAccessTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsUiBypassAccessTest.php
index a3a575d77e..186b694774 100644
--- a/core/modules/node/tests/src/Functional/NodeRevisionsUiBypassAccessTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsUiBypassAccessTest.php
@@ -67,7 +67,7 @@ public function testDisplayRevisionTab() {
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published');
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
$this->assertUrl($node->toUrl());
$this->assertNoLink(t('Revisions'));
@@ -78,7 +78,7 @@ public function testDisplayRevisionTab() {
// Submit the form without changing the checkbox.
$edit = [];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save and keep published');
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
$this->assertUrl($node->toUrl());
$this->assertLink(t('Revisions'));
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
index cc364cda29..a437e4aba8 100644
--- a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
@@ -55,7 +55,7 @@ public function testNodeFormSaveWithoutRevision() {
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Load the node again and check the revision is the same as before.
$node_storage->resetCache([$node->id()]);
@@ -68,7 +68,7 @@ public function testNodeFormSaveWithoutRevision() {
// Submit the form without changing the checkbox.
$edit = [];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Load the node again and check the revision is different from before.
$node_storage->resetCache([$node->id()]);
diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
index a8a74544ec..a44bea131e 100644
--- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
+++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
@@ -99,7 +99,9 @@ public function testPublishedStatusNoFields() {
'source' => $default_langcode,
'target' => $langcode
], ['language' => $language]);
- $this->drupalPostForm($add_url, $this->getEditValues($values, $langcode), t('Save and unpublish (this translation)'));
+ $edit = $this->getEditValues($values, $langcode);
+ $edit['status[value]'] = FALSE;
+ $this->drupalPostForm($add_url, $edit, t('Save (this translation)'));
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
@@ -139,18 +141,6 @@ protected function getNewEntityValues($langcode) {
/**
* {@inheritdoc}
*/
- protected function getFormSubmitAction(EntityInterface $entity, $langcode) {
- if ($entity->getTranslation($langcode)->isPublished()) {
- return t('Save and keep published') . $this->getFormSubmitSuffix($entity, $langcode);
- }
- else {
- return t('Save and keep unpublished') . $this->getFormSubmitSuffix($entity, $langcode);
- }
- }
-
- /**
- * {@inheritdoc}
- */
protected function doTestPublishedStatus() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
@@ -158,18 +148,18 @@ protected function doTestPublishedStatus() {
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
- $actions = [
- t('Save and keep published'),
- t('Save and unpublish'),
+ $statuses = [
+ TRUE,
+ FALSE,
];
- foreach ($actions as $index => $action) {
+ foreach ($statuses as $index => $value) {
// (Un)publish the node translations and check that the translation
// statuses are (un)published accordingly.
foreach ($this->langcodes as $langcode) {
$options = ['language' => $languages[$langcode]];
$url = $entity->urlInfo('edit-form', $options);
- $this->drupalPostForm($url, [], $action . $this->getFormSubmitSuffix($entity, $langcode), $options);
+ $this->drupalPostForm($url, ['status[value]' => $value], t('Save') . $this->getFormSubmitSuffix($entity, $langcode), $options);
}
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
diff --git a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
index 836740dc44..e29a1d0550 100644
--- a/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
+++ b/core/modules/options/tests/src/Functional/OptionsFieldUITest.php
@@ -328,7 +328,7 @@ public function testNodeDisplay() {
$edit = [
$this->fieldName => '1',
];
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
// Check the node page and see if the values are correct.
$file_formatters = ['list_default', 'list_key'];
diff --git a/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
index 191e0cf0d5..cc77f2b6a6 100644
--- a/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
+++ b/core/modules/search/src/Tests/SearchConfigSettingsFormTest.php
@@ -47,7 +47,7 @@ protected function setUp() {
// also needs the word "pizza" so we can use it as the search keyword.
$body_key = 'body[0][value]';
$edit[$body_key] = \Drupal::l($node->label(), $node->urlInfo()) . ' pizza sandwich';
- $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save'));
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
diff --git a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php
index 64c8c15977..39303fffcd 100644
--- a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php
+++ b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php
@@ -206,7 +206,7 @@ public function testBreadCrumbs() {
$edit = [
'menu[menu_parent]' => $link->getMenuName() . ':' . $link->getPluginId(),
];
- $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save'));
$expected = [
"node" => $link->getTitle(),
];
@@ -227,7 +227,7 @@ public function testBreadCrumbs() {
$edit = [
'field_tags[target_id]' => implode(',', array_keys($tags)),
];
- $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save and keep published'));
+ $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save'));
// Put both terms into a hierarchy Drupal » Breadcrumbs. Required for both
// the menu links and the terms itself, since taxonomy_term_page() resets
diff --git a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
index 09cc586bc7..9ee6d0cd4d 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathRC1TestBaseFilledTest.php
@@ -117,7 +117,7 @@ public function testUpdatedSite() {
$this->assertText('Test Article - New title');
$this->assertText('Test 1');
$this->assertRaw('0.01');
- $this->drupalPostForm('node/8/edit', [], 'Save and keep published (this translation)');
+ $this->drupalPostForm('node/8/edit', [], 'Save (this translation)');
$this->assertResponse(200);
$this->drupalGet('node/8/edit', ['language' => $spanish]);
$this->assertText('Test title Spanish');
diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBaseFilledTest.php b/core/modules/system/src/Tests/Update/UpdatePathTestBaseFilledTest.php
index fe62340af8..3c5aab057e 100644
--- a/core/modules/system/src/Tests/Update/UpdatePathTestBaseFilledTest.php
+++ b/core/modules/system/src/Tests/Update/UpdatePathTestBaseFilledTest.php
@@ -117,7 +117,7 @@ public function testUpdatedSite() {
$this->assertText('Test Article - New title');
$this->assertText('Test 1');
$this->assertRaw('0.01');
- $this->drupalPostForm('node/8/edit', [], 'Save and keep published (this translation)');
+ $this->drupalPostForm('node/8/edit', [], 'Save (this translation)');
$this->assertResponse(200);
$this->drupalGet('node/8/edit', ['language' => $spanish]);
$this->assertText('Test title Spanish');
diff --git a/core/modules/taxonomy/tests/src/Functional/LegacyTest.php b/core/modules/taxonomy/tests/src/Functional/LegacyTest.php
index 289471533b..ac8e82dd66 100644
--- a/core/modules/taxonomy/tests/src/Functional/LegacyTest.php
+++ b/core/modules/taxonomy/tests/src/Functional/LegacyTest.php
@@ -60,7 +60,7 @@ public function testTaxonomyLegacyNode() {
$edit['created[0][value][time]'] = $date->format('H:i:s');
$edit['body[0][value]'] = $this->randomMachineName();
$edit['field_tags[target_id]'] = $this->randomMachineName();
- $this->drupalPostForm('node/add/article', $edit, t('Save and publish'));
+ $this->drupalPostForm('node/add/article', $edit, t('Save'));
// Checks that the node has been saved.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEqual($node->getCreatedTime(), $date->getTimestamp(), 'Legacy node was saved with the right date.');
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
index c94e36e388..9082f2d1bd 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
@@ -66,6 +66,13 @@ content:
weight: 15
region: content
third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 120
+ region: content
+ third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
index 0b7ffd133c..682f1a550c 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
@@ -40,6 +40,13 @@ content:
weight: 15
region: content
third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 120
+ region: content
+ third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
diff --git a/core/themes/bartik/css/components/form.css b/core/themes/bartik/css/components/form.css
index 3e46920757..5ee577a158 100644
--- a/core/themes/bartik/css/components/form.css
+++ b/core/themes/bartik/css/components/form.css
@@ -172,6 +172,14 @@ input.form-submit:focus {
.node-form .form-wrapper {
margin-bottom: 2em;
}
+.node-form .node-form-footer,
+.node-form .field--name-status {
+ margin-bottom: 0;
+}
+.node-form .form-actions {
+ padding-top: 0;
+ margin-top: 0;
+}
/* Contact Form */
.contact-form #edit-name {
diff --git a/core/themes/seven/css/layout/node-add.css b/core/themes/seven/css/layout/node-add.css
index 22faf339bb..f28a1e5f3e 100644
--- a/core/themes/seven/css/layout/node-add.css
+++ b/core/themes/seven/css/layout/node-add.css
@@ -1,3 +1,9 @@
+.layout-region-node-footer__content {
+ border-top: 1px solid #bebfb9;
+ padding-top: 0.5em;
+ margin-top: 1.5em;
+}
+
/**
* Widescreen
*
@@ -14,4 +20,7 @@
margin-top: 1em;
margin-bottom: 1em;
}
+ .layout-region-node-footer__content {
+ margin-top: 0.5em;
+ }
}
diff --git a/core/themes/seven/templates/node-edit-form.html.twig b/core/themes/seven/templates/node-edit-form.html.twig
new file mode 100644
index 0000000000..cf747f9292
--- /dev/null
+++ b/core/themes/seven/templates/node-edit-form.html.twig
@@ -0,0 +1,31 @@
+{#
+/**
+ * @file
+ * Theme override for a node edit form.
+ *
+ * Two column template for the node add/edit form.
+ *
+ * This template will be used when a node edit form specifies 'node_edit_form'
+ * as its #theme callback. Otherwise, by default, node add/edit forms will be
+ * themed by form.html.twig.
+ *
+ * Available variables:
+ * - form: The node add/edit form.
+ *
+ * @see seven_form_node_form_alter()
+ */
+#}
+