summaryrefslogtreecommitdiffstatshomepage
path: root/core/modules/node
diff options
context:
space:
mode:
Diffstat (limited to 'core/modules/node')
-rw-r--r--core/modules/node/js/node.preview.js7
-rw-r--r--core/modules/node/node.module26
-rw-r--r--core/modules/node/src/Controller/NodeController.php7
-rw-r--r--core/modules/node/src/Form/NodeForm.php43
-rw-r--r--core/modules/node/src/Hook/NodeHooks.php9
-rw-r--r--core/modules/node/src/Hook/NodeHooks1.php2
-rw-r--r--core/modules/node/src/Hook/NodeRequirements.php3
-rw-r--r--core/modules/node/src/Hook/NodeThemeHooks.php4
-rw-r--r--core/modules/node/src/NodeAccessControlHandlerInterface.php2
-rw-r--r--core/modules/node/src/NodeGrantDatabaseStorage.php23
-rw-r--r--core/modules/node/src/NodeGrantDatabaseStorageInterface.php10
-rw-r--r--core/modules/node/src/NodePermissions.php24
-rw-r--r--core/modules/node/src/Plugin/Block/SyndicateBlock.php6
-rw-r--r--core/modules/node/src/Plugin/views/UidRevisionTrait.php38
-rw-r--r--core/modules/node/src/Plugin/views/argument/UidRevision.php7
-rw-r--r--core/modules/node/src/Plugin/views/filter/Access.php2
-rw-r--r--core/modules/node/src/Plugin/views/filter/UidRevision.php13
-rw-r--r--core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php4
-rw-r--r--core/modules/node/tests/src/Functional/NodeCreationTest.php20
-rw-r--r--core/modules/node/tests/src/Functional/NodeEditFormTest.php7
-rw-r--r--core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php102
-rw-r--r--core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php16
-rw-r--r--core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php2
-rw-r--r--core/modules/node/tests/src/Functional/NodeTranslationUITest.php24
-rw-r--r--core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php4
-rw-r--r--core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php4
-rw-r--r--core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php4
-rw-r--r--core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php4
-rw-r--r--core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php4
-rw-r--r--core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php1
-rw-r--r--core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php5
31 files changed, 317 insertions, 110 deletions
diff --git a/core/modules/node/js/node.preview.js b/core/modules/node/js/node.preview.js
index 50bc58ade77..e23be0b71e2 100644
--- a/core/modules/node/js/node.preview.js
+++ b/core/modules/node/js/node.preview.js
@@ -34,13 +34,13 @@
const $previewDialog = $(
`<div>${Drupal.theme('nodePreviewModal')}</div>`,
).appendTo('body');
- Drupal.dialog($previewDialog, {
+ const confirmationDialog = Drupal.dialog($previewDialog, {
title: Drupal.t('Leave preview?'),
buttons: [
{
text: Drupal.t('Cancel'),
click() {
- $(this).dialog('close');
+ confirmationDialog.close();
},
},
{
@@ -50,7 +50,8 @@
},
},
],
- }).showModal();
+ });
+ confirmationDialog.showModal();
}
}
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 4ee3c48a00b..6841f24b96b 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -36,8 +36,12 @@ use Drupal\node\NodeTypeInterface;
* @return array|false
* A renderable array containing a list of linked node titles fetched from
* $result, or FALSE if there are no rows in $result.
+ *
+ * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement.
+ * @see https://www.drupal.org/node/3531959
*/
function node_title_list(StatementInterface $result, $title = NULL) {
+ @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3531959', E_USER_DEPRECATED);
$items = [];
$num_rows = FALSE;
$nids = [];
@@ -121,8 +125,12 @@ function node_get_type_label(NodeInterface $node) {
*
* @return string
* The node type description.
+ *
+ * @deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead.
+ * @see https://www.drupal.org/node/3531945
*/
function node_type_get_description(NodeTypeInterface $node_type) {
+ @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:12.0.0. Use $node_type->getDescription() instead. See https://www.drupal.org/node/3531945', E_USER_DEPRECATED);
return $node_type->getDescription();
}
@@ -325,10 +333,11 @@ function template_preprocess_node(&$variables): void {
// $variables['content'] is more flexible and consistent.
$submitted_configurable = $node->getFieldDefinition('created')->isDisplayConfigurable('view') || $node->getFieldDefinition('uid')->isDisplayConfigurable('view');
if (!$skip_custom_preprocessing || !$submitted_configurable) {
- $variables['date'] = \Drupal::service('renderer')->render($variables['elements']['created']);
- unset($variables['elements']['created']);
- $variables['author_name'] = \Drupal::service('renderer')->render($variables['elements']['uid']);
- unset($variables['elements']['uid']);
+ /** @var \Drupal\Core\Render\RendererInterface $renderer */
+ $renderer = \Drupal::service('renderer');
+ $variables['date'] = !empty($variables['elements']['created']) ? $renderer->render($variables['elements']['created']) : '';
+ $variables['author_name'] = !empty($variables['elements']['uid']) ? $renderer->render($variables['elements']['uid']) : '';
+ unset($variables['elements']['created'], $variables['elements']['uid']);
}
if (isset($variables['elements']['title']) && (!$skip_custom_preprocessing || !$node->getFieldDefinition('title')->isDisplayConfigurable('view'))) {
@@ -387,6 +396,11 @@ function node_form_system_themes_admin_form_submit($form, FormStateInterface $fo
}
/**
+ * @addtogroup node_access
+ * @{
+ */
+
+/**
* Fetches an array of permission IDs granted to the given user ID.
*
* The implementation here provides only the universal "all" grant. A node
@@ -658,6 +672,10 @@ function _node_access_rebuild_batch_finished($success, $results, $operations): v
}
/**
+ * @} End of "addtogroup node_access".
+ */
+
+/**
* Marks a node to be re-indexed by the node_search plugin.
*
* @param int $nid
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index e860d0c1d2a..d5a35f64285 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -3,7 +3,6 @@
namespace Drupal\node\Controller;
use Drupal\Component\Utility\Xss;
-use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
@@ -197,10 +196,12 @@ class NodeController extends ControllerBase implements ContainerInjectionInterfa
'username' => $this->renderer->renderInIsolation($username),
'message' => ['#markup' => $revision->revision_log->value, '#allowed_tags' => Xss::getHtmlTagList()],
],
+ // @todo Fix this properly in https://www.drupal.org/project/drupal/issues/3227637.
+ '#cache' => [
+ 'max-age' => 0,
+ ],
],
];
- // @todo Simplify once https://www.drupal.org/node/2334319 lands.
- $this->renderer->addCacheableDependency($column['data'], CacheableMetadata::createFromRenderArray($username));
$row[] = $column;
if ($is_current_revision) {
diff --git a/core/modules/node/src/Form/NodeForm.php b/core/modules/node/src/Form/NodeForm.php
index d5afa396568..295e9ab78ce 100644
--- a/core/modules/node/src/Form/NodeForm.php
+++ b/core/modules/node/src/Form/NodeForm.php
@@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
+use Drupal\Core\Utility\Error;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -163,7 +164,7 @@ class NodeForm extends ContentEntityForm {
$form['meta']['author'] = [
'#type' => 'item',
'#title' => $this->t('Author'),
- '#markup' => $node->getOwner()?->getAccountName(),
+ '#markup' => $node->getOwner()?->getDisplayName(),
'#wrapper_attributes' => ['class' => ['entity-meta__author']],
];
@@ -278,21 +279,22 @@ class NodeForm extends ContentEntityForm {
public function save(array $form, FormStateInterface $form_state) {
$node = $this->entity;
$insert = $node->isNew();
- $node->save();
- $node_link = $node->toLink($this->t('View'))->toString();
- $context = ['@type' => $node->getType(), '%title' => $node->label(), 'link' => $node_link];
- $t_args = ['@type' => node_get_type_label($node), '%title' => $node->toLink()->toString()];
-
- if ($insert) {
- $this->logger('content')->info('@type: added %title.', $context);
- $this->messenger()->addStatus($this->t('@type %title has been created.', $t_args));
- }
- else {
- $this->logger('content')->info('@type: updated %title.', $context);
- $this->messenger()->addStatus($this->t('@type %title has been updated.', $t_args));
- }
- if ($node->id()) {
+ try {
+ $node->save();
+ $node_link = $node->toLink($this->t('View'))->toString();
+ $context = ['@type' => $node->getType(), '%title' => $node->label(), 'link' => $node_link];
+ $t_args = ['@type' => node_get_type_label($node), '%title' => $node->access('view') ? $node->toLink()->toString() : $node->label()];
+
+ if ($insert) {
+ $this->logger('content')->info('@type: added %title.', $context);
+ $this->messenger()->addStatus($this->t('@type %title has been created.', $t_args));
+ }
+ else {
+ $this->logger('content')->info('@type: updated %title.', $context);
+ $this->messenger()->addStatus($this->t('@type %title has been updated.', $t_args));
+ }
+
$form_state->setValue('nid', $node->id());
$form_state->set('nid', $node->id());
if ($node->access('view')) {
@@ -310,10 +312,15 @@ class NodeForm extends ContentEntityForm {
$store = $this->tempStoreFactory->get('node_preview');
$store->delete($node->uuid());
}
- else {
+ catch (\Exception $e) {
// In the unlikely case something went wrong on save, the node will be
- // rebuilt and node form redisplayed the same way as in preview.
- $this->messenger()->addError($this->t('The post could not be saved.'));
+ // rebuilt and node form redisplayed.
+ $this->messenger()->addError($this->t('The content could not be saved. Contact the site administrator if the problem persists.'));
+ // It's likely that this exception is an EntityStorageException in which
+ // case we won't have the actual backtrace available. Attempt to get the
+ // previous exception if available to include the backtrace.
+ $e = $e->getPrevious() ?: $e;
+ \Drupal::logger('node')->error('%type saving node form: @message in %function (line %line of %file) @backtrace_string.', Error::decodeException($e));
$form_state->setRebuild();
}
}
diff --git a/core/modules/node/src/Hook/NodeHooks.php b/core/modules/node/src/Hook/NodeHooks.php
index d5f84e0359b..8a6b4d887c8 100644
--- a/core/modules/node/src/Hook/NodeHooks.php
+++ b/core/modules/node/src/Hook/NodeHooks.php
@@ -66,4 +66,13 @@ class NodeHooks {
}
}
+ /**
+ * Implements hook_block_alter().
+ */
+ #[Hook('block_alter')]
+ public function blockAlter(&$definitions): void {
+ // Hide the deprecated Syndicate block from the UI.
+ $definitions['node_syndicate_block']['_block_ui_hidden'] = TRUE;
+ }
+
}
diff --git a/core/modules/node/src/Hook/NodeHooks1.php b/core/modules/node/src/Hook/NodeHooks1.php
index 8e25f2eb066..d2dbf545c3c 100644
--- a/core/modules/node/src/Hook/NodeHooks1.php
+++ b/core/modules/node/src/Hook/NodeHooks1.php
@@ -92,7 +92,7 @@ class NodeHooks1 {
case 'entity.entity_view_display.node.default':
case 'entity.entity_view_display.node.view_mode':
$type = $route_match->getParameter('node_type');
- return '<p>' . $this->t('Content items can be displayed using different view modes: Teaser, Full content, Print, RSS, etc. <em>Teaser</em> is a short format that is typically used in lists of multiple content items. <em>Full content</em> is typically used when the content is displayed on its own page.') . '</p>' . '<p>' . $this->t('Here, you can define which fields are shown and hidden when %type content is displayed in each view mode, and define how the fields are displayed in each view mode.', ['%type' => $type->label()]) . '</p>';
+ return '<p>' . $this->t('Content items can be displayed using different view modes: Teaser, Full content, Print, RSS, etc. <em>Teaser</em> is a short format that is typically used in lists of multiple content items. <em>Full content</em> is typically used when the content is displayed on its own page.') . '</p><p>' . $this->t('Here, you can define which fields are shown and hidden when %type content is displayed in each view mode, and define how the fields are displayed in each view mode.', ['%type' => $type->label()]) . '</p>';
case 'entity.node.version_history':
return '<p>' . $this->t('Revisions allow you to track differences between multiple versions of your content, and revert to older versions.') . '</p>';
diff --git a/core/modules/node/src/Hook/NodeRequirements.php b/core/modules/node/src/Hook/NodeRequirements.php
index aa8b39d5682..84f74aee98c 100644
--- a/core/modules/node/src/Hook/NodeRequirements.php
+++ b/core/modules/node/src/Hook/NodeRequirements.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\node\Hook;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
@@ -144,7 +145,7 @@ class NodeRequirements {
'title' => $this->t('Content status filter'),
'value' => $this->t('Redundant filters detected'),
'description' => $node_status_filter_description,
- 'severity' => REQUIREMENT_WARNING,
+ 'severity' => RequirementSeverity::Warning,
];
}
}
diff --git a/core/modules/node/src/Hook/NodeThemeHooks.php b/core/modules/node/src/Hook/NodeThemeHooks.php
index 7ee443c458f..7ed0ef91f5f 100644
--- a/core/modules/node/src/Hook/NodeThemeHooks.php
+++ b/core/modules/node/src/Hook/NodeThemeHooks.php
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Drupal\node\Hook;
-use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for the node module.
@@ -14,7 +14,7 @@ class NodeThemeHooks {
/**
* Implements hook_preprocess_HOOK() for node field templates.
*/
- #[Preprocess('field__node')]
+ #[Hook('preprocess_field__node')]
public function preprocessFieldNode(&$variables): void {
// Set a variable 'is_inline' in cases where inline markup is required,
// without any block elements such as <div>.
diff --git a/core/modules/node/src/NodeAccessControlHandlerInterface.php b/core/modules/node/src/NodeAccessControlHandlerInterface.php
index 588391394ee..0d67cfb7bd6 100644
--- a/core/modules/node/src/NodeAccessControlHandlerInterface.php
+++ b/core/modules/node/src/NodeAccessControlHandlerInterface.php
@@ -30,6 +30,8 @@ interface NodeAccessControlHandlerInterface {
/**
* Creates the default node access grant entry on the grant storage.
+ *
+ * @see \Drupal\node\NodeGrantDatabaseStorageInterface::writeDefault()
*/
public function writeDefaultGrant();
diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php
index eea6cc10012..fbaab21a211 100644
--- a/core/modules/node/src/NodeGrantDatabaseStorage.php
+++ b/core/modules/node/src/NodeGrantDatabaseStorage.php
@@ -112,6 +112,13 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface {
if (count($grants) > 0) {
$query->condition($grants);
}
+ if ($query->execute()->fetchField()) {
+ $access_result = AccessResult::allowed();
+ }
+ else {
+ $access_result = AccessResult::neutral();
+ }
+ $access_result->addCacheContexts(['user.node_grants:' . $operation]);
// Only the 'view' node grant can currently be cached; the others currently
// don't have any cacheability metadata. Hopefully, we can add that in the
@@ -119,20 +126,10 @@ class NodeGrantDatabaseStorage implements NodeGrantDatabaseStorageInterface {
// cases. For now, this must remain marked as uncacheable, even when it is
// theoretically cacheable, because we don't have the necessary metadata to
// know it for a fact.
- $set_cacheability = function (AccessResult $access_result) use ($operation) {
- $access_result->addCacheContexts(['user.node_grants:' . $operation]);
- if ($operation !== 'view') {
- $access_result->setCacheMaxAge(0);
- }
- return $access_result;
- };
-
- if ($query->execute()->fetchField()) {
- return $set_cacheability(AccessResult::allowed());
- }
- else {
- return $set_cacheability(AccessResult::neutral());
+ if ($operation !== 'view') {
+ $access_result->setCacheMaxAge(0);
}
+ return $access_result;
}
/**
diff --git a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php b/core/modules/node/src/NodeGrantDatabaseStorageInterface.php
index cce74476563..d343a2f350b 100644
--- a/core/modules/node/src/NodeGrantDatabaseStorageInterface.php
+++ b/core/modules/node/src/NodeGrantDatabaseStorageInterface.php
@@ -42,8 +42,8 @@ interface NodeGrantDatabaseStorageInterface {
* @param string $base_table
* The base table of the query.
*
- * @return int
- * Status of the access check.
+ * @return void
+ * No return value.
*/
public function alterQuery($query, array $tables, $operation, AccountInterface $account, $base_table);
@@ -83,6 +83,12 @@ interface NodeGrantDatabaseStorageInterface {
/**
* Creates the default node access grant entry.
+ *
+ * The default node access grant is a special grant added to the node_access
+ * table when no modules implement hook_node_grants. It grants view access
+ * to any published node.
+ *
+ * @see self::access()
*/
public function writeDefault();
diff --git a/core/modules/node/src/NodePermissions.php b/core/modules/node/src/NodePermissions.php
index e913f5326f3..5f651830192 100644
--- a/core/modules/node/src/NodePermissions.php
+++ b/core/modules/node/src/NodePermissions.php
@@ -2,6 +2,9 @@
namespace Drupal\node;
+use Drupal\Core\DependencyInjection\AutowireTrait;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\BundlePermissionHandlerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\Entity\NodeType;
@@ -9,19 +12,34 @@ use Drupal\node\Entity\NodeType;
/**
* Provides dynamic permissions for nodes of different types.
*/
-class NodePermissions {
+class NodePermissions implements ContainerInjectionInterface {
+
+ use AutowireTrait;
use BundlePermissionHandlerTrait;
use StringTranslationTrait;
+ public function __construct(
+ protected ?EntityTypeManagerInterface $entityTypeManager = NULL,
+ ) {
+ if ($entityTypeManager === NULL) {
+ @trigger_error('Calling ' . __METHOD__ . ' without the $entityTypeManager argument is deprecated in drupal:11.2.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3515921', E_USER_DEPRECATED);
+ $this->entityTypeManager = \Drupal::entityTypeManager();
+ }
+ }
+
/**
* Returns an array of node type permissions.
*
* @return array
* The node type permissions.
- * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
+ *
+ * @see \Drupal\user\PermissionHandlerInterface::getPermissions()
*/
public function nodeTypePermissions() {
- return $this->generatePermissions(NodeType::loadMultiple(), [$this, 'buildPermissions']);
+ return $this->generatePermissions(
+ $this->entityTypeManager->getStorage('node_type')->loadMultiple(),
+ [$this, 'buildPermissions']
+ );
}
/**
diff --git a/core/modules/node/src/Plugin/Block/SyndicateBlock.php b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
index b10c63527e5..45cfe1eb45c 100644
--- a/core/modules/node/src/Plugin/Block/SyndicateBlock.php
+++ b/core/modules/node/src/Plugin/Block/SyndicateBlock.php
@@ -14,6 +14,11 @@ use Drupal\Core\Url;
/**
* Provides a 'Syndicate' block that links to the site's RSS feed.
+ *
+ * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no
+ * replacement.
+ *
+ * @see https://www.drupal.org/node/3519248
*/
#[Block(
id: "node_syndicate_block",
@@ -43,6 +48,7 @@ class SyndicateBlock extends BlockBase implements ContainerFactoryPluginInterfac
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory) {
+ @trigger_error('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248', E_USER_DEPRECATED);
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $configFactory;
}
diff --git a/core/modules/node/src/Plugin/views/UidRevisionTrait.php b/core/modules/node/src/Plugin/views/UidRevisionTrait.php
new file mode 100644
index 00000000000..5cbf21d56d4
--- /dev/null
+++ b/core/modules/node/src/Plugin/views/UidRevisionTrait.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node\Plugin\views;
+
+/**
+ * Checks for nodes that a user posted or created a revision on.
+ */
+trait UidRevisionTrait {
+
+ /**
+ * Checks for nodes that a user posted or created a revision on.
+ *
+ * @param array $uids
+ * A list of user ids.
+ * @param int $group
+ * See \Drupal\views\Plugin\views\query\Sql::addWhereExpression() $group.
+ */
+ public function uidRevisionQuery(array $uids, int $group = 0): void {
+ $this->ensureMyTable();
+
+ // As per https://www.php.net/manual/en/pdo.prepare.php "you cannot use a
+ // named parameter marker of the same name more than once in a prepared
+ // statement".
+ $placeholder_1 = $this->placeholder() . '[]';
+ $placeholder_2 = $this->placeholder() . '[]';
+
+ $args = array_values($uids);
+
+ $this->query->addWhereExpression($group, "$this->tableAlias.uid IN ($placeholder_1) OR
+ EXISTS (SELECT 1 FROM {node_revision} nr WHERE nr.revision_uid IN ($placeholder_2) AND nr.nid = $this->tableAlias.nid)", [
+ $placeholder_1 => $args,
+ $placeholder_2 => $args,
+ ]);
+ }
+
+}
diff --git a/core/modules/node/src/Plugin/views/argument/UidRevision.php b/core/modules/node/src/Plugin/views/argument/UidRevision.php
index 982152080a6..9be0cc9d7b6 100644
--- a/core/modules/node/src/Plugin/views/argument/UidRevision.php
+++ b/core/modules/node/src/Plugin/views/argument/UidRevision.php
@@ -2,6 +2,7 @@
namespace Drupal\node\Plugin\views\argument;
+use Drupal\node\Plugin\views\UidRevisionTrait;
use Drupal\user\Plugin\views\argument\Uid;
use Drupal\views\Attribute\ViewsArgument;
@@ -15,13 +16,13 @@ use Drupal\views\Attribute\ViewsArgument;
)]
class UidRevision extends Uid {
+ use UidRevisionTrait;
+
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
- $this->ensureMyTable();
- $placeholder = $this->placeholder();
- $this->query->addWhereExpression(0, "$this->tableAlias.uid = $placeholder OR ((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nr.revision_uid = $placeholder AND nr.nid = $this->tableAlias.nid) > 0)", [$placeholder => $this->argument]);
+ $this->uidRevisionQuery([$this->argument]);
}
}
diff --git a/core/modules/node/src/Plugin/views/filter/Access.php b/core/modules/node/src/Plugin/views/filter/Access.php
index 4934a2f2e63..4d579f687ce 100644
--- a/core/modules/node/src/Plugin/views/filter/Access.php
+++ b/core/modules/node/src/Plugin/views/filter/Access.php
@@ -36,7 +36,7 @@ class Access extends FilterPluginBase {
*/
public function query() {
$account = $this->view->getUser();
- if (!$account->hasPermission('bypass node access')) {
+ if (!$account->hasPermission('bypass node access') && $this->moduleHandler->hasImplementations('node_grants')) {
$table = $this->ensureMyTable();
$grants = $this->query->getConnection()->condition('OR');
foreach (node_access_grants('view', $account) as $realm => $gids) {
diff --git a/core/modules/node/src/Plugin/views/filter/UidRevision.php b/core/modules/node/src/Plugin/views/filter/UidRevision.php
index b7f186fa07d..cf962a2897e 100644
--- a/core/modules/node/src/Plugin/views/filter/UidRevision.php
+++ b/core/modules/node/src/Plugin/views/filter/UidRevision.php
@@ -2,6 +2,7 @@
namespace Drupal\node\Plugin\views\filter;
+use Drupal\node\Plugin\views\UidRevisionTrait;
use Drupal\user\Plugin\views\filter\Name;
use Drupal\views\Attribute\ViewsFilter;
@@ -13,19 +14,13 @@ use Drupal\views\Attribute\ViewsFilter;
#[ViewsFilter("node_uid_revision")]
class UidRevision extends Name {
+ use UidRevisionTrait;
+
/**
* {@inheritdoc}
*/
public function query($group_by = FALSE) {
- $this->ensureMyTable();
-
- $placeholder = $this->placeholder() . '[]';
-
- $args = array_values($this->value);
-
- $this->query->addWhereExpression($this->options['group'], "$this->tableAlias.uid IN($placeholder) OR
- ((SELECT COUNT(DISTINCT vid) FROM {node_revision} nr WHERE nr.revision_uid IN ($placeholder) AND nr.nid = $this->tableAlias.nid) > 0)", [$placeholder => $args],
- $args);
+ $this->uidRevisionQuery($this->value, $this->options['group']);
}
}
diff --git a/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php
index 1887bc56ea6..201e781d196 100644
--- a/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php
+++ b/core/modules/node/tests/modules/node_test/src/Hook/NodeTestHooks.php
@@ -32,14 +32,14 @@ class NodeTestHooks {
];
// Add content that should be displayed only in the RSS feed.
$build['extra_feed_content'] = [
- '#markup' => '<p>' . 'Extra data that should appear only in the RSS feed for node ' . $node->id() . '.</p>',
+ '#markup' => '<p>Extra data that should appear only in the RSS feed for node ' . $node->id() . '.</p>',
'#weight' => 10,
];
}
if ($view_mode != 'rss') {
// Add content that should NOT be displayed in the RSS feed.
$build['extra_non_feed_content'] = [
- '#markup' => '<p>' . 'Extra data that should appear everywhere except the RSS feed for node ' . $node->id() . '.</p>',
+ '#markup' => '<p>Extra data that should appear everywhere except the RSS feed for node ' . $node->id() . '.</p>',
];
}
}
diff --git a/core/modules/node/tests/src/Functional/NodeCreationTest.php b/core/modules/node/tests/src/Functional/NodeCreationTest.php
index f0192966a1b..7e99e3ba2ec 100644
--- a/core/modules/node/tests/src/Functional/NodeCreationTest.php
+++ b/core/modules/node/tests/src/Functional/NodeCreationTest.php
@@ -183,8 +183,9 @@ class NodeCreationTest extends NodeTestBase {
// Confirm that the node was created.
$this->assertSession()->pageTextContains('Basic page ' . $edit['title[0][value]'] . ' has been created.');
- // Verify that the creation message contains a link to a node.
- $this->assertSession()->elementExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "node/")]');
+ // Verify that the creation message doesn't contain a link to a node since
+ // the user cannot view unpublished nodes.
+ $this->assertSession()->elementNotExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "node/")]');
}
/**
@@ -310,6 +311,21 @@ class NodeCreationTest extends NodeTestBase {
}
/**
+ * Tests exception handling when saving a node through the form.
+ */
+ public function testNodeCreateExceptionHandling(): void {
+ $this->drupalGet('node/add/page');
+
+ $this->submitForm([
+ 'title[0][value]' => 'testing_transaction_exception',
+ 'body[0][value]' => $this->randomMachineName(16),
+ ], 'Save');
+
+ $this->assertSession()->pageTextNotContains('The website encountered an unexpected error.');
+ $this->assertSession()->pageTextContains('The content could not be saved. Contact the site administrator if the problem persists.');
+ }
+
+ /**
* Gets the watchdog IDs of the records with the rollback exception message.
*
* @return int[]
diff --git a/core/modules/node/tests/src/Functional/NodeEditFormTest.php b/core/modules/node/tests/src/Functional/NodeEditFormTest.php
index dc47998c909..f661ae18ebb 100644
--- a/core/modules/node/tests/src/Functional/NodeEditFormTest.php
+++ b/core/modules/node/tests/src/Functional/NodeEditFormTest.php
@@ -251,10 +251,15 @@ class NodeEditFormTest extends NodeTestBase {
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->submitForm($edit, 'Save');
+ // Enable user_hooks_test to test the users display name is visible on the
+ // edit form.
+ \Drupal::service('module_installer')->install(['user_hooks_test']);
+ \Drupal::keyValue('user_hooks_test')->set('user_format_name_alter', TRUE);
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
- $this->drupalGet("node/" . $node->id() . "/edit");
+ $this->drupalGet($node->toUrl('edit-form'));
$this->assertSession()->pageTextContains('Published');
$this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short'));
+ $this->assertSession()->responseContains('<em>' . $this->adminUser->id() . '</em>');
}
/**
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php
new file mode 100644
index 00000000000..5a930df3e2d
--- /dev/null
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsAuthorTest.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\node\Functional;
+
+use Drupal\Core\Url;
+
+/**
+ * Tests reverting node revisions correctly sets authorship information.
+ *
+ * @group node
+ */
+class NodeRevisionsAuthorTest extends NodeTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'stark';
+
+ /**
+ * Tests node authorship is retained after reverting revisions.
+ */
+ public function testNodeRevisionRevertAuthors(): void {
+ // Create and log in user.
+ $initialUser = $this->drupalCreateUser([
+ 'view page revisions',
+ 'revert page revisions',
+ 'edit any page content',
+ ]);
+ $initialRevisionUser = $this->drupalCreateUser();
+ // Third user is an author only and needs no permissions
+ $initialRevisionAuthor = $this->drupalCreateUser();
+
+ // Create initial node (author: $user1).
+ $this->drupalLogin($initialUser);
+ $node = $this->drupalCreateNode();
+ $originalRevisionId = $node->getRevisionId();
+ $originalBody = $node->body->value;
+ $originalTitle = $node->getTitle();
+
+ // Create a revision (as $initialUser) showing $initialRevisionAuthor
+ // as author.
+ $node->setRevisionLogMessage('Changed author');
+ $revisedTitle = $this->randomMachineName();
+ $node->setTitle($revisedTitle);
+ $revisedBody = $this->randomMachineName(32);
+ $node->set('body', [
+ 'value' => $revisedBody,
+ 'format' => filter_default_format(),
+ ]);
+ $node->setOwnerId($initialRevisionAuthor->id());
+ $node->setRevisionUserId($initialRevisionUser->id());
+ $node->setNewRevision();
+ $node->save();
+ $revisedRevisionId = $node->getRevisionId();
+
+ $nodeStorage = \Drupal::entityTypeManager()->getStorage('node');
+
+ self::assertEquals($node->getOwnerId(), $initialRevisionAuthor->id());
+ self::assertEquals($node->getRevisionUserId(), $initialRevisionUser->id());
+
+ // Revert to the original node revision.
+ $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
+ 'node' => $node->id(),
+ 'node_revision' => $originalRevisionId,
+ ]));
+ $this->submitForm([], 'Revert');
+ $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $originalTitle));
+
+ // With the revert done, reload the node and verify that the authorship
+ // fields have reverted correctly.
+ $nodeStorage->resetCache([$node->id()]);
+ /** @var \Drupal\node\NodeInterface $revertedNode */
+ $revertedNode = $nodeStorage->load($node->id());
+ self::assertEquals($originalBody, $revertedNode->body->value);
+ self::assertEquals($initialUser->id(), $revertedNode->getOwnerId());
+ self::assertEquals($initialUser->id(), $revertedNode->getRevisionUserId());
+
+ // Revert again to the revised version and check that node author and
+ // revision author fields are correct.
+ // Revert to the original node.
+ $this->drupalGet(Url::fromRoute('node.revision_revert_confirm', [
+ 'node' => $revertedNode->id(),
+ 'node_revision' => $revisedRevisionId,
+ ]));
+ $this->submitForm([], 'Revert');
+ $this->assertSession()->pageTextContains(\sprintf('Basic page %s has been reverted', $revisedTitle));
+
+ // With the reversion done, reload the node and verify that the
+ // authorship fields have reverted correctly.
+ $nodeStorage->resetCache([$revertedNode->id()]);
+ /** @var \Drupal\node\NodeInterface $re_reverted_node */
+ $re_reverted_node = $nodeStorage->load($revertedNode->id());
+ self::assertEquals($revisedBody, $re_reverted_node->body->value);
+ self::assertEquals($initialRevisionAuthor->id(), $re_reverted_node->getOwnerId());
+ // The new revision user will be the current logged in user as set in
+ // NodeRevisionRevertForm.
+ self::assertEquals($initialUser->id(), $re_reverted_node->getRevisionUserId());
+ }
+
+}
diff --git a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
index 201d4b6c7d2..88fe3e34e3e 100644
--- a/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
+++ b/core/modules/node/tests/src/Functional/NodeRevisionsUiTest.php
@@ -215,20 +215,4 @@ class NodeRevisionsUiTest extends NodeTestBase {
$this->assertSession()->elementsCount('xpath', $xpath, 1);
}
- /**
- * Tests the node revisions page is cacheable by dynamic page cache.
- */
- public function testNodeRevisionsCacheability(): void {
- $this->drupalLogin($this->editor);
- $node = $this->drupalCreateNode();
- // Admin paths are always uncacheable by dynamic page cache, swap node
- // to non admin theme to test cacheability.
- $this->config('node.settings')->set('use_admin_theme', FALSE)->save();
- \Drupal::service('router.builder')->rebuild();
- $this->drupalGet($node->toUrl('version-history'));
- $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
- $this->drupalGet($node->toUrl('version-history'));
- $this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
- }
-
}
diff --git a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
index c3a3d46b496..f8d52b06ecb 100644
--- a/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
+++ b/core/modules/node/tests/src/Functional/NodeSyndicateBlockTest.php
@@ -8,6 +8,7 @@ namespace Drupal\Tests\node\Functional;
* Tests if the syndicate block is available.
*
* @group node
+ * @group legacy
*/
class NodeSyndicateBlockTest extends NodeTestBase {
@@ -40,6 +41,7 @@ class NodeSyndicateBlockTest extends NodeTestBase {
$this->drupalPlaceBlock('node_syndicate_block', ['id' => 'test_syndicate_block', 'label' => 'Subscribe to RSS Feed']);
$this->drupalGet('');
$this->assertSession()->elementExists('xpath', '//div[@id="block-test-syndicate-block"]/*');
+ $this->expectDeprecation('The Syndicate block is deprecated in drupal:11.2.0 and will be removed from drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3519248');
// Verify syndicate block title.
$this->assertSession()->pageTextContains('Subscribe to RSS Feed');
diff --git a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
index 2bb252f7c6e..ac1e8664bad 100644
--- a/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
+++ b/core/modules/node/tests/src/Functional/NodeTranslationUITest.php
@@ -242,21 +242,19 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
// Set up the default admin theme and use it for node editing.
$this->container->get('theme_installer')->install(['claro']);
- $edit = [];
- $edit['admin_theme'] = 'claro';
- $edit['use_admin_theme'] = TRUE;
- $this->drupalGet('admin/appearance');
- $this->submitForm($edit, 'Save configuration');
- $this->drupalGet('node/' . $article->id() . '/translations');
+ $this->config('system.theme')->set('admin', 'claro')->save();
+
// Verify that translation uses the admin theme if edit is admin.
+ $this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseContains('core/themes/claro/css/base/elements.css');
// Turn off admin theme for editing, assert inheritance to translations.
- $edit['use_admin_theme'] = FALSE;
- $this->drupalGet('admin/appearance');
- $this->submitForm($edit, 'Save configuration');
- $this->drupalGet('node/' . $article->id() . '/translations');
+ $this->config('node.settings')->set('use_admin_theme', FALSE)->save();
+ // Changing node.settings:use_admin_theme requires a route rebuild.
+ $this->container->get('router.builder')->rebuild();
+
// Verify that translation uses the frontend theme if edit is frontend.
+ $this->drupalGet('node/' . $article->id() . '/translations');
$this->assertSession()->responseNotContains('core/themes/claro/css/base/elements.css');
// Assert presence of translation page itself (vs. DisabledBundle below).
@@ -561,12 +559,10 @@ class NodeTranslationUITest extends ContentTranslationUITestBase {
'translatable' => TRUE,
])->save();
- $this->drupalLogin($this->administrator);
// Make the image field a multi-value field in order to display a
// details form element.
- $edit = ['field_storage[subform][cardinality_number]' => 2];
- $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image');
- $this->submitForm($edit, 'Save');
+ $fieldStorage = FieldStorageConfig::loadByName('node', 'field_image');
+ $fieldStorage->setCardinality(2)->save();
// Enable the display of the image field.
EntityFormDisplay::load('node.article.default')
diff --git a/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php b/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php
index 84f4a376af9..6333930b886 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/CollapsedSummariesTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests that outlines of node meta values are displayed in summaries and tabs.
- *
- * @group node
*/
+#[Group('node')]
class CollapsedSummariesTest extends WebDriverTestBase {
/**
diff --git a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
index 4c499c01a86..876d9606776 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/ContextualLinksTest.php
@@ -7,12 +7,12 @@ namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\node\Entity\Node;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
+use PHPUnit\Framework\Attributes\Group;
/**
* Create a node with revisions and test contextual links.
- *
- * @group node
*/
+#[Group('node')]
class ContextualLinksTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
diff --git a/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php b/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php
index 5eb912c577c..22a33d0c4b8 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/NodeDeleteConfirmTest.php
@@ -6,12 +6,12 @@ namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\views\Views;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests JavaScript functionality specific to delete operations.
- *
- * @group node
*/
+#[Group('node')]
class NodeDeleteConfirmTest extends WebDriverTestBase {
/**
diff --git a/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php
index 33a8795a649..e1dbf426cec 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/NodePreviewLinkTest.php
@@ -6,12 +6,12 @@ namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the JavaScript prevention of navigation away from node previews.
- *
- * @group node
*/
+#[Group('node')]
class NodePreviewLinkTest extends WebDriverTestBase {
/**
diff --git a/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php b/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php
index 99ba0722d00..309c14c37b3 100644
--- a/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php
+++ b/core/modules/node/tests/src/FunctionalJavascript/SettingSummariesContentTypeTest.php
@@ -5,12 +5,12 @@ declare(strict_types=1);
namespace Drupal\Tests\node\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use PHPUnit\Framework\Attributes\Group;
/**
* Tests the JavaScript updating of summaries on content type form.
- *
- * @group node
*/
+#[Group('node')]
class SettingSummariesContentTypeTest extends WebDriverTestBase {
/**
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
index cbe9b346623..ac47588d5ec 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeCompleteTest.php
@@ -18,6 +18,7 @@ use Drupal\Tests\migrate_drupal\Traits\NodeMigrateTypeTestTrait;
* Test class for a complete node migration for Drupal 7.
*
* @group migrate_drupal_7
+ * @group #slow
*/
class MigrateNodeCompleteTest extends MigrateDrupal7TestBase {
diff --git a/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php b/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php
index 60ce5c7cdb0..b86b69e8ad1 100644
--- a/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php
+++ b/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\Tests\node\Kernel;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\views\Entity\View;
@@ -77,7 +78,7 @@ class NodeRequirementsStatusFilterWarningTest extends KernelTestBase {
$requirements = $this->getRequirements();
$this->assertArrayHasKey('node_status_filter', $requirements);
- $this->assertEquals(REQUIREMENT_WARNING, $requirements['node_status_filter']['severity']);
+ $this->assertEquals(RequirementSeverity::Warning, $requirements['node_status_filter']['severity']);
}
/**
@@ -102,7 +103,7 @@ class NodeRequirementsStatusFilterWarningTest extends KernelTestBase {
$requirements = $this->getRequirements();
$this->assertArrayHasKey('node_status_filter', $requirements);
- $this->assertEquals(REQUIREMENT_WARNING, $requirements['node_status_filter']['severity']);
+ $this->assertEquals(RequirementSeverity::Warning, $requirements['node_status_filter']['severity']);
}
/**