diff options
Diffstat (limited to 'core/modules/node')
60 files changed, 712 insertions, 443 deletions
diff --git a/core/modules/node/config/optional/views.view.content.yml b/core/modules/node/config/optional/views.view.content.yml index d0bbb09cf5a..09ba3dcc35a 100644 --- a/core/modules/node/config/optional/views.view.content.yml +++ b/core/modules/node/config/optional/views.view.content.yml @@ -625,6 +625,7 @@ display: empty_table: true caption: '' description: '' + class: '' row: type: fields query: diff --git a/core/modules/node/config/optional/views.view.glossary.yml b/core/modules/node/config/optional/views.view.glossary.yml index 2698473d527..868d51fb9e2 100644 --- a/core/modules/node/config/optional/views.view.glossary.yml +++ b/core/modules/node/config/optional/views.view.glossary.yml @@ -343,6 +343,7 @@ display: summary: '' order: asc empty_table: false + class: '' row: type: fields options: diff --git a/core/modules/node/config/schema/node.schema.yml b/core/modules/node/config/schema/node.schema.yml index 08fe92cc401..8c81f68ae60 100644 --- a/core/modules/node/config/schema/node.schema.yml +++ b/core/modules/node/config/schema/node.schema.yml @@ -24,7 +24,7 @@ node.type.*: label: 'Machine-readable name' constraints: # Node type machine names are specifically limited to 32 characters. - # @see \Drupal\node\NodeTypeForm::form() + # @see \Drupal\node\Form\NodeTypeForm::form() Length: max: 32 description: @@ -50,7 +50,7 @@ node.type.*: constraints: # These are the values of the DRUPAL_DISABLED, DRUPAL_OPTIONAL, and # DRUPAL_REQUIRED constants. - # @see \Drupal\node\NodeTypeForm::form() + # @see \Drupal\node\Form\NodeTypeForm::form() Choice: [0, 1, 2] display_submitted: type: boolean 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.install b/core/modules/node/node.install index fb5c14c4767..4e232bdc2de 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -5,137 +5,10 @@ * Install, update and uninstall functions for the node module. */ -use Drupal\Core\Link; -use Drupal\Core\StringTranslation\PluralTranslatableMarkup; -use Drupal\Core\Url; use Drupal\Core\Database\Database; use Drupal\user\RoleInterface; /** - * Implements hook_requirements(). - */ -function node_requirements($phase): array { - $requirements = []; - if ($phase === 'runtime') { - // Only show rebuild button if there are either 0, or 2 or more, rows - // in the {node_access} table, or if there are modules that - // implement hook_node_grants(). - $grant_count = \Drupal::entityTypeManager()->getAccessControlHandler('node')->countGrants(); - $has_node_grants_implementations = \Drupal::moduleHandler()->hasImplementations('node_grants'); - if ($grant_count != 1 || $has_node_grants_implementations) { - $value = \Drupal::translation()->formatPlural($grant_count, 'One permission in use', '@count permissions in use', ['@count' => $grant_count]); - } - else { - $value = t('Disabled'); - } - - $requirements['node_access'] = [ - 'title' => t('Node Access Permissions'), - 'value' => $value, - 'description' => t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Rebuilding will remove all privileges to content and replace them with permissions based on the current modules and settings. Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed, content will automatically use the new permissions. <a href=":rebuild">Rebuild permissions</a>', [ - ':rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString(), - ]), - ]; - - // Report when the "Published status or admin user" has no impact on the - // result of dependent views due to active node access modules. - // @see https://www.drupal.org/node/3472976 - if ($has_node_grants_implementations && \Drupal::moduleHandler()->moduleExists('views')) { - $node_status_filter_problematic_views = []; - $active_view_ids = \Drupal::entityQuery('view') - ->condition('status', TRUE) - ->accessCheck(FALSE) - ->execute(); - - $views_storage = \Drupal::entityTypeManager()->getStorage('view'); - foreach ($views_storage->loadMultiple($active_view_ids) as $view) { - foreach ($view->get('display') as $display_id => $display) { - if (array_key_exists('filters', $display['display_options'])) { - foreach ($display['display_options']['filters'] as $filter) { - if (array_key_exists('plugin_id', $filter) && $filter['plugin_id'] === 'node_status') { - $node_status_filter_problematic_views[$view->id()][$display_id] = [ - 'view_label' => $view->label(), - 'display_name' => $display['display_title'] ?? $display_id, - ]; - break; - } - } - } - } - } - - if ($node_status_filter_problematic_views !== []) { - $node_access_implementations = []; - $module_data = \Drupal::service('extension.list.module')->getAllInstalledInfo(); - foreach (['node_grants', 'node_grants_alter'] as $hook) { - \Drupal::moduleHandler()->invokeAllWith( - $hook, - static function (callable $hook, string $module) use (&$node_access_implementations, $module_data) { - $node_access_implementations[$module] = $module_data[$module]['name']; - } - ); - } - uasort($node_access_implementations, 'strnatcasecmp'); - $views_ui_enabled = \Drupal::moduleHandler()->moduleExists('views_ui'); - $node_status_filter_problematic_views_list = []; - foreach ($node_status_filter_problematic_views as $view_id => $displays) { - foreach ($displays as $display_id => $info) { - $text = "{$info['view_label']} ({$info['display_name']})"; - if ($views_ui_enabled) { - $url = Url::fromRoute('entity.view.edit_display_form', [ - 'view' => $view_id, - 'display_id' => $display_id, - ]); - if ($url->access()) { - $node_status_filter_problematic_views_list[] = Link::fromTextAndUrl($text, $url)->toString(); - } - else { - $node_status_filter_problematic_views_list[] = $text; - } - } - else { - $node_status_filter_problematic_views_list[] = $text; - } - } - } - - $node_status_filter_problematic_views_count = count($node_status_filter_problematic_views_list); - $node_status_filter_description_arguments = [ - '%modules' => implode(', ', $node_access_implementations), - '%status_filter' => t('Published status or admin user'), - ]; - - if ($node_status_filter_problematic_views_count > 1) { - $node_status_filter_problematic_views_list = [ - '#theme' => 'item_list', - '#items' => $node_status_filter_problematic_views_list, - ]; - $node_status_filter_description_arguments['@views'] = \Drupal::service('renderer')->renderInIsolation($node_status_filter_problematic_views_list); - } - else { - $node_status_filter_description_arguments['%view'] = reset($node_status_filter_problematic_views_list); - } - - $node_status_filter_description = new PluralTranslatableMarkup( - $node_status_filter_problematic_views_count, - 'The %view view uses the %status_filter filter but it has no effect because the following module(s) control access: %modules. Review and consider removing the filter.', - 'The following views use the %status_filter filter but it has no effect because the following module(s) control access: %modules. Review and consider removing the filter from these views: @views', - $node_status_filter_description_arguments, - ); - - $requirements['node_status_filter'] = [ - 'title' => t('Content status filter'), - 'value' => t('Redundant filters detected'), - 'description' => $node_status_filter_description, - 'severity' => REQUIREMENT_WARNING, - ]; - } - } - } - return $requirements; -} - -/** * Implements hook_schema(). */ function node_schema(): array { diff --git a/core/modules/node/node.module b/core/modules/node/node.module index e2b0fcccb2e..c99569a1c67 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 = []; @@ -105,10 +109,11 @@ function node_type_get_names() { * @return string|false * The node type label or FALSE if the node type is not found. * - * @todo Add this as generic helper method for config entities representing - * entity bundles. + * @deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use $node->getBundleEntity()->label() instead. + * @see https://www.drupal.org/node/3533301 */ function node_get_type_label(NodeInterface $node) { + @trigger_error(__FUNCTION__ . '() is deprecated in drupal:11.3.0 and is removed from drupal:13.0.0. Use $node->getBundleEntity()->label(). See https://www.drupal.org/node/3533301', E_USER_DEPRECATED); $type = NodeType::load($node->bundle()); return $type ? $type->label() : FALSE; } @@ -121,8 +126,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(); } @@ -257,30 +266,6 @@ function node_preprocess_block(&$variables): void { } /** - * Implements hook_preprocess_HOOK() for node field templates. - */ -function node_preprocess_field__node(&$variables): void { - // Set a variable 'is_inline' in cases where inline markup is required, - // without any block elements such as <div>. - - if ($variables['element']['#is_page_title'] ?? FALSE) { - // Page title is always inline because it will be displayed inside <h1>. - $variables['is_inline'] = TRUE; - } - elseif (in_array($variables['field_name'], ['created', 'uid', 'title'], TRUE)) { - // Display created, uid and title fields inline because they will be - // displayed inline by node.html.twig. Skip this if the field - // display is configurable and skipping has been enabled. - // @todo Delete as part of https://www.drupal.org/node/3015623 - - /** @var \Drupal\node\NodeInterface $node */ - $node = $variables['element']['#object']; - $skip_custom_preprocessing = $node->getEntityType()->get('enable_base_field_custom_preprocess_skipping'); - $variables['is_inline'] = !$skip_custom_preprocessing || !$node->getFieldDefinition($variables['field_name'])->isDisplayConfigurable('view'); - } -} - -/** * Implements hook_theme_suggestions_HOOK(). */ function node_theme_suggestions_node(array $variables): array { @@ -349,10 +334,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'))) { @@ -411,6 +397,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 @@ -568,7 +559,7 @@ function node_access_rebuild($batch_mode = FALSE): void { } } else { - // Try to allocate enough time to rebuild node grants + // Try to allocate enough time to rebuild node grants. Environment::setTimeLimit(240); // Rebuild newest nodes first so that recent content becomes available @@ -682,6 +673,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/node.routing.yml b/core/modules/node/node.routing.yml index 4ceb843af18..aa53d913cf7 100644 --- a/core/modules/node/node.routing.yml +++ b/core/modules/node/node.routing.yml @@ -45,6 +45,7 @@ entity.node.preview: requirements: _node_preview_access: '{node_preview}' options: + no_cache: TRUE parameters: node_preview: type: 'node_preview' diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml index e5daad35429..83eb19f7355 100644 --- a/core/modules/node/node.services.yml +++ b/core/modules/node/node.services.yml @@ -1,3 +1,16 @@ +parameters: + node.moved_classes: + 'Drupal\node\NodeForm': + class: 'Drupal\node\Form\NodeForm' + deprecation_version: drupal:11.2.0 + removed_version: drupal:12.0.0 + change_record: https://www.drupal.org/node/3517871 + 'Drupal\node\NodeTypeForm': + class: 'Drupal\node\Form\NodeTypeForm' + deprecation_version: drupal:11.2.0 + removed_version: drupal:12.0.0 + change_record: https://www.drupal.org/node/3517871 + services: _defaults: autoconfigure: true @@ -23,13 +36,6 @@ services: tags: - { name: paramconverter } lazy: true - node.page_cache_response_policy.deny_node_preview: - class: Drupal\node\PageCache\DenyNodePreview - arguments: ['@current_route_match'] - public: false - tags: - - { name: page_cache_response_policy } - - { name: dynamic_page_cache_response_policy } cache_context.user.node_grants: class: Drupal\node\Cache\NodeAccessGrantsCacheContext arguments: ['@current_user'] 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/Entity/Node.php b/core/modules/node/src/Entity/Node.php index f4c519a43c8..0f61a74f1c0 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -11,8 +11,8 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\Form\DeleteMultiple; use Drupal\node\Form\NodeDeleteForm; +use Drupal\node\Form\NodeForm; use Drupal\node\NodeAccessControlHandler; -use Drupal\node\NodeForm; use Drupal\node\NodeInterface; use Drupal\node\NodeListBuilder; use Drupal\node\NodeStorage; diff --git a/core/modules/node/src/Entity/NodeType.php b/core/modules/node/src/Entity/NodeType.php index 35b911d7ffb..48d295635f3 100644 --- a/core/modules/node/src/Entity/NodeType.php +++ b/core/modules/node/src/Entity/NodeType.php @@ -9,7 +9,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\node\Form\NodeTypeDeleteConfirm; use Drupal\node\NodeTypeAccessControlHandler; -use Drupal\node\NodeTypeForm; +use Drupal\node\Form\NodeTypeForm; use Drupal\node\NodeTypeInterface; use Drupal\node\NodeTypeListBuilder; use Drupal\user\Entity\EntityPermissionsRouteProvider; diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/Form/NodeForm.php index ab81a4e3f92..5498c94c497 100644 --- a/core/modules/node/src/NodeForm.php +++ b/core/modules/node/src/Form/NodeForm.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\node; +namespace Drupal\node\Form; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Datetime\DateFormatterInterface; @@ -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; /** @@ -124,7 +125,7 @@ class NodeForm extends ContentEntityForm { if ($this->operation == 'edit') { $form['#title'] = $this->t('<em>Edit @type</em> @title', [ - '@type' => node_get_type_label($node), + '@type' => $node->getBundleEntity()->label(), '@title' => $node->label(), ]); } @@ -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->getBundleEntity()->label(), '%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/Form/NodeRevisionRevertForm.php b/core/modules/node/src/Form/NodeRevisionRevertForm.php index ffb58fb71fb..a1e04e2942d 100644 --- a/core/modules/node/src/Form/NodeRevisionRevertForm.php +++ b/core/modules/node/src/Form/NodeRevisionRevertForm.php @@ -136,7 +136,7 @@ class NodeRevisionRevertForm extends ConfirmFormBase { $this->logger('content')->info('@type: reverted %title revision %revision.', ['@type' => $this->revision->bundle(), '%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]); $this->messenger() ->addStatus($this->t('@type %title has been reverted to the revision from %revision-date.', [ - '@type' => node_get_type_label($this->revision), + '@type' => $this->revision->getBundleEntity()->label(), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp), ])); diff --git a/core/modules/node/src/NodeTypeForm.php b/core/modules/node/src/Form/NodeTypeForm.php index 3328ade970d..93d510d4387 100644 --- a/core/modules/node/src/NodeTypeForm.php +++ b/core/modules/node/src/Form/NodeTypeForm.php @@ -1,6 +1,6 @@ <?php -namespace Drupal\node; +namespace Drupal\node\Form; use Drupal\Core\Entity\BundleEntityFormBase; use Drupal\Core\Entity\EntityFieldManagerInterface; 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..e103717fb61 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>'; @@ -219,7 +219,7 @@ class NodeHooks1 { $ranking = [ 'relevance' => [ 'title' => $this->t('Keyword relevance'), - // Average relevance values hover around 0.15 + // Average relevance values hover around 0.15. 'score' => 'i.relevance', ], 'sticky' => [ diff --git a/core/modules/node/src/Hook/NodeRequirements.php b/core/modules/node/src/Hook/NodeRequirements.php new file mode 100644 index 00000000000..84f74aee98c --- /dev/null +++ b/core/modules/node/src/Hook/NodeRequirements.php @@ -0,0 +1,155 @@ +<?php + +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; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Link; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\Url; + +/** + * Requirements for the Node module. + */ +class NodeRequirements { + + use StringTranslationTrait; + + public function __construct( + protected readonly EntityTypeManagerInterface $entityTypeManager, + protected readonly ModuleHandlerInterface $moduleHandler, + protected readonly TranslationInterface $translation, + protected readonly ModuleExtensionList $moduleExtensionList, + ) {} + + /** + * Implements hook_runtime_requirements(). + */ + #[Hook('runtime_requirements')] + public function runtime(): array { + $requirements = []; + // Only show rebuild button if there are either 0, or 2 or more, rows + // in the {node_access} table, or if there are modules that + // implement hook_node_grants(). + $grant_count = $this->entityTypeManager->getAccessControlHandler('node')->countGrants(); + $has_node_grants_implementations = $this->moduleHandler->hasImplementations('node_grants'); + if ($grant_count != 1 || $has_node_grants_implementations) { + $value = $this->translation->formatPlural($grant_count, 'One permission in use', '@count permissions in use', ['@count' => $grant_count]); + } + else { + $value = $this->t('Disabled'); + } + + $requirements['node_access'] = [ + 'title' => $this->t('Node Access Permissions'), + 'value' => $value, + 'description' => $this->t('If the site is experiencing problems with permissions to content, you may have to rebuild the permissions cache. Rebuilding will remove all privileges to content and replace them with permissions based on the current modules and settings. Rebuilding may take some time if there is a lot of content or complex permission settings. After rebuilding has completed, content will automatically use the new permissions. <a href=":rebuild">Rebuild permissions</a>', [ + ':rebuild' => Url::fromRoute('node.configure_rebuild_confirm')->toString(), + ]), + ]; + + // Report when the "Published status or admin user" has no impact on the + // result of dependent views due to active node access modules. + // @see https://www.drupal.org/node/3472976 + if ($has_node_grants_implementations && $this->moduleHandler->moduleExists('views')) { + $node_status_filter_problematic_views = []; + $query = $this->entityTypeManager->getStorage('view')->getQuery(); + $query->condition('status', TRUE); + $query->accessCheck(FALSE); + $active_view_ids = $query->execute(); + + $views_storage = $this->entityTypeManager->getStorage('view'); + foreach ($views_storage->loadMultiple($active_view_ids) as $view) { + foreach ($view->get('display') as $display_id => $display) { + if (array_key_exists('filters', $display['display_options'])) { + foreach ($display['display_options']['filters'] as $filter) { + if (array_key_exists('plugin_id', $filter) && $filter['plugin_id'] === 'node_status') { + $node_status_filter_problematic_views[$view->id()][$display_id] = [ + 'view_label' => $view->label(), + 'display_name' => $display['display_title'] ?? $display_id, + ]; + break; + } + } + } + } + } + + if ($node_status_filter_problematic_views !== []) { + $node_access_implementations = []; + $module_data = $this->moduleExtensionList->getAllInstalledInfo(); + foreach (['node_grants', 'node_grants_alter'] as $hook) { + $this->moduleHandler->invokeAllWith( + $hook, + static function (callable $hook, string $module) use (&$node_access_implementations, $module_data) { + $node_access_implementations[$module] = $module_data[$module]['name']; + } + ); + } + uasort($node_access_implementations, 'strnatcasecmp'); + $views_ui_enabled = $this->moduleHandler->moduleExists('views_ui'); + $node_status_filter_problematic_views_list = []; + foreach ($node_status_filter_problematic_views as $view_id => $displays) { + foreach ($displays as $display_id => $info) { + $text = "{$info['view_label']} ({$info['display_name']})"; + if ($views_ui_enabled) { + $url = Url::fromRoute('entity.view.edit_display_form', [ + 'view' => $view_id, + 'display_id' => $display_id, + ]); + if ($url->access()) { + $node_status_filter_problematic_views_list[] = Link::fromTextAndUrl($text, $url)->toString(); + } + else { + $node_status_filter_problematic_views_list[] = $text; + } + } + else { + $node_status_filter_problematic_views_list[] = $text; + } + } + } + + $node_status_filter_problematic_views_count = count($node_status_filter_problematic_views_list); + $node_status_filter_description_arguments = [ + '%modules' => implode(', ', $node_access_implementations), + '%status_filter' => $this->t('Published status or admin user'), + ]; + + if ($node_status_filter_problematic_views_count > 1) { + $node_status_filter_problematic_views_list = [ + '#theme' => 'item_list', + '#items' => $node_status_filter_problematic_views_list, + ]; + $node_status_filter_description_arguments['@views'] = \Drupal::service('renderer')->renderInIsolation($node_status_filter_problematic_views_list); + } + else { + $node_status_filter_description_arguments['%view'] = reset($node_status_filter_problematic_views_list); + } + + $node_status_filter_description = new PluralTranslatableMarkup( + $node_status_filter_problematic_views_count, + 'The %view view uses the %status_filter filter but it has no effect because the following module(s) control access: %modules. Review and consider removing the filter.', + 'The following views use the %status_filter filter but it has no effect because the following module(s) control access: %modules. Review and consider removing the filter from these views: @views', + $node_status_filter_description_arguments, + ); + + $requirements['node_status_filter'] = [ + 'title' => $this->t('Content status filter'), + 'value' => $this->t('Redundant filters detected'), + 'description' => $node_status_filter_description, + 'severity' => RequirementSeverity::Warning, + ]; + } + } + return $requirements; + } + +} diff --git a/core/modules/node/src/Hook/NodeThemeHooks.php b/core/modules/node/src/Hook/NodeThemeHooks.php new file mode 100644 index 00000000000..7ed0ef91f5f --- /dev/null +++ b/core/modules/node/src/Hook/NodeThemeHooks.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\node\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for the node module. + */ +class NodeThemeHooks { + + /** + * Implements hook_preprocess_HOOK() for node field templates. + */ + #[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>. + if ($variables['element']['#is_page_title'] ?? FALSE) { + // Page title is always inline because it will be displayed inside <h1>. + $variables['is_inline'] = TRUE; + } + elseif (in_array($variables['field_name'], ['created', 'uid', 'title'], TRUE)) { + // Display created, uid and title fields inline because they will be + // displayed inline by node.html.twig. Skip this if the field + // display is configurable and skipping has been enabled. + // @todo Delete as part of https://www.drupal.org/node/3015623 + + /** @var \Drupal\node\NodeInterface $node */ + $node = $variables['element']['#object']; + $skip_custom_preprocessing = $node->getEntityType()->get('enable_base_field_custom_preprocess_skipping'); + $variables['is_inline'] = !$skip_custom_preprocessing || !$node->getFieldDefinition($variables['field_name'])->isDisplayConfigurable('view'); + } + } + +} diff --git a/core/modules/node/src/Hook/NodeTokensHooks.php b/core/modules/node/src/Hook/NodeTokensHooks.php index 3d7f0b0adf4..a7a90347313 100644 --- a/core/modules/node/src/Hook/NodeTokensHooks.php +++ b/core/modules/node/src/Hook/NodeTokensHooks.php @@ -109,7 +109,7 @@ class NodeTokensHooks { break; case 'type-name': - $type_name = node_get_type_label($node); + $type_name = $node->getBundleEntity()->label(); $replacements[$original] = $type_name; break; diff --git a/core/modules/node/src/Hook/NodeViewsHooks.php b/core/modules/node/src/Hook/NodeViewsHooks.php index 477784c153d..8729f547c17 100644 --- a/core/modules/node/src/Hook/NodeViewsHooks.php +++ b/core/modules/node/src/Hook/NodeViewsHooks.php @@ -26,7 +26,7 @@ class NodeViewsHooks { if ($view->storage->get('base_table') == 'node') { foreach ($view->displayHandlers as $display) { if (!$display->isDefaulted('access') || !$display->isDefaulted('filters')) { - // Check for no access control + // Check for no access control. $access = $display->getOption('access'); if (empty($access['type']) || $access['type'] == 'none') { $anonymous_role = Role::load(RoleInterface::ANONYMOUS_ID); diff --git a/core/modules/node/src/NodeAccessControlHandler.php b/core/modules/node/src/NodeAccessControlHandler.php index 963ab53ded4..7121f62e283 100644 --- a/core/modules/node/src/NodeAccessControlHandler.php +++ b/core/modules/node/src/NodeAccessControlHandler.php @@ -223,7 +223,16 @@ class NodeAccessControlHandler extends EntityAccessControlHandler implements Nod return NULL; } + // When access is granted due to the 'view own unpublished content' + // permission and for no other reason, node grants are bypassed. However, + // to ensure the full set of cacheable metadata is available to variation + // cache, additionally add the node_grants cache context so that if the + // status or the owner of the node changes, cache redirects will continue to + // reflect the latest state without needing to be invalidated. $cacheability->addCacheContexts(['user']); + if ($this->moduleHandler->hasImplementations('node_grants')) { + $cacheability->addCacheContexts(['user.node_grants:view']); + } if ($account->id() != $node->getOwnerId()) { return NULL; } 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/NodeListBuilder.php b/core/modules/node/src/NodeListBuilder.php index 0fa80dee411..2b356eb2b34 100644 --- a/core/modules/node/src/NodeListBuilder.php +++ b/core/modules/node/src/NodeListBuilder.php @@ -95,7 +95,7 @@ class NodeListBuilder extends EntityListBuilder { '#title' => $entity->label(), '#url' => $entity->toUrl(), ]; - $row['type'] = node_get_type_label($entity); + $row['type'] = $entity->getBundleEntity()->label(); $row['author']['data'] = [ '#theme' => 'username', '#account' => $entity->getOwner(), 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/NodeTranslationHandler.php b/core/modules/node/src/NodeTranslationHandler.php index 6c802440f37..88a3680a572 100644 --- a/core/modules/node/src/NodeTranslationHandler.php +++ b/core/modules/node/src/NodeTranslationHandler.php @@ -50,8 +50,7 @@ class NodeTranslationHandler extends ContentTranslationHandler { * {@inheritdoc} */ protected function entityFormTitle(EntityInterface $entity) { - $type_name = node_get_type_label($entity); - return $this->t('<em>Edit @type</em> @title', ['@type' => $type_name, '@title' => $entity->label()]); + return $this->t('<em>Edit @type</em> @title', ['@type' => $entity->getBundleEntity()->label(), '@title' => $entity->label()]); } /** diff --git a/core/modules/node/src/PageCache/DenyNodePreview.php b/core/modules/node/src/PageCache/DenyNodePreview.php deleted file mode 100644 index 0325f52b201..00000000000 --- a/core/modules/node/src/PageCache/DenyNodePreview.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -namespace Drupal\node\PageCache; - -use Drupal\Core\PageCache\ResponsePolicyInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * Cache policy for node preview page. - * - * This policy rule denies caching of responses generated by the - * entity.node.preview route. - */ -class DenyNodePreview implements ResponsePolicyInterface { - - /** - * The current route match. - * - * @var \Drupal\Core\Routing\RouteMatchInterface - */ - protected $routeMatch; - - /** - * Constructs a deny node preview page cache policy. - * - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The current route match. - */ - public function __construct(RouteMatchInterface $route_match) { - $this->routeMatch = $route_match; - } - - /** - * {@inheritdoc} - */ - public function check(Response $response, Request $request) { - if ($this->routeMatch->getRouteName() === 'entity.node.preview') { - return static::DENY; - } - } - -} 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/src/Plugin/views/wizard/Node.php b/core/modules/node/src/Plugin/views/wizard/Node.php index 84a21c3f7f2..bef4ddc8da5 100644 --- a/core/modules/node/src/Plugin/views/wizard/Node.php +++ b/core/modules/node/src/Plugin/views/wizard/Node.php @@ -89,14 +89,10 @@ class Node extends WizardPluginBase { } /** - * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::getAvailableSorts(). - * - * @return array - * An array whose keys are the available sort options and whose - * corresponding values are human readable labels. + * {@inheritdoc} */ public function getAvailableSorts() { - // You can't execute functions in properties, so override the method + // You can't execute functions in properties, so override the method. return [ 'node_field_data-title:ASC' => $this->t('Title'), ]; @@ -238,9 +234,7 @@ class Node extends WizardPluginBase { } /** - * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::buildFilters(). - * - * Add some options for filter by taxonomy terms. + * {@inheritdoc} */ protected function buildFilters(&$form, FormStateInterface $form_state) { parent::buildFilters($form, $form_state); diff --git a/core/modules/node/src/Plugin/views/wizard/NodeRevision.php b/core/modules/node/src/Plugin/views/wizard/NodeRevision.php index c10389cc239..96422504cf5 100644 --- a/core/modules/node/src/Plugin/views/wizard/NodeRevision.php +++ b/core/modules/node/src/Plugin/views/wizard/NodeRevision.php @@ -29,11 +29,10 @@ class NodeRevision extends WizardPluginBase { protected $createdColumn = 'changed'; /** - * Overrides Drupal\views\Plugin\views\wizard\WizardPluginBase::rowStyleOptions(). - * - * Node revisions do not support full posts or teasers, so remove them. + * {@inheritdoc} */ protected function rowStyleOptions() { + // Node revisions do not support full posts or teasers, so remove them. $options = parent::rowStyleOptions(); unset($options['teasers']); unset($options['full_posts']); diff --git a/core/modules/node/tests/modules/node_no_default_author/node_no_default_author.info.yml b/core/modules/node/tests/modules/node_no_default_author/node_no_default_author.info.yml new file mode 100644 index 00000000000..c4f56344370 --- /dev/null +++ b/core/modules/node/tests/modules/node_no_default_author/node_no_default_author.info.yml @@ -0,0 +1,5 @@ +name: 'Node no default author' +type: module +description: 'Disables the default value callback for the uid field on node.' +package: Testing +version: VERSION diff --git a/core/modules/node/tests/modules/node_no_default_author/src/Hook/NodeNoDefaultAuthorHooks.php b/core/modules/node/tests/modules/node_no_default_author/src/Hook/NodeNoDefaultAuthorHooks.php new file mode 100644 index 00000000000..700e82236ad --- /dev/null +++ b/core/modules/node/tests/modules/node_no_default_author/src/Hook/NodeNoDefaultAuthorHooks.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\node_no_default_author\Hook; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for node_no_default_author. + */ +class NodeNoDefaultAuthorHooks { + + /** + * Implements hook_entity_base_field_info_alter(). + */ + #[Hook('entity_base_field_info_alter')] + public function entityBaseFieldInfoAlter(&$fields, EntityTypeInterface $entity_type): void { + if ($entity_type->id() === 'node') { + $fields['uid']->setDefaultValueCallback(static::class . '::noDefaultAuthor'); + } + } + + /** + * An empty callback to set for the default value callback of uid. + */ + public static function noDefaultAuthor(): void { + } + +} 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..11753f8ca2f 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>', ]; } } @@ -130,7 +130,7 @@ class NodeTestHooks { #[Hook('node_presave')] public function nodePresave(NodeInterface $node): void { if ($node->getTitle() == 'testing_node_presave') { - // Sun, 19 Nov 1978 05:00:00 GMT + // Sun, 19 Nov 1978 05:00:00 GMT. $node->setCreatedTime(280299600); // Drupal 1.0 release. $node->changed = 979534800; diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml index 766f656fc2b..65ca4665871 100644 --- a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_access_join.yml @@ -68,6 +68,16 @@ display: quantity: 9 style: type: table + options: + grouping: { } + class: '' + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' row: type: fields fields: diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml index d7a3fa080fe..107165a27fb 100644 --- a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_node_bulk_form.yml @@ -19,6 +19,8 @@ display: display_options: style: type: table + options: + class: '' row: type: fields fields: diff --git a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php index 2e584f3b826..9eba7e9614d 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php +++ b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php @@ -128,7 +128,7 @@ class NodeAccessBaseTableTest extends NodeTestBase { $num_simple_users = 2; $simple_users = []; - // Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private; + // Nodes keyed by uid and nid: "$nodes[$uid][$nid] = $is_private". $this->nodesByUser = []; // Titles keyed by nid. $titles = []; diff --git a/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php new file mode 100644 index 00000000000..0d49a7c416c --- /dev/null +++ b/core/modules/node/tests/src/Functional/NodeAccessCacheRedirectWarningTest.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\node\Functional; + +/** + * Tests the node access grants cache context service. + * + * @group node + * @group Cache + */ +class NodeAccessCacheRedirectWarningTest extends NodeTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['block', 'node_access_test_empty']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + node_access_rebuild(); + } + + /** + * Ensures that node access checks don't cause cache redirect warnings. + * + * @covers \Drupal\node\NodeAccessControlHandler + */ + public function testNodeAccessCacheRedirectWarning(): void { + $this->drupalPlaceBlock('local_tasks_block'); + + // Ensure that both a node_grants implementation exists, and that the + // current user has 'view own unpublished nodes' permission. Node's access + // control handler bypasses node grants when 'view own published nodes' is + // granted and the node is unpublished, which means that the code path is + // significantly different when a node is published vs. unpublished, and + // that cache contexts vary depend on the state of the node. + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants')); + + $author = $this->drupalCreateUser([ + 'create page content', + 'edit any page content', + 'view own unpublished content', + ]); + $this->drupalLogin($author); + + $node = $this->drupalCreateNode(['uid' => $author->id(), 'status' => 0]); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains($node->label()); + + $node->setPublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains($node->label()); + + // When the node has been viewed in both the unpublished and published state + // a cache redirect should exist for the local tasks block. Repeating the + // process of changing the node status and viewing the node will test that + // no stale redirect is found. + $node->setUnpublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains($node->label()); + + $node->setPublished(); + $node->save(); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains($node->label()); + } + +} diff --git a/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php b/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php index 27385e9666b..e67b27d4c3f 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php +++ b/core/modules/node/tests/src/Functional/NodeAccessGrantsCacheContextTest.php @@ -171,7 +171,7 @@ class NodeAccessGrantsCacheContextTest extends NodeTestBase { 3 => 'view.all', ]); - // Uninstall the node_access_test module + // Uninstall the node_access_test module. $this->container->get('module_installer')->uninstall(['node_access_test']); drupal_static_reset('node_access_view_all_nodes'); $this->assertUserCacheContext([ 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 bc8cfa927e4..f661ae18ebb 100644 --- a/core/modules/node/tests/src/Functional/NodeEditFormTest.php +++ b/core/modules/node/tests/src/Functional/NodeEditFormTest.php @@ -251,10 +251,25 @@ 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>'); + } + + /** + * Tests the node form when the author is NULL. + */ + public function testNodeFormNullAuthor(): void { + \Drupal::service('module_installer')->install(['node_no_default_author']); + $this->drupalLogin($this->adminUser); + $this->drupalGet('node/add/page'); + $this->assertSession()->statusCodeEquals(200); } /** 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..42c4df88d3a 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). @@ -481,7 +479,7 @@ class NodeTranslationUITest extends ContentTranslationUITestBase { ->getStorage($this->entityTypeId); $entity = $storage->load($this->entityId); $languages = $this->container->get('language_manager')->getLanguages(); - $type_name = node_get_type_label($entity); + $type_name = $entity->getBundleEntity()->label(); foreach ($this->langcodes as $langcode) { // We only want to test the title for non-english translations. @@ -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/Functional/NodeTypeTest.php b/core/modules/node/tests/src/Functional/NodeTypeTest.php index 4169d88f492..d26e4cef58d 100644 --- a/core/modules/node/tests/src/Functional/NodeTypeTest.php +++ b/core/modules/node/tests/src/Functional/NodeTypeTest.php @@ -147,7 +147,7 @@ class NodeTypeTest extends NodeTestBase { $assert->pageTextContains('Foo'); $assert->pageTextContains('Body'); - // Change the name through the API + // Change the name through the API. /** @var \Drupal\node\NodeTypeInterface $node_type */ $node_type = NodeType::load('page'); $node_type->set('name', 'NewBar'); @@ -277,7 +277,7 @@ class NodeTypeTest extends NodeTestBase { $this->drupalGet('admin/structure/types/manage/page/delete'); $this->submitForm([], 'Delete'); - // Navigate to content type administration screen + // Navigate to content type administration screen. $this->drupalGet('admin/structure/types'); $this->assertSession()->pageTextContains("No content types available. Add content type."); $this->assertSession()->linkExists("Add content type"); diff --git a/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php b/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php index e7f060fe3d4..59c33a921e7 100644 --- a/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php +++ b/core/modules/node/tests/src/Functional/Views/NodeFieldTokensTest.php @@ -53,16 +53,16 @@ class NodeFieldTokensTest extends NodeTestBase { $this->drupalGet('test_node_tokens'); - // Body: {{ body }}<br /> + // Body: "{{ body }}<br />". $this->assertSession()->responseContains("Body: <p>$body</p>"); - // Raw value: {{ body__value }}<br /> + // Raw value: "{{ body__value }}<br />". $this->assertSession()->responseContains("Raw value: $body"); - // Raw summary: {{ body__summary }}<br /> + // Raw summary: "{{ body__summary }}<br />". $this->assertSession()->responseContains("Raw summary: $summary"); - // Raw format: {{ body__format }}<br /> + // Raw format: "{{ body__format }}<br />". $this->assertSession()->responseContains("Raw format: plain_text"); } 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/NodeFieldAccessTest.php b/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php index cfad6986f1e..b69ca50f1be 100644 --- a/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php +++ b/core/modules/node/tests/src/Kernel/NodeFieldAccessTest.php @@ -82,7 +82,7 @@ class NodeFieldAccessTest extends EntityKernelTestBase { // An unprivileged user. $page_unrelated_user = $this->createUser(['access content']); - // List of all users + // List of all users. $test_users = [ $content_admin_user, $page_creator_user, diff --git a/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php b/core/modules/node/tests/src/Kernel/NodeRequirementsStatusFilterWarningTest.php index 6ddb8c8c987..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']); } /** @@ -197,8 +198,7 @@ class NodeRequirementsStatusFilterWarningTest extends KernelTestBase { * The requirements raised by the Node module. */ private function getRequirements(): array { - $this->container->get('module_handler')->loadInclude('node', 'install'); - return node_requirements('runtime'); + return $this->container->get('module_handler')->invoke('node', 'runtime_requirements'); } /** diff --git a/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php b/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php deleted file mode 100644 index 1baf081ed00..00000000000 --- a/core/modules/node/tests/src/Unit/PageCache/DenyNodePreviewTest.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\Tests\node\Unit\PageCache; - -use Drupal\Core\PageCache\ResponsePolicyInterface; -use Drupal\node\PageCache\DenyNodePreview; -use Drupal\Tests\UnitTestCase; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * @coversDefaultClass \Drupal\node\PageCache\DenyNodePreview - * @group node - */ -class DenyNodePreviewTest extends UnitTestCase { - - /** - * The response policy under test. - * - * @var \Drupal\node\PageCache\DenyNodePreview - */ - protected $policy; - - /** - * A request object. - * - * @var \Symfony\Component\HttpFoundation\Request - */ - protected $request; - - /** - * A response object. - * - * @var \Symfony\Component\HttpFoundation\Response - */ - protected $response; - - /** - * The current route match. - * - * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit\Framework\MockObject\MockObject - */ - protected $routeMatch; - - /** - * {@inheritdoc} - */ - protected function setUp(): void { - parent::setUp(); - - $this->routeMatch = $this->createMock('Drupal\Core\Routing\RouteMatchInterface'); - $this->policy = new DenyNodePreview($this->routeMatch); - $this->response = new Response(); - $this->request = new Request(); - } - - /** - * Asserts that caching is denied on the node preview route. - * - * @dataProvider providerPrivateImageStyleDownloadPolicy - * @covers ::check - */ - public function testPrivateImageStyleDownloadPolicy($expected_result, $route_name): void { - $this->routeMatch->expects($this->once()) - ->method('getRouteName') - ->willReturn($route_name); - - $actual_result = $this->policy->check($this->response, $this->request); - $this->assertSame($expected_result, $actual_result); - } - - /** - * Provides data and expected results for the test method. - * - * @return array - * Data and expected results. - */ - public static function providerPrivateImageStyleDownloadPolicy() { - return [ - [ResponsePolicyInterface::DENY, 'entity.node.preview'], - [NULL, 'some.other.route'], - [NULL, NULL], - [NULL, FALSE], - [NULL, TRUE], - [NULL, new \stdClass()], - [NULL, [1, 2, 3]], - ]; - } - -} |