first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<?php
/**
* @file
* Empties the description of the `article` content type.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$data = $connection->select('config')
->condition('name', 'node.type.article')
->fields('config', ['data'])
->execute()
->fetchField();
$data = unserialize($data);
$data['description'] = "\n";
$connection->update('config')
->condition('name', 'node.type.article')
->fields([
'data' => serialize($data),
])
->execute();

View File

@@ -0,0 +1,10 @@
name: 'Node module access tests'
type: module
description: 'Support module for node permission testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,164 @@
<?php
/**
* @file
* Test module for testing the node access system.
*
* This module's functionality depends on the following state variables:
* - node_access_test.no_access_uid: Used in NodeQueryAlterTest to enable the
* node_access_all grant realm.
* - node_access_test.private: When TRUE, the module controls access for nodes
* with a 'private' property set, and inherits the default core access for
* nodes without this flag. When FALSE, the module controls access for all
* nodes.
* - node_access_test_secret_catalan: When set to TRUE and using the Catalan
* 'ca' language code, makes all Catalan content secret.
*
* @see node_access_test_node_grants()
* @see \Drupal\node\Tests\NodeQueryAlterTest
* @see \Drupal\node\Tests\NodeAccessBaseTableTest
*/
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\NodeTypeInterface;
use Drupal\node\NodeInterface;
/**
* Implements hook_node_grants().
*
* Provides three grant realms:
* - node_access_test_author: Grants users view, update, and delete privileges
* on nodes they have authored. Users receive a group ID matching their user
* ID on this realm.
* - node_access_test: Grants users view privileges when they have the
* 'node test view' permission. Users with this permission receive two group
* IDs for the realm, 8888 and 8889. Access for both realms is identical;
* the second group is added so that the interaction of multiple groups on
* a given grant realm can be tested in NodeAccessPagerTest.
* - node_access_all: Provides grants for the user whose user ID matches the
* 'node_access_test.no_access_uid' state variable. Access control on this
* realm is not provided in this module; instead,
* NodeQueryAlterTest::testNodeQueryAlterOverride() manually writes a node
* access record defining the access control for this realm.
*
* @see \Drupal\node\Tests\NodeQueryAlterTest::testNodeQueryAlterOverride()
* @see \Drupal\node\Tests\NodeAccessPagerTest
* @see node_access_test.permissions.yml
* @see node_access_test_node_access_records()
*/
function node_access_test_node_grants($account, $operation) {
$grants = [];
$grants['node_access_test_author'] = [$account->id()];
if ($operation == 'view' && $account->hasPermission('node test view')) {
$grants['node_access_test'] = [8888, 8889];
}
$no_access_uid = \Drupal::state()->get('node_access_test.no_access_uid', 0);
if ($operation == 'view' && $account->id() == $no_access_uid) {
$grants['node_access_all'] = [0];
}
return $grants;
}
/**
* Implements hook_node_access_records().
*
* By default, records are written for all nodes. When the
* 'node_access_test.private' state variable is set to TRUE, records
* are only written for nodes with a "private" property set, which causes the
* Node module to write the default global view grant for nodes that are not
* marked private.
*
* @see \Drupal\node\Tests\NodeAccessBaseTableTest::setUp()
* @see node_access_test_node_grants()
* @see node_access_test.permissions.yml
*/
function node_access_test_node_access_records(NodeInterface $node) {
$grants = [];
// For NodeAccessBaseTableTestCase, only set records for private nodes.
if (!\Drupal::state()->get('node_access_test.private') || (isset($node->private) && $node->private->value)) {
// Groups 8888 and 8889 for the node_access_test realm both receive a view
// grant for all controlled nodes. See node_access_test_node_grants().
$grants[] = [
'realm' => 'node_access_test',
'gid' => 8888,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
$grants[] = [
'realm' => 'node_access_test',
'gid' => 8889,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
// For the author realm, the group ID is equivalent to a user ID, which
// means there are many groups of just 1 user.
$grants[] = [
'realm' => 'node_access_test_author',
'gid' => $node->getOwnerId(),
'grant_view' => 1,
'grant_update' => 1,
'grant_delete' => 1,
'priority' => 0,
];
}
return $grants;
}
/**
* Adds the private field to a node type.
*
* @param \Drupal\node\NodeTypeInterface $type
* A node type entity.
*/
function node_access_test_add_field(NodeTypeInterface $type) {
$field_storage = FieldStorageConfig::create([
'field_name' => 'private',
'entity_type' => 'node',
'type' => 'integer',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'private',
'entity_type' => 'node',
'bundle' => $type->id(),
'label' => 'Private',
]);
$field->save();
// Assign widget settings for the 'default' form mode.
\Drupal::service('entity_display.repository')
->getFormDisplay('node', $type->id())
->setComponent('private', [
'type' => 'number',
])
->save();
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function node_access_test_node_access(NodeInterface $node, $operation, AccountInterface $account) {
$secret_catalan = \Drupal::state()
->get('node_access_test_secret_catalan') ?: 0;
if ($secret_catalan && $node->language()->getId() == 'ca') {
// Make all Catalan content secret.
return AccessResult::forbidden()->setCacheMaxAge(0);
}
// Grant access if a specific user is specified.
if (\Drupal::state()->get('node_access_test.allow_uid') === $account->id()) {
return AccessResult::allowed();
}
// No opinion.
return AccessResult::neutral()->setCacheMaxAge(0);
}

View File

@@ -0,0 +1,2 @@
node test view:
title: 'View content'

View File

@@ -0,0 +1,10 @@
name: 'Node module empty access tests'
type: module
description: 'Support module for node permission testing. Provides empty grants hook implementations.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,22 @@
<?php
/**
* @file
* Empty node access hook implementations.
*/
use Drupal\node\NodeInterface;
/**
* Implements hook_node_grants().
*/
function node_access_test_empty_node_grants($account, $operation) {
return [];
}
/**
* Implements hook_node_access_records().
*/
function node_access_test_empty_node_access_records(NodeInterface $node) {
return [];
}

View File

@@ -0,0 +1,12 @@
name: 'Node module access tests language'
type: module
description: 'Support module for language-aware node access testing.'
package: Testing
# version: VERSION
dependencies:
- drupal:options
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,44 @@
<?php
/**
* @file
* Test module with a language-aware node access implementation.
*
* The module adds a 'private' field to page nodes that allows each translation
* of the node to be marked as private (viewable only by administrators).
*/
use Drupal\node\NodeInterface;
/**
* Implements hook_node_grants().
*
* This module defines a single grant realm. All users belong to this group.
*/
function node_access_test_language_node_grants($account, $operation) {
$grants['node_access_language_test'] = [7888];
return $grants;
}
/**
* Implements hook_node_access_records().
*/
function node_access_test_language_node_access_records(NodeInterface $node) {
$grants = [];
// Create grants for each translation of the node.
foreach ($node->getTranslationLanguages() as $langcode => $language) {
// If the translation is not marked as private, grant access.
$translation = $node->getTranslation($langcode);
$grants[] = [
'realm' => 'node_access_language_test',
'gid' => 7888,
'grant_view' => empty($translation->field_private->value) ? 1 : 0,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
'langcode' => $langcode,
];
}
return $grants;
}

View File

@@ -0,0 +1,9 @@
name: 'Node block test'
type: module
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,39 @@
<?php
namespace Drupal\node_block_test\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'Node Context Test' block.
*/
#[Block(
id: "node_block_test_context",
admin_label: new TranslatableMarkup("Node Context Test"),
context_definitions: [
'node' => new EntityContextDefinition('entity:node', new TranslatableMarkup("Node")),
]
)]
class NodeContextTestBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
/** @var \Drupal\node\NodeInterface $node */
$node = $this->getContextValue('node');
return [
'#type' => 'inline_template',
'#template' => 'Displaying node #{{ id }}, revision #{{ revision_id }}: {{ title }}',
'#context' => [
'id' => $node->id(),
'revision_id' => $node->getRevisionId(),
'title' => $node->label(),
],
];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Node module tests'
type: module
description: 'Support module for node related testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,194 @@
<?php
/**
* @file
* A dummy module for testing node related hooks.
*
* This is a dummy module that implements node related hooks to test API
* interaction with the Node module.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Implements hook_ENTITY_TYPE_view() for node entities.
*/
function node_test_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, $view_mode) {
if ($node->isNew()) {
return;
}
if ($view_mode == 'rss') {
// Add RSS elements and namespaces when building the RSS feed.
$node->rss_elements[] = [
'key' => 'testElement',
'value' => t('Value of testElement RSS element for node @nid.', ['@nid' => $node->id()]),
];
// Add content that should be displayed only in the RSS feed.
$build['extra_feed_content'] = [
'#markup' => '<p>' . t('Extra data that should appear only in the RSS feed for node @nid.', ['@nid' => $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>' . t('Extra data that should appear everywhere except the RSS feed for node @nid.', ['@nid' => $node->id()]) . '</p>',
];
}
}
/**
* Implements hook_ENTITY_TYPE_build_defaults_alter() for node entities.
*/
function node_test_node_build_defaults_alter(array &$build, NodeInterface &$node, $view_mode = 'full') {
if ($view_mode == 'rss') {
$node->rss_namespaces['xmlns:test'] = 'http://example.com/test-namespace';
}
}
/**
* Implements hook_node_grants().
*/
function node_test_node_grants(AccountInterface $account, $operation) {
// Give everyone full grants so we don't break other node tests.
// Our node access tests asserts three realms of access.
// See testGrantAlter().
return [
'test_article_realm' => [1],
'test_page_realm' => [1],
'test_alter_realm' => [2],
];
}
/**
* Implements hook_node_access_records().
*/
function node_test_node_access_records(NodeInterface $node) {
// Return nothing when testing for empty responses.
if (!empty($node->disable_node_access)) {
return;
}
$grants = [];
if ($node->getType() == 'article') {
// Create grant in arbitrary article_realm for article nodes.
$grants[] = [
'realm' => 'test_article_realm',
'gid' => 1,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
elseif ($node->getType() == 'page') {
// Create grant in arbitrary page_realm for page nodes.
$grants[] = [
'realm' => 'test_page_realm',
'gid' => 1,
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
'priority' => 0,
];
}
return $grants;
}
/**
* Implements hook_node_access_records_alter().
*/
function node_test_node_access_records_alter(&$grants, NodeInterface $node) {
if (!empty($grants)) {
foreach ($grants as $key => $grant) {
// Alter grant from test_page_realm to test_alter_realm and modify the gid.
if ($grant['realm'] == 'test_page_realm' && $node->isPromoted()) {
$grants[$key]['realm'] = 'test_alter_realm';
$grants[$key]['gid'] = 2;
}
}
}
}
/**
* Implements hook_node_grants_alter().
*/
function node_test_node_grants_alter(&$grants, AccountInterface $account, $operation) {
// Return an empty array of grants to prove that we can alter by reference.
$grants = [];
}
/**
* Implements hook_ENTITY_TYPE_presave() for node entities.
*/
function node_test_node_presave(NodeInterface $node) {
if ($node->getTitle() == 'testing_node_presave') {
// Sun, 19 Nov 1978 05:00:00 GMT
$node->setCreatedTime(280299600);
// Drupal 1.0 release.
$node->changed = 979534800;
}
// Determine changes.
if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
if ($node->original->getTitle() != $node->getTitle()) {
$node->title->value .= '_presave';
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for node entities.
*/
function node_test_node_update(NodeInterface $node) {
// Determine changes on update.
if (!empty($node->original) && $node->original->getTitle() == 'test_changes') {
if ($node->original->getTitle() != $node->getTitle()) {
$node->title->value .= '_update';
}
}
}
/**
* Implements hook_entity_view_mode_alter().
*/
function node_test_entity_view_mode_alter(&$view_mode, EntityInterface $entity) {
// Only alter the view mode if we are on the test callback.
$change_view_mode = \Drupal::state()->get('node_test_change_view_mode', '');
if ($change_view_mode) {
$view_mode = $change_view_mode;
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for node entities.
*
* This tests saving a node on node insert.
*
* @see \Drupal\node\Tests\NodeSaveTest::testNodeSaveOnInsert()
*/
function node_test_node_insert(NodeInterface $node) {
// Set the node title to the node ID and save.
if ($node->getTitle() == 'new') {
$node->setTitle('Node ' . $node->id());
$node->setNewRevision(FALSE);
$node->save();
}
}
/**
* Implements hook_form_alter().
*/
function node_test_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if (!$form_state->get('node_test_form_alter')) {
\Drupal::messenger()->addStatus('Storage is not set');
$form_state->set('node_test_form_alter', TRUE);
}
else {
\Drupal::messenger()->addStatus('Storage is set');
}
}

View File

@@ -0,0 +1,10 @@
name: 'Node configuration tests'
type: module
description: 'Support module for node configuration tests.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,9 @@
type: import
name: Import
description: 'Import description.'
help: null
new_revision: false
display_submitted: true
preview_mode: 1
status: true
langcode: en

View File

@@ -0,0 +1,10 @@
name: 'Node module exception tests'
type: module
description: 'Support module for node related exception testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,17 @@
<?php
/**
* @file
* A module implementing node related hooks to test API interaction.
*/
use Drupal\node\NodeInterface;
/**
* Implements hook_ENTITY_TYPE_insert() for node entities.
*/
function node_test_exception_node_insert(NodeInterface $node) {
if ($node->getTitle() == 'testing_transaction_exception') {
throw new Exception('Test exception for rollback.');
}
}

View File

@@ -0,0 +1,14 @@
name: 'Node test views'
type: module
description: 'Provides default views for views node tests.'
package: Testing
# version: VERSION
dependencies:
- drupal:node
- drupal:views
- drupal:language
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
<?php
/**
* @file
* Provides views data and hooks for node_test_views module.
*/
/**
* Implements hook_views_data_alter().
*/
function node_test_views_views_data_alter(array &$data) {
// Make node language use the basic field handler if requested.
if (\Drupal::state()->get('node_test_views.use_basic_handler')) {
$data['node_field_data']['langcode']['field']['id'] = 'language';
}
}

View File

@@ -0,0 +1,104 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_argument_node_uid_revision
label: test_argument_node_uid_revision
module: views
description: ''
tag: default
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
cache:
type: tag
exposed_form:
type: basic
fields:
nid:
id: nid
table: node_field_data
field: nid
plugin_id: field
entity_type: node
entity_field: nid
filter_groups:
groups:
1: AND
operator: AND
filters: { }
sorts:
nid:
id: nid
table: node_field_data
field: nid
order: ASC
plugin_id: standard
relationship: none
entity_type: node
entity_field: nid
pager:
type: full
query:
type: views_query
style:
type: default
row:
type: fields
display_extenders: { }
arguments:
uid_revision:
id: uid_revision
table: node_field_data
field: uid_revision
relationship: none
group_type: group
admin_label: ''
default_action: empty
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
entity_type: node
plugin_id: node_uid_revision
display_plugin: default
display_title: Default
id: default
position: 0
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,98 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_contextual_links
label: 'Contextual links'
module: node
description: ''
tag: default
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
row:
type: 'entity:node'
options:
view_mode: teaser
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
title: ''
header: { }
footer: { }
relationships: { }
fields: { }
arguments: { }
display_plugin: default
display_title: Default
id: default
position: 0
page_1:
display_options:
path: node/%/contextual-links
defaults:
arguments: false
arguments:
nid:
field: nid
id: nid
relationship: none
table: node_field_data
plugin_id: numeric
entity_type: node
entity_field: nid
menu:
type: tab
title: 'Test contextual link'
description: ''
menu_name: tools
weight: 0
context: '1'
display_plugin: page
display_title: Page
id: page_1
position: 1

View File

@@ -0,0 +1,375 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_field_filters
label: 'Test field filters'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: 0
offset: 0
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
entity_type: node
entity_field: title
plugin_id: field
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
plugin_id: boolean
entity_type: node
entity_field: status
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
operator: contains
value: Paris
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
entity_type: node
entity_field: title
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
plugin_id: date
entity_type: node
entity_field: created
title: 'Test field filters'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
rendering_language: '***LANGUAGE_entity_translation***'
page_bf:
display_plugin: page
id: page_bf
display_title: 'Body filter page'
position: 1
display_options:
path: test-body-filter
display_description: ''
title: 'Test body filters'
defaults:
title: false
filters: false
filter_groups: false
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
plugin_id: boolean
entity_type: node
entity_field: status
body_value:
id: body_value
table: node__body
field: body_value
relationship: none
group_type: group
admin_label: ''
operator: contains
value: Comida
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
entity_type: node
entity_field: body
filter_groups:
operator: AND
groups:
1: AND
page_bfp:
display_plugin: page
id: page_bfp
display_title: 'Body filter page Paris'
position: 1
display_options:
path: test-body-paris
display_description: ''
title: 'Test body filters'
defaults:
title: false
filters: false
filter_groups: false
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
entity_type: node
entity_field: status
plugin_id: boolean
body_value:
id: body_value
table: node__body
field: body_value
relationship: none
group_type: group
admin_label: ''
operator: contains
value: Paris
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
entity_type: node
entity_field: body
filter_groups:
operator: AND
groups:
1: AND
page_tf:
display_plugin: page
id: page_tf
display_title: 'Title filter page'
position: 1
display_options:
path: test-title-filter
display_description: ''
title: 'Test title filter'
defaults:
title: false
filters: false
filter_groups: false
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
entity_type: node
entity_field: status
plugin_id: boolean
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
operator: contains
value: Comida
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
entity_type: node
entity_field: title
filter_groups:
operator: AND
groups:
1: AND
page_tfp:
display_plugin: page
id: page_tfp
display_title: 'Title filter page Paris'
position: 1
display_options:
path: test-title-paris
display_description: ''
title: 'Test title filter'
defaults:
title: false

View File

@@ -0,0 +1,202 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_filter_node_access
label: test_filter_node_access
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: true
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 10
offset: 0
style:
type: default
row:
type: fields
options:
default_field_elements: true
inline: { }
separator: ''
hide_empty: false
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
nid:
id: nid
table: node_access
field: nid
relationship: none
group_type: group
admin_label: ''
operator: '='
value: ''
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: node_access
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: test_filter_node_access
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test_filter_node_access
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,70 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_filter_node_uid_revision
label: test_filter_node_uid_revision
module: views
description: ''
tag: default
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
cache:
type: tag
exposed_form:
type: basic
fields:
nid:
id: nid
table: node_field_data
field: nid
plugin_id: field
entity_type: node
entity_field: nid
filter_groups:
groups:
1: AND
operator: AND
filters:
uid_revision:
admin_label: ''
field: uid_revision
id: uid_revision
is_grouped: false
operator: in
relationship: none
table: node_field_data
value:
- '1'
plugin_id: node_uid_revision
entity_type: node
entity_field: uid_revision
sorts:
nid:
id: nid
table: node_field_data
field: nid
order: ASC
plugin_id: standard
relationship: none
entity_type: node
entity_field: nid
pager:
type: full
query:
type: views_query
style:
type: default
row:
type: fields
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,290 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_language
label: 'Test language'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: 0
offset: 0
style:
type: default
row:
type: fields
options:
default_field_elements: true
inline: { }
separator: ''
hide_empty: false
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: title
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
label: Language
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: langcode
settings:
native_language: false
type: language
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
plugin_id: boolean
entity_type: node
entity_field: status
type:
id: type
table: node_field_data
field: type
value:
page: page
plugin_id: bundle
entity_type: node
entity_field: type
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
operator: in
value:
fr: fr
es: es
und: und
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: language
entity_type: node
entity_field: langcode
sorts:
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
plugin_id: standard
entity_type: node
entity_field: langcode
title: 'Language filter test'
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: false
title: ''
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
plugin_id: language
entity_type: node
entity_field: langcode
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
path: test-language

View File

@@ -0,0 +1,190 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_nid_argument
label: test_nid_argument
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: null
offset: 0
style:
type: default
row:
type: fields
fields:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: nid
plugin_id: field
filters: { }
sorts: { }
title: test_nid_argument
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: true
title: '{{ arguments.nid }}'
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
entity_type: node
entity_field: nid
plugin_id: node_nid
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-nid-argument
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
cacheable: false

View File

@@ -0,0 +1,337 @@
langcode: en
status: true
dependencies:
config:
- node.type.page
module:
- node
- user
id: test_node_access_join
label: 'Test Node Access Join'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 1000
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: table
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
title_1:
id: title_1
table: node_field_data
field: title
relationship: related_article
group_type: group
admin_label: ''
label: Article
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: title
plugin_id: field
title_2:
id: title_2
table: node_field_data
field: title
relationship: related_article_2
group_type: group
admin_label: ''
label: 'Article 2'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: title
plugin_id: field
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
type:
id: type
table: node_field_data
field: type
value:
page: page
entity_type: node
entity_field: type
plugin_id: bundle
sorts:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: title
plugin_id: standard
title: 'Test Reference Access'
header: { }
footer: { }
empty: { }
relationships:
related_article:
id: related_article
table: node__related_article
field: related_article
relationship: none
group_type: group
admin_label: 'Page related article'
required: false
plugin_id: standard
related_article_2:
id: related_article_2
table: node__related_article
field: related_article
relationship: related_article
group_type: group
admin_label: 'Article related article'
required: false
plugin_id: standard
arguments: { }
display_extenders: { }
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-node-access-join
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,207 @@
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.node.teaser
- node.type.article
module:
- node
- user
id: test_node_article_feed
label: 'Test Node Article Feed'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
operator_limit_selection: false
operator_list: { }
group: 1
type:
id: type
table: node_field_data
field: type
value:
article: article
entity_type: node
entity_field: type
plugin_id: bundle
expose:
operator_limit_selection: false
operator_list: { }
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
feed_1:
display_plugin: feed
id: feed_1
display_title: 'Test Feed'
position: 1
display_options:
display_extenders: { }
row:
type: node_rss
options:
relationship: none
view_mode: teaser
path: test-node-article-feed
display_description: ''
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,68 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_bulk_form
label: test_node_bulk_form
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
style:
type: table
row:
type: fields
fields:
node_bulk_form:
id: node_bulk_form
table: node
field: node_bulk_form
plugin_id: node_bulk_form
entity_type: node
title:
id: title
table: node_field_data
field: title
plugin_id: field
entity_type: node
entity_field: title
sorts:
nid:
id: nid
table: node_field_data
field: nid
order: ASC
plugin_id: standard
entity_type: node
entity_field: nid
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: langcode
plugin_id: standard
display_extenders: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: null
display_options:
path: test-node-bulk-form

View File

@@ -0,0 +1,182 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_path_plugin
label: test_node_path_plugin
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: fields
fields:
path:
id: path
table: node
field: view_node
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: true
text: 'This is <strong>not escaped</strong> and this is <a href="{{ path }}" hreflang="en">the link</a>.'
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
output_url_as_text: true
absolute: false
entity_type: node
plugin_id: entity_link
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: test_node_path_plugin
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-node-path-plugin
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,196 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_revision_id_argument
label: test_node_revision_id_argument
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
id: default
display_title: Default
display_plugin: default
position: 0
display_options:
title: test_node_revision_id_argument
fields:
title:
id: title
table: node_field_revision
field: title
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: title
plugin_id: field
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
pager:
type: none
options:
offset: 0
items_per_page: null
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
empty: { }
sorts: { }
arguments:
vid:
id: vid
table: node_field_revision
field: vid
relationship: none
group_type: group
admin_label: ''
entity_type: node
entity_field: vid
plugin_id: node_vid
default_action: ignore
exception:
value: all
title_enable: false
title: All
title_enable: true
title: '{{ arguments.vid }}'
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
override: false
items_per_page: 25
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
break_phrase: false
not: false
filters: { }
filter_groups:
operator: AND
groups: { }
style:
type: default
row:
type: fields
query:
type: views_query
options:
query_comment: ''
disable_sql_rewrite: false
distinct: false
replica: false
query_tags: { }
relationships: { }
header: { }
footer: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags: { }
cacheable: false
page_1:
id: page_1
display_title: Page
display_plugin: page
position: 1
display_options:
display_extenders: { }
path: test-revision-vid-argument
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags: { }
cacheable: false

View File

@@ -0,0 +1,223 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_links
label: test_node_revision_links
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: vid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: none
options: { }
cache:
type: none
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
items_per_page: 0
offset: 0
style:
type: default
row:
type: fields
fields:
link_to_revision:
id: link_to_revision
table: node_field_revision
field: link_to_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to revision'
entity_type: node
plugin_id: node_revision_link
delete_revision:
id: delete_revision
table: node_field_revision
field: delete_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to delete revision'
entity_type: node
plugin_id: node_revision_link_delete
revert_revision:
id: revert_revision
table: node_field_revision
field: revert_revision
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
text: 'Link to revert revision'
entity_type: node
plugin_id: node_revision_link_revert
filters: { }
sorts: { }
title: test_node_revision_links
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-node-revision-links

View File

@@ -0,0 +1,88 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_nid
label: test_node_revision_nid
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: nid
display:
default:
display_options:
relationships:
nid:
id: nid
table: node_field_revision
field: nid
required: true
plugin_id: field
fields:
vid:
id: vid
table: node_field_revision
field: vid
plugin_id: field
entity_type: node
entity_field: vid
nid_1:
id: nid_1
table: node_field_revision
field: nid
plugin_id: field
entity_type: node
entity_field: nid
nid:
id: nid
table: node_field_data
field: nid
relationship: nid
plugin_id: field
entity_type: node
entity_field: nid
langcode:
id: langcode
table: node_field_revision
field: langcode
plugin_id: field
entity_type: node
entity_field: langcode
arguments:
nid:
id: nid
table: node_field_revision
field: nid
plugin_id: node_nid
entity_type: node
entity_field: nid
sorts:
vid:
id: vid
table: node_field_revision
field: vid
order: ASC
plugin_id: field
entity_type: node
entity_field: vid
langcode:
id: langcode
table: node_field_revision
field: langcode
relationship: none
group_type: group
admin_label: ''
order: DESC
exposed: false
expose:
label: ''
entity_type: node
entity_field: langcode
plugin_id: standard
display_extenders: { }
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,43 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_timestamp
label: test_node_revision_timestamp
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: vid
display:
default:
display_options:
fields:
vid:
id: vid
table: node_field_revision
field: vid
plugin_id: field
entity_type: node
entity_field: vid
revision_timestamp:
id: revision_timestamp
table: node_revision
field: revision_timestamp
plugin_id: field
entity_type: node
entity_field: revision_timestamp
sorts:
revision_timestamp:
id: revision_timestamp
table: node_revision
field: revision_timestamp
order: DESC
plugin_id: field
entity_type: node
entity_field: revision_timestamp
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,397 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_revision_uid
label: 'Test node revision uid'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
nid:
id: nid
table: node_field_revision
field: nid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: nid
plugin_id: field
vid:
id: vid
table: node_field_revision
field: vid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: vid
plugin_id: field
uid:
id: uid
table: node_field_data
field: uid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: false
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: uid
plugin_id: field
revision_uid:
id: revision_uid
table: node_revision
field: revision_uid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: false
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: revision_uid
plugin_id: field
filters:
revision_uid:
id: revision_uid
table: node_revision
field: revision_uid
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: revision_uid_op
label: 'Revision user'
description: ''
use_operator: false
operator: revision_uid_op
operator_limit_selection: false
operator_list: { }
identifier: revision_uid
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
entity_field: revision_uid
plugin_id: user_name
sorts:
vid:
id: vid
table: node_field_revision
field: vid
order: ASC
plugin_id: field
entity_type: node
entity_field: vid
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,80 @@
langcode: en
status: true
dependencies:
module:
- node
id: test_node_revision_vid
label: test_node_revision_vid
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: nid
display:
default:
display_options:
relationships:
vid:
id: vid
table: node_field_revision
field: vid
required: true
plugin_id: standard
fields:
vid:
id: vid
table: node_field_revision
field: vid
plugin_id: field
entity_type: node
entity_field: vid
nid_1:
id: nid_1
table: node_field_revision
field: nid
plugin_id: field
entity_type: node
entity_field: nid
nid:
id: nid
table: node_field_data
field: nid
relationship: vid
plugin_id: field
entity_type: node
entity_field: nid
langcode:
id: langcode
table: node_field_revision
field: langcode
entity_type: node
entity_field: langcode
plugin_id: field
arguments:
nid:
id: nid
table: node_field_revision
field: nid
plugin_id: node_nid
entity_type: node
entity_field: nid
display_extenders: { }
sorts:
langcode:
id: langcode
table: node_field_revision
field: langcode
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: langcode
plugin_id: standard
display_plugin: default
display_title: Default
id: default
position: 0

View File

@@ -0,0 +1,59 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_row_plugin
label: test_node_row_plugin
module: views
description: ''
tag: default
base_table: node_field_data
base_field: nid
display:
default:
display_options:
access:
type: perm
cache:
type: tag
exposed_form:
type: basic
filters:
status:
expose:
operator: ''
field: status
group: 1
id: status
table: node_field_data
value: '1'
plugin_id: boolean
entity_type: node
entity_field: status
pager:
options:
items_per_page: 10
type: full
query:
type: views_query
row:
options:
view_mode: teaser
type: 'entity:node'
sorts: { }
style:
type: default
title: test_node_row_plugin
display_plugin: default
display_title: Default
id: default
position: 0
page_1:
display_options:
path: test-node-row-plugin
display_plugin: page
display_title: Page
id: page_1
position: 0

View File

@@ -0,0 +1,193 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
module:
- node
- text
- user
id: test_node_tokens
label: test_node_tokens
module: views
description: 'Verifies tokens provided by the Node module are replaced correctly.'
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
body:
id: body
table: node__body
field: body
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: true
text: "Body: {{ body }}<br />\nRaw value: {{ body__value }}<br />\nRaw summary: {{ body__summary }}<br />\nRaw format: {{ body__format }}"
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: text_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
filters: { }
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test_node_tokens
cache_metadata:
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
cacheable: false

View File

@@ -0,0 +1,203 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_view
label: test_node_view
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
quantity: 9
style:
type: default
row:
type: fields
fields:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
label: Nid
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: nid
filters:
status:
value: '1'
table: node_field_data
field: status
id: status
expose:
operator: ''
group: 1
plugin_id: boolean
entity_type: node
entity_field: status
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
plugin_id: date
entity_type: node
entity_field: created
title: test_node_view
header: { }
footer: { }
empty: { }
relationships: { }
arguments:
type:
id: type
table: node_field_data
field: type
relationship: none
group_type: group
admin_label: ''
default_action: 'not found'
exception:
value: all
title_enable: false
title: All
title_enable: true
title: '{{ arguments.type }}'
default_argument_type: fixed
default_argument_options:
argument: ''
summary_options:
base_path: ''
count: true
items_per_page: 25
override: false
summary:
sort_order: asc
number_of_records: 0
format: default_summary
specify_validation: false
validate:
type: none
fail: 'not found'
validate_options: { }
glossary: false
limit: 0
case: none
path_case: none
transform_dash: false
break_phrase: false
plugin_id: node_type
entity_type: node
entity_field: type
page_1:
display_plugin: page
id: page_1
display_title: Page
position: null
display_options:
path: test-node-view

View File

@@ -0,0 +1,180 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_node_views_analyze
label: test_node_views_analyze
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous:
next:
style:
type: default
row:
type: 'entity:node'
options:
view_mode: teaser
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
operator_limit_selection: false
operator_list: { }
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: test_node_views_analyze
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: node/%
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }

View File

@@ -0,0 +1,144 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_status_extra
label: test_status_extra
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: perm
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: field
entity_type: node
entity_field: title
filters:
status_extra:
id: status_extra
table: node_field_data
field: status_extra
relationship: none
group_type: group
admin_label: ''
operator: '='
value: false
group: 1
exposed: false
expose:
operator_id: '0'
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: node_status
entity_type: node
sorts:
nid:
id: nid
table: node_field_data
field: nid
order: ASC
plugin_id: standard
entity_type: node
entity_field: nid
filter_groups:
operator: AND
groups:
1: AND
page_1:
display_options:
path: test_status_extra
display_plugin: page
display_title: Page
id: page_1
position: 0

View File

@@ -0,0 +1,10 @@
name: 'Node module access automatic cacheability bubbling tests'
type: module
description: 'Support module for node permission testing. Provides a route which does a node access query without explicitly specifying the corresponding cache context.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
node_access_test_auto_bubbling:
path: '/node_access_test_auto_bubbling'
defaults:
_controller: '\Drupal\node_access_test_auto_bubbling\Controller\NodeAccessTestAutoBubblingController::latest'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,30 @@
<?php
namespace Drupal\node_access_test_auto_bubbling\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\node\NodeInterface;
/**
* Returns a node ID listing.
*/
class NodeAccessTestAutoBubblingController extends ControllerBase implements ContainerInjectionInterface {
/**
* Lists the three latest published node IDs.
*
* @return array
* A render array.
*/
public function latest() {
$nids = $this->entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(TRUE)
->condition('status', NodeInterface::PUBLISHED)
->sort('created', 'DESC')
->range(0, 3)
->execute();
return ['#markup' => $this->t('The three latest nodes are: @nids.', ['@nids' => implode(', ', $nids)])];
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for node.
*
* @group node
*/
class GenericTest extends GenericModuleTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
// Ensure the generic test base is working as expected.
$this->assertSame('node', $this->getModule());
parent::setUp();
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the persistence of basic options through multiple steps.
*
* @group node
*/
class MultiStepNodeFormBasicOptionsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The field name to create.
*
* @var string
*/
protected $fieldName;
/**
* Tests changing the default values of basic options to ensure they persist.
*/
public function testMultiStepNodeFormBasicOptions(): void {
// Prepare a user to create the node.
$web_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($web_user);
// Create an unlimited cardinality field.
$this->fieldName = $this->randomMachineName();
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'text',
'cardinality' => -1,
])->save();
// Attach an instance of the field to the page content type.
FieldConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'bundle' => 'page',
'label' => $this->randomMachineName() . '_label',
])->save();
\Drupal::service('entity_display.repository')
->getFormDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'text_textfield',
])
->save();
$edit = [
'title[0][value]' => 'a',
'promote[value]' => FALSE,
'sticky[value]' => 1,
"{$this->fieldName}[0][value]" => $this->randomString(32),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Add another item');
$this->assertSession()->checkboxNotChecked('edit-promote-value');
$this->assertSession()->checkboxChecked('edit-sticky-value');
}
}

View File

@@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests behavior of the node access subsystem if the base table is not node.
*
* @group node
*/
class NodeAccessBaseTableTest extends NodeTestBase {
use EntityReferenceFieldCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node_access_test',
'views',
'taxonomy',
'search',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Nodes by user.
*
* @var array
*/
protected $nodesByUser;
/**
* A public tid.
*
* @var \Drupal\Core\Database\StatementInterface
*/
protected $publicTid;
/**
* A private tid.
*
* @var \Drupal\Core\Database\StatementInterface
*/
protected $privateTid;
/**
* A web user.
*/
protected $webUser;
/**
* The nids visible.
*
* @var array
*/
protected $nidsVisible;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create the vocabulary for the tag field.
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'vid' => 'tags',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$vocabulary->save();
$field_name = 'field_' . $vocabulary->id();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'article', $field_name, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type_manager
->getStorage('entity_form_display')
->load('node.article.default')
->setComponent($field_name, [
'type' => 'entity_reference_autocomplete_tags',
'weight' => -4,
])
->save();
node_access_test_add_field(NodeType::load('article'));
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests the "private" node access functionality.
*
* - Create 2 users with "access content" and "create article" permissions.
* - Each user creates one private and one not private article.
*
* - Test that each user can view the other user's non-private article.
* - Test that each user cannot view the other user's private article.
* - Test that each user finds only appropriate (non-private + own private)
* in taxonomy listing.
* - Create another user with 'view any private content'.
* - Test that user 4 can view all content created above.
* - Test that user 4 can view all content on taxonomy listing.
*/
public function testNodeAccessBasic(): void {
$num_simple_users = 2;
$simple_users = [];
// Nodes keyed by uid and nid: $nodes[$uid][$nid] = $is_private;
$this->nodesByUser = [];
// Titles keyed by nid.
$titles = [];
// Array of nids marked private.
$private_nodes = [];
for ($i = 0; $i < $num_simple_users; $i++) {
$simple_users[$i] = $this->drupalCreateUser([
'access content',
'create article content',
]);
}
foreach ($simple_users as $this->webUser) {
$this->drupalLogin($this->webUser);
foreach ([0 => 'Public', 1 => 'Private'] as $is_private => $type) {
$edit = [
'title[0][value]' => "$type Article created by " . $this->webUser->getAccountName(),
];
if ($is_private) {
$edit['private[0][value]'] = TRUE;
$edit['body[0][value]'] = 'private node';
$edit['field_tags[target_id]'] = 'private';
}
else {
$edit['body[0][value]'] = 'public node';
$edit['field_tags[target_id]'] = 'public';
}
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($is_private, (int) $node->private->value, 'The private status of the node was properly set in the node_access_test table.');
if ($is_private) {
$private_nodes[] = $node->id();
}
$titles[$node->id()] = $edit['title[0][value]'];
$this->nodesByUser[$this->webUser->id()][$node->id()] = $is_private;
}
}
$public_tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('name', 'public')
->condition('default_langcode', 1)
->execute();
$this->publicTid = reset($public_tids);
$private_tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('name', 'private')
->condition('default_langcode', 1)
->execute();
$this->privateTid = reset($private_tids);
$this->assertNotEmpty($this->publicTid, 'Public tid was found');
$this->assertNotEmpty($this->privateTid, 'Private tid was found');
foreach ($simple_users as $this->webUser) {
$this->drupalLogin($this->webUser);
// Check own nodes to see that all are readable.
foreach ($this->nodesByUser as $uid => $data) {
foreach ($data as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
if ($is_private) {
$should_be_visible = $uid == $this->webUser->id();
}
else {
$should_be_visible = TRUE;
}
$this->assertSession()->statusCodeEquals($should_be_visible ? 200 : 403);
}
}
// Check to see that the correct nodes are shown on taxonomy/private
// and taxonomy/public.
$this->assertTaxonomyPage(FALSE);
}
// Now test that a user with 'node test view' permissions can view content.
$access_user = $this->drupalCreateUser([
'access content',
'create article content',
'node test view',
'search content',
]);
$this->drupalLogin($access_user);
foreach ($this->nodesByUser as $private_status) {
foreach ($private_status as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
$this->assertSession()->statusCodeEquals(200);
}
}
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
// Rebuild the node access permissions, repeat the test. This is done to
// ensure that node access is rebuilt correctly even if the current user
// does not have the bypass node access permission.
node_access_rebuild();
foreach ($this->nodesByUser as $private_status) {
foreach ($private_status as $nid => $is_private) {
$this->drupalGet('node/' . $nid);
$this->assertSession()->statusCodeEquals(200);
}
}
// This user should be able to see all of the nodes on the relevant
// taxonomy pages.
$this->assertTaxonomyPage(TRUE);
}
/**
* Checks taxonomy/term listings to ensure only accessible nodes are listed.
*
* @param bool $is_admin
* A boolean indicating whether the current user is an administrator. If
* TRUE, all nodes should be listed. If FALSE, only public nodes and the
* user's own private nodes should be listed.
*
* @internal
*/
protected function assertTaxonomyPage(bool $is_admin): void {
foreach ([$this->publicTid, $this->privateTid] as $tid_is_private => $tid) {
$this->drupalGet("taxonomy/term/$tid");
$this->nidsVisible = [];
foreach ($this->xpath("//a[text()='Read more']") as $link) {
// See also testTranslationRendering() in NodeTranslationUITest.
$this->assertEquals(1, preg_match('|node/(\d+)$|', $link->getAttribute('href'), $matches), 'Read more points to a node');
$this->nidsVisible[$matches[1]] = TRUE;
}
foreach ($this->nodesByUser as $uid => $data) {
foreach ($data as $nid => $is_private) {
// Private nodes should be visible on the private term page,
// public nodes should be visible on the public term page.
$should_be_visible = $tid_is_private == $is_private;
// Non-administrators can only see their own nodes on the private
// term page.
if (!$is_admin && $tid_is_private) {
$should_be_visible = $should_be_visible && $uid == $this->webUser->id();
}
$this->assertSame($should_be_visible, isset($this->nidsVisible[$nid]), strtr('A %private node by user %uid is %visible for user %current_uid on the %tid_is_private page.', ['%private' => $is_private ? 'private' : 'public', '%uid' => $uid, '%visible' => isset($this->nidsVisible[$nid]) ? 'visible' : 'not visible', '%current_uid' => $this->webUser->id(), '%tid_is_private' => $tid_is_private ? 'private' : 'public']));
}
}
}
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the node access automatic cacheability bubbling logic.
*
* @group node
* @group Cache
* @group cacheability_safeguards
*/
class NodeAccessCacheabilityTest extends NodeTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node_access_test',
'node_access_test_auto_bubbling',
];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
}
/**
* Tests that the node grants cache context is auto-added, only when needed.
*
* @see node_query_node_access_alter()
*/
public function testNodeAccessCacheabilitySafeguard(): void {
// The node grants cache context should be added automatically.
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertCacheContext('user.node_grants:view');
// The root user has the 'bypass node access' permission, which means the
// node grants cache context is not necessary.
$this->drupalLogin($this->rootUser);
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertNoCacheContext('user.node_grants:view');
$this->drupalLogout();
// Uninstall the module with the only hook_node_grants() implementation.
$this->container->get('module_installer')->uninstall(['node_access_test']);
$this->rebuildContainer();
// Because there are no node grants defined, there also is no need for the
// node grants cache context to be bubbled.
$this->drupalGet(new Url('node_access_test_auto_bubbling'));
$this->assertNoCacheContext('user.node_grants:view');
}
/**
* Tests that the user cache contexts are correctly set.
*/
public function testNodeAccessCacheContext(): void {
// Create a user, with edit/delete own content permission.
$test_user1 = $this->drupalCreateUser([
'access content',
'edit own page content',
'delete own page content',
]);
$this->drupalLogin($test_user1);
$node1 = $this->createNode(['type' => 'page']);
// User should be able to edit/delete their own content.
// Therefore after the access check in node_node_access the user cache
// context should be added.
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->assertCacheContext('user');
$this->drupalGet('node/' . $node1->id() . '/delete');
$this->assertCacheContext('user');
// Create a user without edit/delete permission.
$test_user2 = $this->drupalCreateUser([
'access content',
]);
$this->drupalLogin($test_user2);
$node2 = $this->createNode(['type' => 'page']);
// The user shouldn't have access to the node edit/delete pages.
// Therefore after the access check in node_node_access the user permissions
// cache context should be added.
$this->drupalGet('node/' . $node2->id() . '/edit');
$this->assertCacheContext('user.permissions');
$this->drupalGet('node/' . $node2->id() . '/delete');
$this->assertCacheContext('user.permissions');
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests node view access cacheability with node grants.
*
* @group node
*/
class NodeAccessCacheabilityWithNodeGrants extends BrowserTestBase {
use EntityReferenceFieldCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests node view access cacheability with node grants.
*/
public function testAccessCacheabilityWithNodeGrants(): void {
NodeType::create(['type' => 'page', 'name' => 'Page'])->save();
$this->createEntityReferenceField('node', 'page', 'ref', 'Ref', 'node');
EntityViewDisplay::create([
'targetEntityType' => 'node',
'bundle' => 'page',
'mode' => 'default',
'status' => TRUE,
])->setComponent('ref', ['type' => 'entity_reference_label'])
->save();
// Check that at least one module implements hook_node_grants() as this test
// only tests this case.
// @see \node_test_node_grants()
$this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants'));
// Create an unpublished node.
$referenced = $this->createNode(['status' => FALSE]);
// Create a node referencing $referenced.
$node = $this->createNode(['ref' => $referenced]);
// Check that the referenced entity link doesn't show on the host entity.
$this->drupalGet($node->toUrl());
$this->assertSession()->linkNotExists($referenced->label());
// Publish the referenced node.
$referenced->setPublished()->save();
// Check that the referenced entity link shows on the host entity.
$this->getSession()->reload();
$this->assertSession()->linkExists($referenced->label());
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests the interaction of the node access system with fields.
*
* @group node
*/
class NodeAccessFieldTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node_access_test', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A user with permission to manage content types and fields.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAdminUser;
/**
* The name of the created field.
*
* @var string
*/
protected $fieldName;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some users.
$this->adminUser = $this->drupalCreateUser([
'access content',
'bypass node access',
]);
$this->contentAdminUser = $this->drupalCreateUser([
'access content',
'administer content types',
'administer node fields',
]);
// Add a custom field to the page content type.
$this->fieldName = $this->randomMachineName() . '_field_name';
FieldStorageConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'type' => 'text',
])->save();
FieldConfig::create([
'field_name' => $this->fieldName,
'entity_type' => 'node',
'bundle' => 'page',
])->save();
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getViewDisplay('node', 'page')
->setComponent($this->fieldName)
->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent($this->fieldName)
->save();
}
/**
* Tests administering fields when node access is restricted.
*/
public function testNodeAccessAdministerField(): void {
// Create a page node.
$fieldData = [];
$value = $fieldData[0]['value'] = $this->randomMachineName();
$node = $this->drupalCreateNode([$this->fieldName => $fieldData]);
// Log in as the administrator and confirm that the field value is present.
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($value);
// Log in as the content admin and try to view the node.
$this->drupalLogin($this->contentAdminUser);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains('Access denied');
// Modify the field default as the content admin.
$edit = [
'set_default_value' => '1',
];
$default = 'Sometimes words have two meanings';
$edit["default_value_input[{$this->fieldName}][0][value]"] = $default;
$this->drupalGet("admin/structure/types/manage/page/fields/node.page.{$this->fieldName}");
$this->submitForm($edit, 'Save settings');
// Log in as the administrator.
$this->drupalLogin($this->adminUser);
// Confirm that the existing node still has the correct field value.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($value);
// Confirm that the new default value appears when creating a new node.
$this->drupalGet('node/add/page');
$this->assertSession()->responseContains($default);
}
}

View File

@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\user\Entity\User;
/**
* Tests the node access grants cache context service.
*
* @group node
* @group Cache
*/
class NodeAccessGrantsCacheContextTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node_access_test'];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to view content.
*/
protected $accessUser;
/**
* User without permission to view content.
*/
protected $noAccessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User
*/
protected User $noAccessUser2;
/**
* @var array
*/
protected array $userMapping;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
// Create user with simple node access permission. The 'node test view'
// permission is implemented and granted by the node_access_test module.
$this->accessUser = $this->drupalCreateUser([
'access content overview',
'access content',
'node test view',
]);
$this->noAccessUser = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->noAccessUser2 = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->userMapping = [
1 => $this->rootUser,
2 => $this->accessUser,
3 => $this->noAccessUser,
];
}
/**
* Asserts that for each given user, the expected cache context is returned.
*
* @param array $expected
* Expected values, keyed by user ID, expected cache contexts as values.
*
* @internal
*/
protected function assertUserCacheContext(array $expected): void {
foreach ($expected as $uid => $context) {
if ($uid > 0) {
$this->drupalLogin($this->userMapping[$uid]);
}
$this->assertSame($context, $this->container->get('cache_context.user.node_grants')->getContext('view'));
}
$this->drupalLogout();
}
/**
* Tests NodeAccessGrantsCacheContext::getContext().
*/
public function testCacheContext(): void {
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0;node_access_all:0',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all:0;node_access_test_author:3',
]);
// Grant view to all nodes (because nid = 0) for users in the
// 'node_access_all' realm.
$record = [
'nid' => 0,
'gid' => 0,
'realm' => 'node_access_all',
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
];
Database::getConnection()->insert('node_access')->fields($record)->execute();
// Put user accessUser (uid 0) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', 0);
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all:0;node_access_test_author:3',
]);
// Put user accessUser (uid 2) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', $this->accessUser->id());
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0',
1 => 'all',
2 => 'view.all',
3 => 'view.all:0;node_access_test_author:3',
]);
// Put user noAccessUser (uid 3) in the realm.
\Drupal::state()->set('node_access_test.no_access_uid', $this->noAccessUser->id());
drupal_static_reset('node_access_view_all_nodes');
$this->assertUserCacheContext([
0 => 'view.all:0;node_access_test_author:0',
1 => 'all',
2 => 'view.all:0;node_access_test_author:2;node_access_test:8888,8889',
3 => 'view.all',
]);
// 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([
0 => 'view.all',
1 => 'all',
2 => 'view.all',
3 => 'view.all',
]);
}
}

View File

@@ -0,0 +1,356 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\user\UserInterface;
use Drupal\views\Tests\ViewTestData;
/**
* Tests Node Access on join.
*
* @group views
*/
class NodeAccessJoinTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_access_test', 'node_test_views', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The user that will create the articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $authorUser;
/**
* Another user that will create articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $otherUser;
/**
* A user with just access content permissions.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $regularUser;
/**
* A user with access to private articles.
*
* @var \Drupal\user\UserInterface
*/
protected UserInterface $accessUser;
/**
* Articles.
*
* @var array
*/
protected array $articles;
/**
* Views used by this test.
*
* @var array
*/
public static array $testViews = ['test_node_access_join'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_test_add_field(NodeType::load('article'));
$field_storage = FieldStorageConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'translatable' => FALSE,
'entity_types' => [],
'settings' => [
'target_type' => 'node',
],
'type' => 'entity_reference',
]);
$field_storage->save();
$field = FieldConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Related Article',
'settings' => [
'handler' => 'default',
'handler_settings' => [
// Reference a single vocabulary.
'target_bundles' => [
'article',
],
],
],
]);
$field->save();
$entity_display = \Drupal::service('entity_display.repository');
$entity_display->getViewDisplay('node', 'page', 'default')
->setComponent('related_article')
->save();
$entity_display->getFormDisplay('node', 'page', 'default')
->setComponent('related_article', [
'type' => 'entity_reference_autocomplete',
])
->save();
$field = FieldConfig::create([
'field_name' => 'related_article',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Related Article',
'settings' => [
'handler' => 'default',
'handler_settings' => [
// Reference a single vocabulary.
'target_bundles' => [
'article',
],
],
],
]);
$field->save();
$entity_display->getViewDisplay('node', 'article', 'default')
->setComponent('related_article')
->save();
$entity_display->getFormDisplay('node', 'article', 'default')
->setComponent('related_article', [
'type' => 'entity_reference_autocomplete',
])
->save();
node_access_rebuild();
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Tests the accessibility of joined nodes.
*
* - Create two users with "access content" and "create article" permissions
* who can each access their own private articles but not others'.
* - Create article-type nodes with and without references to other articles.
* The articles and references represent all possible combinations of the
* tested access types.
* - Create page-type nodes referencing each of the articles, as well as a
* page with no reference.
* - Use a custom view that creates two joins between nodes and has a
* node_access tag. The view lists the page nodes, the article
* referenced by each page node, and the article referenced by each
* article.
*
* - Login with the author user and check that user does not have access to
* private nodes created by other users. Test access using total row
* count as well as checking for presence of individual page titles.
* - Repeat tests using a user with only the "access content" permission,
* confirming this user does not have access to any private nodes.
* - Repeat tests using a user with "access content" and "node test view"
* permissions, confirming this user sees the complete view.
*/
public function testNodeAccessJoin(): void {
$permissions = ['access content', 'create article content'];
// User to add articles and test author access.
$this->authorUser = $this->drupalCreateUser($permissions);
// Another user to add articles whose private articles can not be accessed
// by authorUser.
$this->otherUser = $this->drupalCreateUser($permissions);
// Create the articles. The articles are stored in an array keyed by
// $article and $reference2, where $article is the access type of the
// article itself, and $reference2 is the access type of the reference
// linked to by the article. 'public' articles are created by otherUser with
// private=0. 'private' articles are created by otherUser with private=1.
// 'author_public' articles are created by authorUser with private=0.
// 'author_private' articles are created by authorUser with private=1.
// 'no_reference' is used for references when there is no related article.
$access_type = ['public', 'private', 'author_public', 'author_private'];
$reference_access_type = array_merge(['no_reference'], $access_type);
foreach ($reference_access_type as $reference2) {
foreach ($access_type as $article) {
$is_author = (str_starts_with($article, 'author'));
$is_private = (str_ends_with($article, 'private'));
$edit = [
'type' => 'article',
'uid' => $is_author ? $this->authorUser->id() : $this->otherUser->id(),
];
$edit['private'][0]['value'] = $is_private;
// The article names provide the access status of the article and the
// access status of the related article, if any. The naming system
// ensures that the text 'Article $article' will only appear in the view
// if an article with that access type is displayed in the view. The
// text '$article' alone will appear in the titles of other nodes that
// reference an article.
$edit['title'] = "Article $article - $reference2";
if ($reference2 !== 'no_reference') {
$edit['related_article'][0]['target_id'] = $this->articles[$reference2]['no_reference'];
}
$node = $this->drupalCreateNode($edit);
$this->articles[$article][$reference2] = $node->id();
$this->assertEquals((int) $is_private, (int) $node->private->value, 'The private status of the article node was properly set in the node_access_test table.' . $node->uid->target_id);
if ($reference2 !== 'no_reference') {
$this->assertEquals((int) $this->articles[$reference2]['no_reference'], (int) $node->related_article->target_id, 'Proper article attached to article.');
}
}
}
// Add a blank 'no_reference' entry to the article list, so that a page with
// no reference gets created.
$this->articles['no_reference']['no_reference'] = NULL;
$total = 0;
$count_s_total = $count_s2_total = 0;
$count_s_public = $count_s2_public = 0;
$count_s_author = $count_s2_author = 0;
$total_public = $total_author = 0;
// Create page nodes referencing each article, as a page without reference.
foreach ($this->articles as $reference => $list) {
foreach ($list as $reference2 => $article_nid) {
$title = "Page - $reference";
if ($reference !== 'no_reference') {
$title .= " - $reference2";
}
$edit = [
'type' => 'page',
'title' => $title,
];
if ($article_nid) {
$edit['related_article'][0]['target_id'] = $article_nid;
}
$node = $this->drupalCreateNode($edit);
if ($article_nid) {
$this->assertEquals((int) $article_nid, (int) $node->related_article->target_id, 'Proper article attached to page.');
}
// Calculate totals expected for each user type.
$total++;
// Total number of primary and secondary references.
if ($reference !== 'no_reference') {
$count_s_total++;
if ($reference2 !== 'no_reference') {
$count_s2_total++;
}
}
// Public users only see 'public' and 'author_public' articles.
if (str_ends_with($reference, 'public')) {
$count_s_public++;
if (str_ends_with($reference2, 'public')) {
$count_s2_public++;
}
}
// authorUser sees 'public','author_public', 'author_private' articles.
if (str_ends_with($reference, 'public') || str_starts_with($reference, 'author')) {
$count_s_author++;
if (str_ends_with($reference2, 'public') || str_starts_with($reference2, 'author')) {
$count_s2_author++;
}
}
// $total_public and $total_author are not currently in use -- but
// represent the totals when joins are handled by adding an is-null
// check (i.e., if inaccessible references caused the entire row to be
// hidden from view, instead of hiding just one cell of the table).
// Count of pages where all related articles are accessible by
// public users.
if (!str_ends_with($reference, 'private') && !str_ends_with($reference2, 'private')) {
$total_public++;
}
// Count of pages where all related articles are accessible by
// authorUser.
if ($reference !== 'private' && $reference2 !== 'private') {
$total_author++;
}
}
}
// Generate a view listing all the pages, and check the view's content for
// users with three different access levels.
ViewTestData::createTestViews(get_class($this), ['node_test_views']);
// Check the author of the 'author' articles.
$this->drupalLogin($this->authorUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Author should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_author, 'Author should see ' . $count_s_author . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_author, 'Author should see ' . $count_s2_author . ' secondary references. Actual: ' . $chk_total);
$session = $this->assertSession();
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Page - author_private - no_reference');
$session->pageTextContains('Article public');
$session->pageTextNotContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextContains('Article author_private');
// Check a regular user who did not author any articles.
$this->regularUser = $this->drupalCreateUser(['access content']);
$this->drupalLogin($this->regularUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Public user should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_public, 'Public user should see ' . $count_s_public . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_public, 'Public user should see ' . $count_s2_public . ' secondary references. Actual: ' . $chk_total);
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Article public');
$session->pageTextNotContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextNotContains('Article author_private');
// Check that a user with 'node test view' permission, can view all pages
// and articles.
$this->accessUser = $this->drupalCreateUser([
'access content',
'node test view',
]);
$this->drupalLogin($this->accessUser);
$this->drupalGet('test-node-access-join');
$chk_total = count($this->xpath("//td[@headers='view-title-table-column']"));
$this->assertEquals($chk_total, $total, 'Full-access user should see ' . $total . ' rows. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-1-table-column']/a"));
$this->assertEquals($chk_total, $count_s_total, 'Full-access user should see ' . $count_s_total . ' primary references. Actual: ' . $chk_total);
$chk_total = count($this->xpath("//td[@headers='view-title-2-table-column']/a"));
$this->assertEquals($chk_total, $count_s2_total, 'Full-access user should see ' . $count_s2_total . ' secondary references. Actual: ' . $chk_total);
$session->pageTextContains('Page - no_reference');
$session->pageTextContains('Page - public - no_reference');
$session->pageTextContains('Page - public - public');
$session->pageTextContains('Page - author_private - no_reference');
$session->pageTextContains('Article public');
$session->pageTextContains('Article private');
$session->pageTextContains('Article author_public');
$session->pageTextContains('Article author_private');
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests that the node_access system stores the proper fallback marker.
*
* @group node
*/
class NodeAccessLanguageFallbackTest extends NodeTestBase {
/**
* Enable language and a non-language-aware node access module.
*
* @var array
*/
protected static $modules = [
'language',
'node_access_test',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// After enabling a node access module, the {node_access} table has to be
// rebuilt.
node_access_rebuild();
// Add Hungarian, Catalan, and Afrikaans.
ConfigurableLanguage::createFromLangcode('hu')->save();
ConfigurableLanguage::createFromLangcode('ca')->save();
ConfigurableLanguage::createFromLangcode('af')->save();
// Enable content translation for the current entity type.
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
}
/**
* Tests node access fallback handling with multiple node languages.
*/
public function testNodeAccessLanguageFallback(): void {
// The node_access_test module allows nodes to be marked private. We need to
// ensure that system honors the fallback system of node access properly.
// Note that node_access_test_language is language-sensitive and does not
// apply to the fallback test.
// Create one node in Hungarian and marked as private.
$node = $this->drupalCreateNode([
'body' => [[]],
'langcode' => 'hu',
'private' => [['value' => 1]],
'status' => 1,
]);
// There should be one entry in node_access, with fallback set to hu.
$this->checkRecords(1, 'hu');
// Create a translation user.
$admin = $this->drupalCreateUser([
'bypass node access',
'administer nodes',
'translate any entity',
'administer content translation',
]);
$this->drupalLogin($admin);
$this->drupalGet('node/' . $node->id() . '/translations');
$this->assertSession()->statusCodeEquals(200);
// Create a Catalan translation through the UI.
$url_options = ['language' => \Drupal::languageManager()->getLanguage('ca')];
$this->drupalGet('node/' . $node->id() . '/translations/add/hu/ca', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
// Check the node access table.
$this->checkRecords(2, 'hu');
// Programmatically create a translation. This process lets us check that
// both forms and code behave in the same way.
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Reload the node.
$node = $storage->load(1);
// Create an Afrikaans translation.
$translation = $node->addTranslation('af');
$translation->title->value = $this->randomString();
$translation->status = 1;
$node->save();
// Check the node access table.
$this->checkRecords(3, 'hu');
// For completeness, edit the Catalan version again.
$this->drupalGet('node/' . $node->id() . '/edit', $url_options);
$this->assertSession()->statusCodeEquals(200);
// Save the form.
$this->getSession()->getPage()->pressButton('Save (this translation)');
$this->assertSession()->statusCodeEquals(200);
// Check the node access table.
$this->checkRecords(3, 'hu');
}
/**
* Queries the node_access table and checks for proper storage.
*
* @param int $count
* The number of rows expected by the query (equal to the translation
* count).
* @param $langcode
* The expected language code set as the fallback property.
*/
public function checkRecords($count, $langcode = 'hu') {
$select = \Drupal::database()
->select('node_access', 'na')
->fields('na', ['nid', 'fallback', 'langcode', 'grant_view'])
->condition('na.realm', 'node_access_test', '=')
->condition('na.gid', 8888, '=');
$records = $select->execute()->fetchAll();
// Check that the expected record count is returned.
$this->assertCount($count, $records);
// The fallback value is 'hu' and should be set to 1. For other languages,
// it should be set to 0. Casting to boolean lets us run that comparison.
foreach ($records as $record) {
$this->assertEquals((bool) $record->fallback, $record->langcode === $langcode);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\user\RoleInterface;
/**
* Tests the interaction of the node access system with menu links.
*
* @group node
*/
class NodeAccessMenuLinkTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['menu_ui', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to manage menu links and create nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $contentAdminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:main');
$this->contentAdminUser = $this->drupalCreateUser([
'access content',
'administer content types',
'bypass node access',
'administer menu',
]);
$this->config('user.role.' . RoleInterface::ANONYMOUS_ID)->set('permissions', [])->save();
}
/**
* SA-CORE-2015-003: Tests menu links to nodes when node access is restricted.
*/
public function testNodeAccessMenuLink(): void {
$menu_link_title = $this->randomString();
$this->drupalLogin($this->contentAdminUser);
$edit = [
'title[0][value]' => $this->randomString(),
'body[0][value]' => $this->randomString(),
'menu[enabled]' => 1,
'menu[title]' => $menu_link_title,
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists($menu_link_title);
// Ensure anonymous users without "access content" permission do not see
// this menu link.
$this->drupalLogout();
$this->drupalGet('');
$this->assertSession()->linkNotExists($menu_link_title);
// Ensure anonymous users with "access content" permission see this menu
// link.
$this->config('user.role.' . RoleInterface::ANONYMOUS_ID)->set('permissions', ['access content'])->save();
$this->drupalGet('');
$this->assertSession()->linkExists($menu_link_title);
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\CommentInterface;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\comment\Entity\Comment;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\User;
/**
* Tests access controlled node views have the right amount of comment pages.
*
* @group node
*/
class NodeAccessPagerTest extends BrowserTestBase {
use CommentTestTrait;
/**
* An user.
*
* @var \Drupal\user\Entity\User
*/
protected User $webUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'node_access_test', 'comment'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->addDefaultCommentField('node', 'page');
$this->webUser = $this->drupalCreateUser([
'access content',
'access comments',
'node test view',
]);
}
/**
* Tests the comment pager for nodes with multiple grants per realm.
*/
public function testCommentPager(): void {
// Create a node.
$node = $this->drupalCreateNode();
// Create 60 comments.
for ($i = 0; $i < 60; $i++) {
$comment = Comment::create([
'entity_id' => $node->id(),
'entity_type' => 'node',
'field_name' => 'comment',
'subject' => $this->randomMachineName(),
'comment_body' => [
['value' => $this->randomMachineName()],
],
'status' => CommentInterface::PUBLISHED,
]);
$comment->save();
}
$this->drupalLogin($this->webUser);
// View the node page. With the default 50 comments per page there should
// be two pages (0, 1) but no third (2) page.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($node->label());
$this->assertSession()->pageTextContains('Comments');
$this->assertSession()->responseContains('page=1');
$this->assertSession()->responseNotContains('page=2');
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\NodeType;
/**
* Tests node access rebuild functions with multiple node access modules.
*
* @group node
*/
class NodeAccessRebuildNodeGrantsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user to create nodes that only it has access to.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* A user to test the rebuild nodes feature which can't access the nodes.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'access site reports',
'administer nodes',
]);
$this->drupalLogin($this->adminUser);
$this->webUser = $this->drupalCreateUser();
}
/**
* Tests rebuilding the node access permissions table with content.
*/
public function testNodeAccessRebuildNodeGrants(): void {
\Drupal::service('module_installer')->install(['node_access_test']);
\Drupal::state()->set('node_access_test.private', TRUE);
node_access_test_add_field(NodeType::load('page'));
$this->resetAll();
// Create 30 nodes so that _node_access_rebuild_batch_operation() has to run
// more than once.
for ($i = 0; $i < 30; $i++) {
$nodes[] = $this->drupalCreateNode([
'uid' => $this->webUser->id(),
'private' => [['value' => 1]],
]);
}
/** @var \Drupal\node\NodeGrantDatabaseStorageInterface $grant_storage */
$grant_storage = \Drupal::service('node.grant_storage');
// Default realm access and node records are present.
foreach ($nodes as $node) {
$this->assertNotEmpty($node->private->value);
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the node author.');
$this->assertTrue($grant_storage->access($node, 'view', $this->adminUser)->isAllowed(), 'Prior to rebuilding node access the grant storage returns allowed for the admin user.');
}
$this->assertEquals(1, \Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is an all realm access record');
$this->assertTrue(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt');
// Rebuild permissions.
$this->drupalGet('admin/reports/status');
$this->clickLink('Rebuild permissions');
$this->submitForm([], 'Rebuild permissions');
$this->assertSession()->pageTextContains('The content access permissions have been rebuilt.');
// Test if the rebuild by user that cannot bypass node access and does not
// have access to the nodes has been successful.
$this->assertFalse($this->adminUser->hasPermission('bypass node access'));
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt');
foreach ($nodes as $node) {
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.');
$this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.');
}
$this->assertEmpty(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record');
// Test an anonymous node access rebuild from code.
$this->drupalLogout();
node_access_rebuild();
foreach ($nodes as $node) {
$this->assertTrue($grant_storage->access($node, 'view', $this->webUser)->isAllowed(), 'After rebuilding node access the grant storage returns allowed for the node author.');
$this->assertFalse($grant_storage->access($node, 'view', $this->adminUser)->isForbidden(), 'After rebuilding node access the grant storage returns forbidden for the admin user.');
}
$this->assertEmpty(\Drupal::service('node.grant_storage')->checkAll($this->webUser), 'There is no all realm access record');
}
/**
* Tests rebuilding the node access permissions table with no content.
*/
public function testNodeAccessRebuildNoAccessModules(): void {
// Default realm access is present.
$this->assertEquals(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record');
// No need to rebuild permissions.
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions need to be rebuilt');
// Rebuild permissions.
$this->drupalGet('admin/reports/status');
$this->clickLink('Rebuild permissions');
$this->submitForm([], 'Rebuild permissions');
$this->assertSession()->pageTextContains('Content permissions have been rebuilt.');
$this->assertNull(\Drupal::state()->get('node.node_access_needs_rebuild'), 'Node access permissions have been rebuilt');
// Default realm access is still present.
$this->assertEquals(1, \Drupal::service('node.grant_storage')->count(), 'There is an all realm access record');
}
}

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\user\RoleInterface;
/**
* Tests node administration page functionality.
*
* @group node
*/
class NodeAdminTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A user with the 'access content overview' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser1;
/**
* A normal user with permission to view own unpublished content.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser2;
/**
* A normal user with permission to bypass node access content.
*
* @var \Drupal\user\UserInterface
*/
protected $baseUser3;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Remove the "view own unpublished content" permission which is set
// by default for authenticated users so we can test this permission
// correctly.
user_role_revoke_permissions(RoleInterface::AUTHENTICATED_ID, ['view own unpublished content']);
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'access content overview',
'administer nodes',
'bypass node access',
]);
$this->baseUser1 = $this->drupalCreateUser(['access content overview']);
$this->baseUser2 = $this->drupalCreateUser([
'access content overview',
'view own unpublished content',
]);
$this->baseUser3 = $this->drupalCreateUser([
'access content overview',
'bypass node access',
]);
}
/**
* Tests that the table sorting works on the content admin pages.
*/
public function testContentAdminSort(): void {
$this->drupalLogin($this->adminUser);
$changed = \Drupal::time()->getRequestTime();
$connection = Database::getConnection();
foreach (['dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB'] as $prefix) {
$changed += 1000;
$node = $this->drupalCreateNode(['title' => $prefix . $this->randomMachineName(6)]);
$connection->update('node_field_data')
->fields(['changed' => $changed])
->condition('nid', $node->id())
->execute();
}
// Test that the default sort by node.changed DESC actually fires properly.
$nodes_query = $connection->select('node_field_data', 'n')
->fields('n', ['title'])
->orderBy('changed', 'DESC')
->execute()
->fetchCol();
$this->drupalGet('admin/content');
foreach ($nodes_query as $delta => $string) {
// Verify that the node was found in the correct order.
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//table/tbody/tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', [
':label' => $string,
]));
}
// Compare the rendered HTML node list to a query for the nodes ordered by
// title to account for possible database-dependent sort order.
$nodes_query = $connection->select('node_field_data', 'n')
->fields('n', ['title'])
->orderBy('title')
->execute()
->fetchCol();
$this->drupalGet('admin/content', ['query' => ['sort' => 'asc', 'order' => 'title']]);
foreach ($nodes_query as $delta => $string) {
// Verify that the node was found in the correct order.
$this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery('//table/tbody/tr[' . ($delta + 1) . ']/td[2]/a[normalize-space(text())=:label]', [
':label' => $string,
]));
}
// Verify aria-sort is present and its value matches the sort order.
$this->assertSession()->elementAttributeContains('css', 'table thead tr th.views-field-title', 'aria-sort', 'ascending');
}
/**
* Tests content overview with different user permissions.
*
* Taxonomy filters are tested separately.
*
* @see TaxonomyNodeFilterTestCase
*/
public function testContentAdminPages(): void {
$this->drupalLogin($this->adminUser);
// Use an explicit changed time to ensure the expected order in the content
// admin listing. We want these to appear in the table in the same order as
// they appear in the following code, and the 'content' View has a table
// style configuration with a default sort on the 'changed' field DESC.
$time = time();
$nodes['published_page'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--]);
$nodes['published_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--]);
$nodes['unpublished_page_1'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'uid' => $this->baseUser1->id(), 'status' => 0]);
$nodes['unpublished_page_2'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time, 'uid' => $this->baseUser2->id(), 'status' => 0]);
// Verify view, edit, and delete links for any content.
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$node_type_labels = $this->xpath('//td[contains(@class, "views-field-type")]');
$delta = 0;
foreach ($nodes as $node) {
$this->assertSession()->linkByHrefExists('node/' . $node->id());
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/delete');
// Verify that we can see the content type label.
$this->assertEquals(trim($node_type_labels[$delta]->getText()), $node->type->entity->label());
$delta++;
}
// Verify filtering by publishing status.
$this->drupalGet('admin/content', ['query' => ['status' => TRUE]]);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_article']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
// Verify filtering by status and content type.
$this->drupalGet('admin/content', ['query' => ['status' => TRUE, 'type' => 'page']]);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/edit');
// Verify no operation links are displayed for regular users.
$this->drupalLogout();
$this->drupalLogin($this->baseUser1);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_page']->id());
$this->assertSession()->linkByHrefExists('node/' . $nodes['published_article']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_page']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_page']->id() . '/delete');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['published_article']->id() . '/delete');
// Verify no unpublished content is displayed without permission.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/delete');
// Verify no tableselect.
$this->assertSession()->fieldNotExists('nodes[' . $nodes['published_page']->id() . ']');
// Verify unpublished content is displayed with permission.
$this->drupalLogout();
$this->drupalLogin($this->baseUser2);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists('node/' . $nodes['unpublished_page_2']->id());
// Verify no operation links are displayed.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_2']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_2']->id() . '/delete');
// Verify user cannot see unpublished content of other users.
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id());
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/edit');
$this->assertSession()->linkByHrefNotExists('node/' . $nodes['unpublished_page_1']->id() . '/delete');
// Verify no tableselect.
$this->assertSession()->fieldNotExists('nodes[' . $nodes['unpublished_page_2']->id() . ']');
// Verify node access can be bypassed.
$this->drupalLogout();
$this->drupalLogin($this->baseUser3);
$this->drupalGet('admin/content');
$this->assertSession()->statusCodeEquals(200);
foreach ($nodes as $node) {
$this->assertSession()->linkByHrefExists('node/' . $node->id());
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/edit');
$this->assertSession()->linkByHrefExists('node/' . $node->id() . '/delete');
}
// Ensure that the language table column and the language exposed filter are
// not visible on monolingual sites.
$this->assertSession()->fieldNotExists('langcode');
$this->assertEquals(0, count($this->cssSelect('td.views-field-langcode')));
$this->assertEquals(0, count($this->cssSelect('td.views-field-langcode')));
}
/**
* Tests content overview for a multilingual site.
*/
public function testContentAdminPageMultilingual(): void {
$this->drupalLogin($this->adminUser);
\Drupal::service('module_installer')->install(['language']);
ConfigurableLanguage::create([
'id' => 'es',
'label' => 'Spanish',
])->save();
$this->drupalCreateNode(['type' => 'page', 'title' => 'English title'])
->addTranslation('es')
->setTitle('Spanish title')
->save();
$this->drupalGet('admin/content');
// Ensure that both the language table column as well as the language
// exposed filter are visible on multilingual sites.
$this->assertSession()->fieldExists('langcode');
$this->assertEquals(2, count($this->cssSelect('td.views-field-langcode')));
$this->assertEquals(2, count($this->cssSelect('td.views-field-langcode')));
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => '***LANGUAGE_site_default***']]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'en']]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'und']]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => 'zxx']]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('admin/content', ['query' => ['langcode' => html_entity_decode('***LANGUAGE_language_interface***')]]);
$this->assertSession()->pageTextContains('English title');
$this->assertSession()->pageTextNotContains('Spanish title');
$this->drupalGet('es/admin/content', ['query' => ['langcode' => html_entity_decode('***LANGUAGE_language_interface***')]]);
$this->assertSession()->pageTextNotContains('English title');
$this->assertSession()->pageTextContains('Spanish title');
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\block\Entity\Block;
use Drupal\Core\Database\Database;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\user\RoleInterface;
/**
* Tests node block functionality.
*
* @group node
*/
class NodeBlockFunctionalTest extends NodeTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An administrative user for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An unprivileged user for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block', 'views', 'node_block_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users and test node.
$this->adminUser = $this->drupalCreateUser([
'administer content types',
'administer nodes',
'bypass node access',
'administer blocks',
'access content overview',
]);
$this->webUser = $this->drupalCreateUser([
'access content',
'create article content',
]);
}
/**
* Tests the recent comments block.
*/
public function testRecentNodeBlock(): void {
$this->drupalLogin($this->adminUser);
// Disallow anonymous users to view content.
user_role_change_permissions(RoleInterface::ANONYMOUS_ID, [
'access content' => FALSE,
]);
// Enable the recent content block with two items.
$block = $this->drupalPlaceBlock('views_block:content_recent-block_1', ['id' => 'test_block', 'items_per_page' => 2]);
// Test that block is not visible without nodes.
$this->drupalGet('');
$this->assertSession()->pageTextContains('No content available.');
// Add some test nodes.
$default_settings = ['uid' => $this->webUser->id(), 'type' => 'article'];
$node1 = $this->drupalCreateNode($default_settings);
$node2 = $this->drupalCreateNode($default_settings);
$node3 = $this->drupalCreateNode($default_settings);
// Create a second revision of node1.
$node1_revision_1 = $node1;
$node1->setNewRevision(TRUE);
$node1->setTitle('Node revision 2 title');
$node1->save();
$connection = Database::getConnection();
// Change the changed time for node so that we can test ordering.
$connection->update('node_field_data')
->fields([
'changed' => $node1->getChangedTime() + 100,
])
->condition('nid', $node2->id())
->execute();
$connection->update('node_field_data')
->fields([
'changed' => $node1->getChangedTime() + 200,
])
->condition('nid', $node3->id())
->execute();
// Test that a user without the 'access content' permission cannot
// see the block.
$this->drupalLogout();
$this->drupalGet('');
$this->assertSession()->pageTextNotContains($block->label());
// Test that only the 2 latest nodes are shown.
$this->drupalLogin($this->webUser);
$this->assertSession()->pageTextNotContains($node1->label());
$this->assertSession()->pageTextContains($node2->label());
$this->assertSession()->pageTextContains($node3->label());
// Check to make sure nodes are in the right order.
$this->assertSession()->elementExists('xpath', '//div[@id="block-test-block"]//div[@class="item-list"]/ul/li[1]/div/span/a[text() = "' . $node3->label() . '"]');
$this->drupalLogout();
$this->drupalLogin($this->adminUser);
// Set the number of recent nodes to show to 10.
$block->getPlugin()->setConfigurationValue('items_per_page', 10);
$block->save();
// Post an additional node.
$node4 = $this->drupalCreateNode($default_settings);
// Test that all four nodes are shown.
$this->drupalGet('');
$this->assertSession()->pageTextContains($node1->label());
$this->assertSession()->pageTextContains($node2->label());
$this->assertSession()->pageTextContains($node3->label());
$this->assertSession()->pageTextContains($node4->label());
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user']);
// Enable the "Powered by Drupal" block only on article nodes.
$theme = \Drupal::service('theme_handler')->getDefault();
$this->drupalGet("admin/structure/block/add/system_powered_by_block/{$theme}");
$this->assertSession()->pageTextContains('Content type');
$edit = [
'id' => $this->randomMachineName(),
'region' => 'sidebar_first',
'visibility[entity_bundle:node][bundles][article]' => 'article',
];
$this->submitForm($edit, 'Save block');
$block = Block::load($edit['id']);
$visibility = $block->getVisibility();
$this->assertTrue(isset($visibility['entity_bundle:node']['bundles']['article']), 'Visibility settings were saved to configuration');
// Create a page node.
$node5 = $this->drupalCreateNode(['uid' => $this->adminUser->id(), 'type' => 'page']);
$this->drupalLogout();
$this->drupalLogin($this->webUser);
// Verify visibility rules.
$this->drupalGet('');
$label = $block->label();
// Check that block is not displayed on the front page.
$this->assertSession()->pageTextNotContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route']);
// Ensure that a page that does not have a node context can still be cached,
// the front page is the user page which is already cached from the login
// request above.
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
$this->drupalGet('node/add/article');
// Check that block is displayed on the add article page.
$this->assertSession()->pageTextContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'session', 'theme', 'url.path', 'url.query_args', 'user', 'route']);
// The node/add/article page is an admin path and currently uncacheable.
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'UNCACHEABLE');
$this->drupalGet('node/' . $node1->id());
// Check that block is displayed on the node page when node is of type
// 'article'.
$this->assertSession()->pageTextContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route', 'timezone']);
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->drupalGet('node/' . $node1->id());
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
$this->drupalGet('node/' . $node5->id());
// Check that block is not displayed on the node page when node is of type
// 'page'.
$this->assertSession()->pageTextNotContains($label);
$this->assertCacheContexts(['languages:language_content', 'languages:language_interface', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.site', 'user', 'route', 'timezone']);
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->drupalGet('node/' . $node5->id());
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
// Place a block to determine which revision is provided as context
// to blocks.
$this->drupalPlaceBlock('node_block_test_context', [
'context_mapping' => ['node' => '@node.node_route_context:node'],
]);
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/' . $node1->id());
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1->getRevisionId() . ': Node revision 2 title');
// Assert that the preview page displays the block as well.
$this->drupalGet('node/' . $node1->id() . '/edit');
$this->submitForm([], 'Preview');
$this->assertSession()->pageTextContains($label);
// The previewed node object has no revision ID.
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #: Node revision 2 title');
// Assert that the revision page for both revisions displays the block.
$this->drupalGet(Url::fromRoute('entity.node.revision', ['node' => $node1->id(), 'node_revision' => $node1_revision_1->getRevisionId()]));
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1_revision_1->getRevisionId() . ': ' . $node1_revision_1->label());
$this->drupalGet(Url::fromRoute('entity.node.revision', ['node' => $node1->id(), 'node_revision' => $node1->getRevisionId()]));
$this->assertSession()->pageTextContains($label);
$this->assertSession()->pageTextContains('Displaying node #' . $node1->id() . ', revision #' . $node1->getRevisionId() . ': Node revision 2 title');
$this->drupalGet('admin/structure/block');
// Check that block is displayed on the admin/structure/block page.
$this->assertSession()->pageTextContains($label);
$this->assertSession()->linkByHrefExists($block->toUrl()->toString());
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\system\Functional\Entity\EntityWithUriCacheTagsTestBase;
/**
* Tests the Node entity's cache tags.
*
* @group node
*/
class NodeCacheTagsTest extends EntityWithUriCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" node type.
NodeType::create([
'name' => 'Camelids',
'type' => 'camelids',
])->save();
// Create a "Llama" node.
$node = Node::create(['type' => 'camelids']);
$node->setTitle('Llama')
->setPublished()
->save();
return $node;
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheContextsForEntity(EntityInterface $entity) {
return ['timezone'];
}
/**
* {@inheritdoc}
*
* Each node must have an author.
*/
protected function getAdditionalCacheTagsForEntity(EntityInterface $node) {
return ['user:' . $node->getOwnerId(), 'user_view'];
}
/**
* {@inheritdoc}
*/
protected function getAdditionalCacheContextsForEntityListing() {
return ['user.node_grants:view'];
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Tests views contextual links on nodes.
*
* @group node
*/
class NodeContextualLinksTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'contextual',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests contextual links.
*/
public function testNodeContextualLinks(): void {
// Create a node item.
$node = Node::create([
'type' => 'article',
'title' => 'Unnamed',
]);
$node->save();
$user = $this->drupalCreateUser([
'administer nodes',
'access contextual links',
]);
$this->drupalLogin($user);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'node:node=' . $node->id() . ':');
}
}

View File

@@ -0,0 +1,336 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Language\LanguageInterface;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Create a node and test saving it.
*
* @group node
* @group #slow
*/
class NodeCreationTest extends NodeTestBase {
use ContentTypeCreationTrait;
/**
* Modules to enable.
*
* Enable dummy module that implements hook_ENTITY_TYPE_insert() for
* exceptions (function node_test_exception_node_insert() ).
*
* @var array
*/
protected static $modules = [
'node_test_exception',
'dblog',
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'create page content',
'edit own page content',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the order of the node types on the add page.
*/
public function testNodeAddPageOrder(): void {
$this->createContentType(['type' => 'bundle_1', 'name' => 'Bundle 1']);
$this->createContentType(['type' => 'bundle_2', 'name' => 'Aaa Bundle 2']);
$admin_content_types = $this->drupalCreateUser(['bypass node access']);
$this->drupalLogin($admin_content_types);
$this->drupalGet('node/add');
$this->assertSession()->pageTextMatches('/Aaa Bundle 2(.*)Bundle 1/');
}
/**
* Creates a "Basic page" node and verifies its consistency in the database.
*/
public function testNodeCreation(): void {
$node_type_storage = \Drupal::entityTypeManager()->getStorage('node_type');
// Test /node/add page with only one content type.
$node_type_storage->load('article')->delete();
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals('node/add/page');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the Basic page has been 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/")]');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotEmpty($node, 'Node found in database.');
// Verify that pages do not show submitted information by default.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextNotContains($node->getOwner()->getAccountName());
$this->assertSession()->pageTextNotContains($this->container->get('date.formatter')->format($node->getCreatedTime()));
// Change the node type setting to show submitted by information.
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = $node_type_storage->load('page');
$node_type->setDisplaySubmitted(TRUE);
$node_type->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($node->getOwner()->getAccountName());
$this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getCreatedTime()));
// Check if the node revision checkbox is not rendered on node creation form.
$admin_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
$this->assertSession()->fieldNotExists('edit-revision', NULL);
// Check that a user with administer content types permission is not
// allowed to create content.
$content_types_admin = $this->drupalCreateUser(['administer content types']);
$this->drupalLogin($content_types_admin);
$this->drupalGet('node/add/page');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Verifies that a transaction rolls back the failed creation.
*/
public function testFailedPageCreation(): void {
// Create a node.
$edit = [
'uid' => $this->loggedInUser->id(),
'name' => $this->loggedInUser->name,
'type' => 'page',
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'title' => 'testing_transaction_exception',
];
try {
// An exception is generated by node_test_exception_node_insert() if the
// title is 'testing_transaction_exception'.
Node::create($edit)->save();
$this->fail('Expected exception has not been thrown.');
}
catch (\Exception $e) {
// Expected exception; just continue testing.
}
// Check that the node does not exist in the database.
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertFalse($node);
// Check that the rollback error was logged.
$records = static::getWatchdogIdsForTestExceptionRollback();
// Verify that the rollback explanatory error was logged.
$this->assertNotEmpty($records);
}
/**
* Creates an unpublished node and confirms correct redirect behavior.
*/
public function testUnpublishedNodeCreation(): void {
// Set the front page to the test page.
$this->config('system.site')->set('page.front', '/test-page')->save();
// Set "Basic page" content type to be unpublished by default.
$fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page');
$fields['status']->getConfig('page')
->setDefaultValue(FALSE)
->save();
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the user was redirected to the home page.
$this->assertSession()->addressEquals('');
$this->assertSession()->pageTextContains('Test page text');
// 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/")]');
}
/**
* Creates nodes with different authored dates.
*/
public function testAuthoredDate(): void {
$now = \Drupal::time()->getRequestTime();
$admin = $this->drupalCreateUser([], NULL, TRUE);
$this->drupalLogin($admin);
// Create a node with the default creation date.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotNull($node->getCreatedTime());
// Create a node with the custom creation date in the past.
$date = $now - 86400;
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => date('Y-m-d', $date),
'created[0][value][time]' => date('H:i:s', $date),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($date, $node->getCreatedTime());
// Create a node with the custom creation date in the future.
$date = $now + 86400;
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => date('Y-m-d', $date),
'created[0][value][time]' => date('H:i:s', $date),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertEquals($date, $node->getCreatedTime());
// Test an invalid date.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => '2013-13-13',
'created[0][value][time]' => '11:00:00',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The Authored on date is invalid.');
$this->assertFalse($this->drupalGetNodeByTitle($edit['title[0][value]']));
// Test an invalid time.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
'created[0][value][date]' => '2012-01-01',
'created[0][value][time]' => '30:00:00',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The Authored on date is invalid.');
$this->assertFalse($this->drupalGetNodeByTitle($edit['title[0][value]']));
}
/**
* Tests the author autocompletion textfield.
*/
public function testAuthorAutocomplete(): void {
$admin_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
// Verify that no autocompletion exists without access user profiles.
$this->assertSession()->elementNotExists('xpath', '//input[@id="edit-uid-0-value" and contains(@data-autocomplete-path, "user/autocomplete")]');
$admin_user = $this->drupalCreateUser([
'administer nodes',
'create page content',
'access user profiles',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('node/add/page');
// Ensure that the user does have access to the autocompletion.
$this->assertSession()->elementsCount('xpath', '//input[@id="edit-uid-0-target-id" and contains(@data-autocomplete-path, "/entity_reference_autocomplete/user/default")]', 1);
}
/**
* Check node/add when no node types exist.
*/
public function testNodeAddWithoutContentTypes(): void {
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefNotExists('/admin/structure/types/add');
// Test /node/add page without content types.
foreach (\Drupal::entityTypeManager()->getStorage('node_type')->loadMultiple() as $entity) {
$entity->delete();
}
$this->drupalGet('node/add');
$this->assertSession()->statusCodeEquals(403);
$admin_content_types = $this->drupalCreateUser([
'administer content types',
]);
$this->drupalLogin($admin_content_types);
$this->drupalGet('node/add');
$this->assertSession()->linkByHrefExists('/admin/structure/types/add');
}
/**
* Gets the watchdog IDs of the records with the rollback exception message.
*
* @return int[]
* Array containing the IDs of the log records with the rollback exception
* message.
*/
protected static function getWatchdogIdsForTestExceptionRollback() {
// PostgreSQL doesn't support bytea LIKE queries, so we need to unserialize
// first to check for the rollback exception message.
$matches = [];
$query = Database::getConnection()->select('watchdog', 'w')
->fields('w', ['wid', 'variables'])
->execute();
foreach ($query as $row) {
$variables = (array) unserialize($row->variables);
if (isset($variables['@message']) && $variables['@message'] === 'Test exception for rollback.') {
$matches[] = $row->wid;
}
}
return $matches;
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
/**
* Create a node and test node edit functionality.
*
* @group node
*/
class NodeEditFormTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A normal logged in user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* A user with permission to bypass content access checks.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* The node storage.
*
* @var \Drupal\node\NodeStorageInterface
*/
protected $nodeStorage;
/**
* Modules to enable.
*
* @var string[]
*/
protected static $modules = ['block', 'node', 'datetime'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->webUser = $this->drupalCreateUser([
'edit own page content',
'create page content',
]);
$this->adminUser = $this->drupalCreateUser([
'bypass node access',
'administer nodes',
]);
$this->drupalPlaceBlock('local_tasks_block');
$this->nodeStorage = $this->container->get('entity_type.manager')->getStorage('node');
}
/**
* Checks node edit functionality.
*/
public function testNodeEdit(): void {
$this->drupalLogin($this->webUser);
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
// Create node to edit.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
// Check that "edit" link points to correct page.
$this->clickLink('Edit');
$this->assertSession()->addressEquals($node->toUrl('edit-form'));
// Check that the title and body fields are displayed with the correct values.
// @todo Ideally assertLink would support HTML, but it doesn't.
$this->assertSession()->responseContains('Edit');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
// Edit the content of the node.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
// Stay on the current page, without reloading.
$this->submitForm($edit, 'Save');
// Check that the title and body fields are displayed with the updated values.
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
// Log in as a second administrator user.
$second_web_user = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
]);
$this->drupalLogin($second_web_user);
// Edit the same node, creating a new revision.
$this->drupalGet("node/" . $node->id() . "/edit");
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$edit['revision'] = TRUE;
$this->submitForm($edit, 'Save');
// Ensure that the node revision has been created.
$revised_node = $this->drupalGetNodeByTitle($edit['title[0][value]'], TRUE);
$this->assertNotSame($node->getRevisionId(), $revised_node->getRevisionId(), 'A new revision has been created.');
// Ensure that the node author is preserved when it was not changed in the
// edit form.
$this->assertSame($node->getOwnerId(), $revised_node->getOwnerId(), 'The node author has been preserved.');
// Ensure that the revision authors are different since the revisions were
// made by different users.
$node_storage = \Drupal::service('entity_type.manager')->getStorage('node');
$first_node_version = $node_storage->loadRevision($node->getRevisionId());
$second_node_version = $node_storage->loadRevision($revised_node->getRevisionId());
$this->assertNotSame($first_node_version->getRevisionUser()->id(), $second_node_version->getRevisionUser()->id(), 'Each revision has a distinct user.');
// Check if the node revision checkbox is rendered on node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldExists('edit-revision', NULL);
// Check that details form element opens when there are errors on child
// elements.
$this->drupalGet('node/' . $node->id() . '/edit');
$edit = [];
// This invalid date will trigger an error.
$edit['created[0][value][date]'] = $this->randomMachineName(8);
// Get the current amount of open details elements.
$open_details_elements = count($this->cssSelect('details[open="open"]'));
$this->submitForm($edit, 'Save');
// The node author details must be open.
$this->assertSession()->responseContains('<details class="node-form-author js-form-wrapper form-wrapper" data-drupal-selector="edit-author" id="edit-author" open="open">');
// Only one extra details element should now be open.
$open_details_elements++;
$this->assertCount($open_details_elements, $this->cssSelect('details[open="open"]'), 'Exactly one extra open &lt;details&gt; element found.');
// Edit the same node, save it and verify it's unpublished after unchecking
// the 'Published' boolean_checkbox and clicking 'Save'.
$this->drupalGet("node/" . $node->id() . "/edit");
$edit = ['status[value]' => FALSE];
$this->submitForm($edit, 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertFalse($node->isPublished(), 'Node is unpublished');
}
/**
* Tests changing a node's "authored by" field.
*/
public function testNodeEditAuthoredBy(): void {
$this->drupalLogin($this->adminUser);
// Create node to edit.
$body_key = 'body[0][value]';
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node was authored by the currently logged in user.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertSame($this->adminUser->id(), $node->getOwnerId(), 'Node authored by admin user.');
$this->checkVariousAuthoredByValues($node, 'uid[0][target_id]');
// Check that normal users cannot change the authored by information.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldNotExists('uid[0][target_id]');
// Now test with the Autocomplete (Tags) field widget.
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
$form_display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load('node.page.default');
$widget = $form_display->getComponent('uid');
$widget['type'] = 'entity_reference_autocomplete_tags';
$widget['settings'] = [
'match_operator' => 'CONTAINS',
'size' => 60,
'placeholder' => '',
];
$form_display->setComponent('uid', $widget);
$form_display->save();
$this->drupalLogin($this->adminUser);
// Save the node without making any changes.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwner()->id());
$this->checkVariousAuthoredByValues($node, 'uid[target_id]');
// Hide the 'authored by' field from the form.
$form_display->removeComponent('uid')->save();
// Check that saving the node without making any changes keeps the proper
// author ID.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwner()->id());
}
/**
* Tests the node meta information.
*/
public function testNodeMetaInformation(): void {
// Check that regular users (i.e. without the 'administer nodes' permission)
// can not see the meta information.
$this->drupalLogin($this->webUser);
$this->drupalGet('node/add/page');
$this->assertSession()->pageTextNotContains('Not saved yet');
// Create node to edit.
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->pageTextNotContains('Published');
$this->assertSession()->pageTextNotContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short'));
// Check that users with the 'administer nodes' permission can see the meta
// information.
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/page');
$this->assertSession()->pageTextContains('Not saved yet');
// Create node to edit.
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->pageTextContains('Published');
$this->assertSession()->pageTextContains($this->container->get('date.formatter')->format($node->getChangedTime(), 'short'));
}
/**
* Checks that the "authored by" works correctly with various values.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
* @param string $form_element_name
* The name of the form element to populate.
*/
protected function checkVariousAuthoredByValues(NodeInterface $node, $form_element_name) {
// Try to change the 'authored by' field to an invalid user name.
$edit = [
$form_element_name => 'invalid-name',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('There are no users matching "invalid-name".');
// Change the authored by field to an empty string, which should assign
// authorship to the anonymous user (uid 0).
$edit[$form_element_name] = '';
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$uid = $node->getOwnerId();
// Most SQL database drivers stringify fetches but entities are not
// necessarily stored in a SQL database. At the same time, NULL/FALSE/""
// won't do.
$this->assertTrue($uid === 0 || $uid === '0', 'Node authored by anonymous user.');
// Go back to the edit form and check that the correct value is displayed
// in the author widget.
$this->drupalGet('node/' . $node->id() . '/edit');
$anonymous_user = User::getAnonymousUser();
$expected = $anonymous_user->label() . ' (' . $anonymous_user->id() . ')';
$this->assertSession()->fieldValueEquals($form_element_name, $expected);
// Change the authored by field to another user's name (that is not
// logged in).
$edit[$form_element_name] = $this->webUser->getAccountName();
$this->submitForm($edit, 'Save');
$this->nodeStorage->resetCache([$node->id()]);
$node = $this->nodeStorage->load($node->id());
$this->assertSame($this->webUser->id(), $node->getOwnerId(), 'Node authored by normal user.');
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\EntityViewTrait;
/**
* Tests changing view modes for nodes.
*
* @group node
*/
class NodeEntityViewModeAlterTest extends NodeTestBase {
use EntityViewTrait;
/**
* Enable dummy module that implements hook_ENTITY_TYPE_view() for nodes.
*/
protected static $modules = ['node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Create a "Basic page" node and verify its consistency in the database.
*/
public function testNodeViewModeChange(): void {
$web_user = $this->drupalCreateUser([
'create page content',
'edit own page content',
]);
$this->drupalLogin($web_user);
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = 'Data that should appear only in the body for the node.';
$edit['body[0][summary]'] = 'Extra data that should appear only in the teaser for the node.';
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
// Set the flag to alter the view mode and view the node.
\Drupal::state()->set('node_test_change_view_mode', 'teaser');
Cache::invalidateTags(['rendered']);
$this->drupalGet('node/' . $node->id());
// Check that teaser mode is viewed.
$this->assertSession()->pageTextContains('Extra data that should appear only in the teaser for the node.');
// Make sure body text is not present.
$this->assertSession()->pageTextNotContains('Data that should appear only in the body for the node.');
// Test that the correct build mode has been set.
$build = $this->buildEntityView($node);
$this->assertEquals('teaser', $build['#view_mode'], 'The view mode has correctly been set to teaser.');
}
}

View File

@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Tests multilingual support for fields.
*
* @group node
*/
class NodeFieldMultilingualTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page node type.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Setup users.
$admin_user = $this->drupalCreateUser([
'administer languages',
'administer content types',
'access administration pages',
'create page content',
'edit own page content',
]);
$this->drupalLogin($admin_user);
// Add a new language.
ConfigurableLanguage::createFromLangcode('it')->save();
// Enable URL language detection and selection.
$edit = ['language_interface[enabled][language-url]' => '1'];
$this->drupalGet('admin/config/regional/language/detection');
$this->submitForm($edit, 'Save settings');
// Set "Basic page" content type to use multilingual support.
$edit = [
'language_configuration[language_alterable]' => TRUE,
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The content type Basic page has been updated.");
// Make node body translatable.
$field_storage = FieldStorageConfig::loadByName('node', 'body');
$field_storage->setTranslatable(TRUE);
$field_storage->save();
}
/**
* Tests whether field languages are correctly set through the node form.
*/
public function testMultilingualNodeForm(): void {
// Create "Basic page" content.
$langcode = language_get_default_langcode('node', 'page');
$title_key = 'title[0][value]';
$title_value = $this->randomMachineName(8);
$body_key = 'body[0][value]';
$body_value = $this->randomMachineName(16);
// Create node to edit.
$edit = [];
$edit[$title_key] = $title_value;
$edit[$body_key] = $body_value;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
$this->assertSame($langcode, $node->language()->getId());
$this->assertSame($body_value, $node->body->value);
// Change node language.
$langcode = 'it';
$this->drupalGet("node/{$node->id()}/edit");
$edit = [
$title_key => $this->randomMachineName(8),
'langcode[0][value]' => $langcode,
];
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key], TRUE);
$this->assertNotEmpty($node, 'Node found in database.');
$this->assertSame($langcode, $node->language()->getId());
$this->assertSame($body_value, $node->body->value);
// Enable content language URL detection.
$this->container->get('language_negotiator')->saveConfiguration(LanguageInterface::TYPE_CONTENT, [LanguageNegotiationUrl::METHOD_ID => 0]);
// Test multilingual field language fallback logic.
$this->drupalGet("it/node/{$node->id()}");
// Verify that body is correctly displayed using Italian as requested
// language.
$this->assertSession()->pageTextContains($body_value);
$this->drupalGet("node/{$node->id()}");
// Verify that body is correctly displayed using English as requested
// language.
$this->assertSession()->pageTextContains($body_value);
}
/**
* Tests multilingual field display settings.
*/
public function testMultilingualDisplaySettings(): void {
// Create "Basic page" content.
$title_key = 'title[0][value]';
$title_value = $this->randomMachineName(8);
$body_key = 'body[0][value]';
$body_value = $this->randomMachineName(16);
// Create node to edit.
$edit = [];
$edit[$title_key] = $title_value;
$edit[$body_key] = $body_value;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
$this->assertNotEmpty($node, 'Node found in database.');
// Check if node body is showed.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementTextEquals('xpath', "//article/div//p", $node->body->value);
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests updating the changed time after API and FORM entity save.
*
* @group node
*/
class NodeFormSaveChangedTimeTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permissions to create and edit articles.
*
* @var \Drupal\user\UserInterface
*/
protected $authorUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a node type.
$this->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$this->authorUser = $this->drupalCreateUser([
'access content',
'create article content',
'edit any article content',
], 'author');
$this->drupalLogin($this->authorUser);
// Create one node of the above node type .
$this->drupalCreateNode([
'type' => 'article',
]);
}
/**
* Tests the changed time after API and FORM save without changes.
*/
public function testChangedTimeAfterSaveWithoutChanges(): void {
$storage = $this->container->get('entity_type.manager')->getStorage('node');
$storage->resetCache([1]);
$node = $storage->load(1);
$changed_timestamp = $node->getChangedTime();
$node->save();
$storage->resetCache([1]);
$node = $storage->load(1);
$this->assertEquals($changed_timestamp, $node->getChangedTime(), "The entity's changed time wasn't updated after API save without changes.");
// Ensure different save timestamps.
sleep(1);
// Save the node on the regular node edit form.
$this->drupalGet('node/1/edit');
$this->submitForm([], 'Save');
$storage->resetCache([1]);
$node = $storage->load(1);
$this->assertNotEquals($node->getChangedTime(), $changed_timestamp, "The entity's changed time was updated after form save without changes.");
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests help functionality for nodes.
*
* @group node
*/
class NodeHelpTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block', 'node', 'help'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The name of the test node type to create.
*
* @var string
*/
protected $testType;
/**
* The test 'node help' text to be checked.
*
* @var string
*/
protected $testText;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create user.
$admin_user = $this->drupalCreateUser([
'administer content types',
'administer nodes',
'bypass node access',
]);
$this->drupalLogin($admin_user);
$this->drupalPlaceBlock('help_block');
$this->testType = 'type';
$this->testText = 'Help text to find on node forms.';
// Create content type.
$this->drupalCreateContentType([
'type' => $this->testType,
'help' => $this->testText,
]);
}
/**
* Verifies that help text appears on node add/edit forms.
*/
public function testNodeShowHelpText(): void {
// Check the node add form.
$this->drupalGet('node/add/' . $this->testType);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->testText);
// Create node and check the node edit form.
$node = $this->drupalCreateNode(['type' => $this->testType]);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains($this->testText);
}
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\NodeInterface;
/**
* Tests the output of node links (read more, add new comment, etc).
*
* @group node
*/
class NodeLinksTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that the links can be hidden in the view display settings.
*/
public function testHideLinks(): void {
$node = $this->drupalCreateNode([
'type' => 'article',
'promote' => NodeInterface::PROMOTED,
]);
// Links are displayed by default.
$this->drupalGet('node');
$this->assertSession()->pageTextContains($node->getTitle());
$this->assertSession()->linkExists('Read more');
// Hide links.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'article', 'teaser')
->removeComponent('links')
->save();
$this->drupalGet('node');
$this->assertSession()->pageTextContains($node->getTitle());
$this->assertSession()->linkNotExists('Read more');
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests the node setting for displaying author and date information.
*
* @group node
*/
class NodePostSettingsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'create page content',
'administer content types',
'access user profiles',
]);
$this->drupalLogin($web_user);
}
/**
* Confirms "Basic page" content type and post information is on a new node.
*/
public function testPagePostInfo(): void {
// Set "Basic page" content type to display post information.
$edit = [];
$edit['display_submitted'] = TRUE;
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the post information is displayed.
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertSession()->pageTextContainsOnce('Submitted by');
$node->delete();
// Set "Basic page" content type to display post information.
$edit = [];
$edit['display_submitted'] = FALSE;
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
// Create a node.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(8);
$edit['body[0][value]'] = $this->randomMachineName(16);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Check that the post information is not displayed.
$this->assertSession()->pageTextNotContains('Submitted by');
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Session\AccountInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
/**
* Tests the node entity preview functionality for anonymous user.
*
* @group node
*/
class NodePreviewAnonymousTest extends BrowserTestBase {
/**
* Enable node module to test on the preview.
*
* @var array
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page node type.
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
// Grant create and editing permissions to anonymous user:
$anonymous_role = Role::load(AccountInterface::ANONYMOUS_ROLE);
$anonymous_role->grantPermission('create page content');
$anonymous_role->save();
}
/**
* Checks the node preview functionality for anonymous users.
*/
public function testAnonymousPagePreview(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
// Fill in node creation form and preview node.
$edit = [
$title_key => $this->randomMachineName(),
$body_key => $this->randomMachineName(),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Check that the preview is displaying the title, body and term.
$this->assertSession()->linkExists('Back to content editing');
$this->assertSession()->responseContains($edit[$body_key]);
$this->assertSession()->titleEquals($edit[$title_key] . ' | Drupal');
}
}

View File

@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\user\Entity\User;
/**
* Tests that node access queries are properly altered by the node module.
*
* @group node
* @group #slow
*/
class NodeQueryAlterTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node_access_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with permission to view content.
*/
protected $accessUser;
/**
* User without permission to view content.
*/
protected $noAccessUser;
/**
* User without permission to view content.
*
* @var \Drupal\user\Entity\User
*/
protected User $noAccessUser2;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
// Create some content.
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
$this->drupalCreateNode();
// Create user with simple node access permission. The 'node test view'
// permission is implemented and granted by the node_access_test module.
$this->accessUser = $this->drupalCreateUser([
'access content overview',
'access content',
'node test view',
]);
$this->noAccessUser = $this->drupalCreateUser([
'access content overview',
'access content',
]);
$this->noAccessUser2 = $this->drupalCreateUser([
'access content overview',
'access content',
]);
}
/**
* Tests 'node_access' query alter, for user with access.
*
* Verifies that a non-standard table alias can be used, and that a user with
* node access can view the nodes.
*/
public function testNodeQueryAlterLowLevelWithAccess(): void {
// User with access should be able to view 4 nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->accessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(4, $result, 'User with access can see correct nodes');
}
catch (\Exception $e) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter with revision-enabled nodes.
*/
public function testNodeQueryAlterWithRevisions(): void {
// Execute a query that only deals with the 'node_revision' table.
try {
$query = \Drupal::entityTypeManager()->getStorage('node')->getQuery();
$result = $query
->accessCheck(TRUE)
->allRevisions()
->execute();
$this->assertCount(4, $result, 'User with access can see correct nodes');
}
catch (\Exception $e) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter, for user without access.
*
* Verifies that a non-standard table alias can be used, and that a user
* without node access cannot view the nodes.
*/
public function testNodeQueryAlterLowLevelNoAccess(): void {
// User without access should be able to view 0 nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User with no access cannot see nodes');
}
catch (\Exception $e) {
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter, for edit access.
*
* Verifies that a non-standard table alias can be used, and that a user with
* view-only node access cannot edit the nodes.
*/
public function testNodeQueryAlterLowLevelEditAccess(): void {
// User with view-only access should not be able to edit nodes.
try {
$query = Database::getConnection()->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'update');
$query->addMetaData('account', $this->accessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User with view-only access cannot edit nodes');
}
catch (\Exception $e) {
$this->fail($e->getMessage());
$this->fail((string) $query);
$this->fail('Altered query is malformed');
}
}
/**
* Tests 'node_access' query alter override.
*
* Verifies that node_access_view_all_nodes() is called from
* node_query_node_access_alter(). We do this by checking that a user who
* normally would not have view privileges is able to view the nodes when we
* add a record to {node_access} paired with a corresponding privilege in
* hook_node_grants().
*/
public function testNodeQueryAlterOverride(): void {
$record = [
'nid' => 0,
'gid' => 0,
'realm' => 'node_access_all',
'grant_view' => 1,
'grant_update' => 0,
'grant_delete' => 0,
];
$connection = Database::getConnection();
$connection->insert('node_access')->fields($record)->execute();
// Test that the noAccessUser still doesn't have the 'view'
// privilege after adding the node_access record.
drupal_static_reset('node_access_view_all_nodes');
try {
$query = $connection->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(0, $result, 'User view privileges are not overridden');
}
catch (\Exception $e) {
$this->fail('Altered query is malformed');
}
// Have node_test_node_grants return a node_access_all privilege,
// to grant the noAccessUser 'view' access. To verify that
// node_access_view_all_nodes is properly checking the specified
// $account instead of the current user, we will log in as
// noAccessUser2.
$this->drupalLogin($this->noAccessUser2);
\Drupal::state()->set('node_access_test.no_access_uid', $this->noAccessUser->id());
drupal_static_reset('node_access_view_all_nodes');
try {
$query = $connection->select('node', 'n')
->fields('n');
$query->addTag('node_access');
$query->addMetaData('op', 'view');
$query->addMetaData('account', $this->noAccessUser);
$result = $query->execute()->fetchAll();
$this->assertCount(4, $result, 'User view privileges are overridden');
}
catch (\Exception $e) {
$this->fail('Altered query is malformed');
}
\Drupal::state()->delete('node_access_test.no_access_uid');
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\filter\Entity\FilterFormat;
/**
* Ensures that data added to nodes by other modules appears in RSS feeds.
*
* Create a node, enable the node_test module to ensure that extra data is
* added to the node's renderable array, then verify that the data appears on
* the site-wide RSS feed at rss.xml.
*
* @group node
*/
class NodeRSSContentTest extends NodeTestBase {
/**
* Enable a module that implements hook_node_view().
*
* @var array
*/
protected static $modules = ['node_test', 'views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Use bypass node access permission here, because the test class uses
// hook_grants_alter() to deny access to everyone on node_access
// queries.
$user = $this->drupalCreateUser([
'bypass node access',
'access content',
'create article content',
]);
$this->drupalLogin($user);
}
/**
* Ensures that a new node includes the custom data when added to an RSS feed.
*/
public function testNodeRSSContent(): void {
// Create a node.
$node = $this->drupalCreateNode(['type' => 'article', 'promote' => 1]);
$this->drupalGet('rss.xml');
// Check that content added in 'rss' view mode appear in RSS feed.
$rss_only_content = 'Extra data that should appear only in the RSS feed for node ' . $node->id() . '.';
$this->assertSession()->responseContains($rss_only_content);
// Check that content added in view modes other than 'rss' doesn't
// appear in RSS feed.
$non_rss_content = 'Extra data that should appear everywhere except the RSS feed for node ' . $node->id() . '.';
$this->assertSession()->responseNotContains($non_rss_content);
// Check that extra RSS elements and namespaces are added to RSS feed.
$test_element = "<testElement>Value of testElement RSS element for node {$node->id()}.</testElement>";
$test_ns = 'xmlns:test="http://example.com/test-namespace"';
$this->assertSession()->responseContains($test_element);
$this->assertSession()->responseContains($test_ns);
// Check that content added in 'rss' view mode doesn't appear when
// viewing node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseNotContains($rss_only_content);
}
/**
* Tests relative, root-relative, protocol-relative and absolute URLs.
*/
public function testUrlHandling(): void {
// Only the plain_text text format is available by default, which escapes
// all HTML.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'filters' => [],
])->save();
$defaults = [
'type' => 'article',
'promote' => 1,
];
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $file_url_generator->generateString('public://root-relative') . '">Root-relative URL</a></p>',
'format' => 'full_html',
],
]);
$protocol_relative_url = substr($file_url_generator->generateAbsoluteString('public://protocol-relative'), strlen(\Drupal::request()->getScheme() . ':'));
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $protocol_relative_url . '">Protocol-relative URL</a></p>',
'format' => 'full_html',
],
]);
$absolute_url = $file_url_generator->generateAbsoluteString('public://absolute');
$this->drupalCreateNode($defaults + [
'body' => [
'value' => '<p><a href="' . $absolute_url . '">Absolute URL</a></p>',
'format' => 'full_html',
],
]);
$this->drupalGet('rss.xml');
// Verify that root-relative URL is transformed to absolute.
$this->assertSession()->responseContains($file_url_generator->generateAbsoluteString('public://root-relative'));
// Verify that protocol-relative URL is left untouched.
$this->assertSession()->responseContains($protocol_relative_url);
// Verify that absolute URL is left untouched.
$this->assertSession()->responseContains($absolute_url);
}
}

View File

@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\node\NodeInterface;
/**
* Tests global node CRUD operation permissions.
*
* @group node
*/
class NodeRevisionsAllTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A list of nodes created to be used as starting point of different tests.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* Revision logs of nodes created by the setup method.
*
* @var string[]
*/
protected $revisionLogs;
/**
* An arbitrary user for revision authoring.
*
* @var \Drupal\user\UserInterface
*/
protected $revisionUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create and log in user.
$web_user = $this->drupalCreateUser(
[
'view page revisions',
'revert page revisions',
'delete page revisions',
'edit any page content',
'delete any page content',
]
);
$this->drupalLogin($web_user);
// Create an initial node.
$node = $this->drupalCreateNode();
// Create a user for revision authoring.
// This must be different from user performing revert.
$this->revisionUser = $this->drupalCreateUser();
$nodes = [];
$logs = [];
// Get the original node.
$nodes[] = clone $node;
// Create three revisions.
$revision_count = 3;
for ($i = 0; $i < $revision_count; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
$this->nodes = $nodes;
$this->revisionLogs = $logs;
}
/**
* Creates a new revision for a given node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*
* @return \Drupal\node\NodeInterface
* A node object with up to date revision information.
*/
protected function createNodeRevision(NodeInterface $node) {
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->setNewRevision();
// Ensure the revision author is a different user.
$node->setRevisionUserId($this->revisionUser->id());
$node->save();
return $node;
}
/**
* Checks node revision operations.
*/
public function testRevisions(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$nodes = $this->nodes;
$logs = $this->revisionLogs;
// Get last node for simple checks.
$node = $nodes[3];
// Create and log in user.
$content_admin = $this->drupalCreateUser(
[
'view all revisions',
'revert all revisions',
'delete all revisions',
'edit any page content',
'delete any page content',
]
);
$this->drupalLogin($content_admin);
// Confirm the correct revision text appears on "view revisions" page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($node->body->value);
// Confirm the correct revision log message appears on the "revisions
// overview" page.
$this->drupalGet("node/" . $node->id() . "/revisions");
foreach ($logs as $revision_log) {
$this->assertSession()->pageTextContains($revision_log);
}
// Confirm that this is the current revision.
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the current one.');
// Confirm that revisions revert properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[1]->getTitle()} has been reverted to the revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())}.");
$node_storage->resetCache([$node->id()]);
$reverted_node = $node_storage->load($node->id());
$this->assertSame($nodes[1]->body->value, $reverted_node->body->value, 'Node reverted correctly.');
// Confirm the revision author is the user performing the revert.
$this->assertSame($this->loggedInUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is user performing revert.');
// And that its not the revision author.
$this->assertNotSame($this->revisionUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is not original revision author.');
// Confirm that this is not the current version.
$node = $node_storage->loadRevision($node->getRevisionId());
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the current one.');
// Confirm that the node can still be updated.
$this->drupalGet("node/" . $reverted_node->id() . "/edit");
$this->submitForm(['body[0][value]' => 'We are Drupal.'], 'Save');
$this->assertSession()->pageTextContains('Basic page ' . $reverted_node->getTitle() . ' has been updated.');
$this->assertSession()->pageTextContains('We are Drupal.');
// Confirm revisions delete properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())} of Basic page {$nodes[1]->getTitle()} has been deleted.");
$nids = \Drupal::entityQuery('node')
->allRevisions()
->accessCheck(FALSE)
->condition('nid', $node->id())
->condition('vid', $nodes[1]->getRevisionId())
->execute();
$this->assertCount(0, $nids);
// Set the revision timestamp to an older date to make sure that the
// confirmation message correctly displays the stored revision date.
$old_revision_date = \Drupal::time()->getRequestTime() - 86400;
Database::getConnection()->update('node_revision')
->condition('vid', $nodes[2]->getRevisionId())
->fields([
'revision_timestamp' => $old_revision_date,
])
->execute();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[2]->getTitle()} has been reverted to the revision from {$this->container->get('date.formatter')->format($old_revision_date)}.");
// Create 50 more revisions in order to trigger paging on the revisions
// overview screen.
$node = $nodes[0];
for ($i = 0; $i < 50; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
$node = $this->createNodeRevision($node);
$nodes[] = clone $node;
}
$this->drupalGet('node/' . $node->id() . '/revisions');
// Check that the pager exists.
$this->assertSession()->responseContains('page=1');
// Check that the last revision is displayed on the first page.
$this->assertSession()->pageTextContains(end($logs));
// Go to the second page and check that one of the initial three revisions
// is displayed.
$this->clickLink('Page 2');
$this->assertSession()->pageTextContains($logs[2]);
}
}

View File

@@ -0,0 +1,482 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Database\Database;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
/**
* Tests per-content-type node CRUD operation permissions.
*
* @group node
*/
class NodeRevisionsTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* An array of node revisions.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes;
/**
* Revision log messages.
*
* @var array
*/
protected $revisionLogs;
/**
* {@inheritdoc}
*/
protected static $modules = [
'node',
'contextual',
'datetime',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable additional languages.
ConfigurableLanguage::createFromLangcode('de')->save();
ConfigurableLanguage::createFromLangcode('it')->save();
$field_storage_definition = [
'field_name' => 'untranslatable_string_field',
'entity_type' => 'node',
'type' => 'string',
'cardinality' => 1,
'translatable' => FALSE,
];
$field_storage = FieldStorageConfig::create($field_storage_definition);
$field_storage->save();
$field_definition = [
'field_storage' => $field_storage,
'bundle' => 'page',
];
$field = FieldConfig::create($field_definition);
$field->save();
// Enable translation for page nodes.
\Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE);
// Create and log in user.
$web_user = $this->drupalCreateUser(
[
'view page revisions',
'revert page revisions',
'delete page revisions',
'edit any page content',
'delete any page content',
'access contextual links',
'translate any entity',
'administer content types',
]
);
$this->drupalLogin($web_user);
// Create initial node.
$node = $this->drupalCreateNode();
$nodes = [];
$logs = [];
// Get original node.
$nodes[] = clone $node;
// Create three revisions.
$revision_count = 3;
for ($i = 0; $i < $revision_count; $i++) {
$logs[] = $node->revision_log = $this->randomMachineName(32);
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision();
// Edit the 1st and 2nd revision with a different user.
if ($i < 2) {
$editor = $this->drupalCreateUser();
$node->setRevisionUserId($editor->id());
}
else {
$node->setRevisionUserId($web_user->id());
}
$node->save();
// Make sure we get revision information.
$node = Node::load($node->id());
$nodes[] = clone $node;
}
$this->nodes = $nodes;
$this->revisionLogs = $logs;
}
/**
* Checks node revision related operations.
*/
public function testRevisions(): void {
// Access to the revision page for a node with 1 revision is allowed.
$node = $this->drupalCreateNode();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->statusCodeEquals(200);
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$nodes = $this->nodes;
$logs = $this->revisionLogs;
// Get last node for simple checks.
$node = $nodes[3];
// Confirm the correct revision text appears on "view revisions" page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($node->body->value);
// Confirm the correct log message appears on "revisions overview" page.
$this->drupalGet("node/" . $node->id() . "/revisions");
foreach ($logs as $revision_log) {
$this->assertSession()->pageTextContains($revision_log);
}
// Original author, and editor names should appear on revisions overview.
$web_user = $nodes[0]->revision_uid->entity;
$this->assertSession()->pageTextContains('by ' . $web_user->getAccountName());
$editor = $nodes[2]->revision_uid->entity;
$this->assertSession()->pageTextContains('by ' . $editor->getAccountName());
// Confirm that this is the default revision.
$this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
// Confirm that revisions revert properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[1]->label()} has been reverted to the revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())}.");
$node_storage->resetCache([$node->id()]);
$reverted_node = $node_storage->load($node->id());
$this->assertSame($nodes[1]->body->value, $reverted_node->body->value, 'Node reverted correctly.');
// Confirm the revision author is the user performing the revert.
$this->assertSame($this->loggedInUser->id(), $reverted_node->getRevisionUserId(), 'Node revision author is user performing revert.');
// And that its not the revision author.
$this->assertNotSame($nodes[1]->getRevisionUserId(), $reverted_node->getRevisionUserId(), 'Node revision author is not original revision author.');
// Confirm that this is not the default version.
$node = $node_storage->loadRevision($node->getRevisionId());
$this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
// Confirm revisions delete properly.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revision from {$this->container->get('date.formatter')->format($nodes[1]->getRevisionCreationTime())} of Basic page {$nodes[1]->label()} has been deleted.");
$connection = Database::getConnection();
$nids = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->allRevisions()
->condition('nid', $node->id())
->condition('vid', $nodes[1]->getRevisionId())
->execute();
$this->assertCount(0, $nids);
// Set the revision timestamp to an older date to make sure that the
// confirmation message correctly displays the stored revision date.
$old_revision_date = \Drupal::time()->getRequestTime() - 86400;
$connection->update('node_revision')
->condition('vid', $nodes[2]->getRevisionId())
->fields([
'revision_timestamp' => $old_revision_date,
])
->execute();
$this->drupalGet("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert");
$this->submitForm([], 'Revert');
$this->assertSession()->pageTextContains("Basic page {$nodes[2]->label()} has been reverted to the revision from {$this->container->get('date.formatter')->format($old_revision_date)}.");
// Confirm user is redirected depending on the remaining revisions,
// when a revision is deleted.
$existing_revision_ids = $node_storage->revisionIds($node);
// Delete all revision except last 3.
$remaining_revision_ids = array_slice($existing_revision_ids, -3, 3);
foreach ($existing_revision_ids as $revision_id) {
if (!in_array($revision_id, $remaining_revision_ids)) {
$node_storage->deleteRevision($revision_id);
}
}
// Confirm user was redirected to revisions history page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $remaining_revision_ids[0] . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextContains("Revisions for {$nodes[2]->label()}");
$this->assertSession()->pageTextNotContains($nodes[2]->body->value);
// Confirm user was redirected to the node page.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $remaining_revision_ids[1] . "/delete");
$this->submitForm([], 'Delete');
$this->assertSession()->pageTextNotContains("Revisions for {$nodes[2]->label()}");
$this->assertSession()->pageTextContains($nodes[2]->body->value);
// Make a new revision and set it to not be default.
// This will create a new revision that is not "front facing".
$new_node_revision = clone $node;
$new_body = $this->randomMachineName();
$new_node_revision->body->value = $new_body;
// Save this as a non-default revision.
$new_node_revision->setNewRevision();
$new_node_revision->isDefaultRevision = FALSE;
$new_node_revision->save();
// Verify that revision body text is not present on default version of node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextNotContains($new_body);
// Verify that the new body text is present on the revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $new_node_revision->getRevisionId() . "/view");
$this->assertSession()->pageTextContains($new_body);
// Verify that the non-default revision vid is greater than the default
// revision vid.
$default_revision = $connection->select('node', 'n')
->fields('n', ['vid'])
->condition('nid', $node->id())
->execute()
->fetchCol();
$default_revision_vid = $default_revision[0];
$this->assertGreaterThan($default_revision_vid, $new_node_revision->getRevisionId());
// Create an 'EN' node with a revision log message.
$node = $this->drupalCreateNode();
$node->title = 'Node title in EN';
$node->revision_log = 'Simple revision message (EN)';
$node->save();
$this->drupalGet("node/" . $node->id() . "/revisions");
// Verify revisions is accessible since the type has revisions enabled.
$this->assertSession()->statusCodeEquals(200);
// Check initial revision is shown on the node revisions overview page.
$this->assertSession()->pageTextContains('Simple revision message (EN)');
// Verify that delete operation is inaccessible for the default revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/delete");
$this->assertSession()->statusCodeEquals(403);
// Verify that revert operation is inaccessible for the default revision.
$this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/revert");
$this->assertSession()->statusCodeEquals(403);
// Create a new revision and new log message.
$node = Node::load($node->id());
$node->body->value = 'New text (EN)';
$node->revision_log = 'New revision message (EN)';
$node->setNewRevision();
$node->save();
// Check both revisions are shown on the node revisions overview page.
$this->drupalGet("node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextContains('New revision message (EN)');
// Create an 'EN' node with a revision log message.
$node = $this->drupalCreateNode();
$node->langcode = 'en';
$node->title = 'Node title in EN';
$node->revision_log = 'Simple revision message (EN)';
$node->save();
$this->drupalGet("node/" . $node->id() . "/revisions");
// Verify revisions is accessible since the type has revisions enabled.
$this->assertSession()->statusCodeEquals(200);
// Check initial revision is shown on the node revisions overview page.
$this->assertSession()->pageTextContains('Simple revision message (EN)');
// Add a translation in 'DE' and create a new revision and new log message.
$translation = $node->addTranslation('de');
$translation->title->value = 'Node title in DE';
$translation->body->value = 'New text (DE)';
$translation->revision_log = 'New revision message (DE)';
$translation->setNewRevision();
$translation->save();
// View the revision UI in 'IT', only the original node revision is shown.
$this->drupalGet("it/node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextNotContains('New revision message (DE)');
// View the revision UI in 'DE', only the translated node revision is shown.
$this->drupalGet("de/node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextNotContains('Simple revision message (EN)');
$this->assertSession()->pageTextContains('New revision message (DE)');
// View the revision UI in 'EN', only the original node revision is shown.
$this->drupalGet("node/" . $node->id() . "/revisions");
$this->assertSession()->pageTextContains('Simple revision message (EN)');
$this->assertSession()->pageTextNotContains('New revision message (DE)');
}
/**
* Checks that revisions are correctly saved without log messages.
*/
public function testNodeRevisionWithoutLogMessage(): void {
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
// Create a node with an initial log message.
$revision_log = $this->randomMachineName(10);
$node = $this->drupalCreateNode(['revision_log' => $revision_log]);
// Save over the same revision and explicitly provide an empty log message
// (for example, to mimic the case of a node form submitted with no text in
// the "log message" field), and check that the original log message is
// preserved.
$new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage1';
$node = clone $node;
$node->title = $new_title;
$node->revision_log = '';
$node->setNewRevision(FALSE);
$node->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($new_title);
$node_storage->resetCache([$node->id()]);
$node_revision = $node_storage->load($node->id());
$this->assertEquals($revision_log, $node_revision->revision_log->value, 'After an existing node revision is re-saved without a log message, the original log message is preserved.');
// Create another node with an initial revision log message.
$node = $this->drupalCreateNode(['revision_log' => $revision_log]);
// Save a new node revision without providing a log message, and check that
// this revision has an empty log message.
$new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage2';
$node = clone $node;
$node->title = $new_title;
$node->setNewRevision();
$node->revision_log = NULL;
$node->save();
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($new_title);
$node_storage->resetCache([$node->id()]);
$node_revision = $node_storage->load($node->id());
$this->assertEmpty($node_revision->revision_log->value, 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
}
/**
* Tests the revision translations are correctly reverted.
*/
public function testRevisionTranslationRevert(): void {
// Create a node and a few revisions.
$node = $this->drupalCreateNode(['langcode' => 'en']);
$initial_revision_id = $node->getRevisionId();
$initial_title = $node->label();
$this->createRevisions($node, 2);
// Translate the node and create a few translation revisions.
$translation = $node->addTranslation('it');
$this->createRevisions($translation, 3);
$revert_id = $node->getRevisionId();
$translated_title = $translation->label();
$untranslatable_string = $node->untranslatable_string_field->value;
// Create a new revision for the default translation in-between a series of
// translation revisions.
$this->createRevisions($node, 1);
$default_translation_title = $node->label();
// And create a few more translation revisions.
$this->createRevisions($translation, 2);
$translation_revision_id = $translation->getRevisionId();
// Now revert the a translation revision preceding the last default
// translation revision, and check that the desired value was reverted but
// the default translation value was preserved.
$revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
'node' => $node->id(),
'node_revision' => $revert_id,
'langcode' => 'it',
]);
$this->drupalGet($revert_translation_url);
$this->submitForm([], 'Revert');
/** @var \Drupal\node\NodeStorage $node_storage */
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($translation_revision_id, $node->getRevisionId());
$this->assertEquals($default_translation_title, $node->label());
$this->assertEquals($translated_title, $node->getTranslation('it')->label());
$this->assertNotEquals($untranslatable_string, $node->untranslatable_string_field->value);
$latest_revision_id = $translation->getRevisionId();
// Now revert the a translation revision preceding the last default
// translation revision again, and check that the desired value was reverted
// but the default translation value was preserved. But in addition the
// untranslated field will be reverted as well.
$this->drupalGet($revert_translation_url);
$this->submitForm(['revert_untranslated_fields' => TRUE], 'Revert');
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($latest_revision_id, $node->getRevisionId());
$this->assertEquals($default_translation_title, $node->label());
$this->assertEquals($translated_title, $node->getTranslation('it')->label());
$this->assertEquals($untranslatable_string, $node->untranslatable_string_field->value);
$latest_revision_id = $translation->getRevisionId();
// Now revert the entity revision to the initial one where the translation
// didn't exist.
$revert_url = Url::fromRoute('node.revision_revert_confirm', [
'node' => $node->id(),
'node_revision' => $initial_revision_id,
]);
$this->drupalGet($revert_url);
$this->submitForm([], 'Revert');
$node_storage->resetCache();
/** @var \Drupal\node\NodeInterface $node */
$node = $node_storage->load($node->id());
$this->assertGreaterThan($latest_revision_id, $node->getRevisionId());
$this->assertEquals($initial_title, $node->label());
$this->assertFalse($node->hasTranslation('it'));
}
/**
* Creates a series of revisions for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* The node object.
* @param $count
* The number of revisions to be created.
*/
protected function createRevisions(NodeInterface $node, $count) {
for ($i = 0; $i < $count; $i++) {
$node->title = $this->randomString();
$node->untranslatable_string_field->value = $this->randomString();
$node->setNewRevision(TRUE);
$node->save();
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\NodeType;
/**
* Tests the revision tab display.
*
* This test is similar to NodeRevisionsUITest except that it uses a user with
* the bypass node access permission to make sure that the revision access
* check adds correct cacheability metadata.
*
* @group node
*/
class NodeRevisionsUiBypassAccessTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* User with bypass node access permission.
*
* @var \Drupal\user\Entity\User
*/
protected $editor;
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user.
$this->editor = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
'view page revisions',
'bypass node access',
'access user profiles',
]);
}
/**
* Checks that the Revision tab is displayed correctly.
*/
public function testDisplayRevisionTab(): void {
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalLogin($this->editor);
// Set page revision setting 'create new revision'. This will mean new
// revisions are created by default when the node is edited.
$type = NodeType::load('page');
$type->setNewRevision(TRUE);
$type->save();
// Create the node.
$node = $this->drupalCreateNode();
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals($node->toUrl());
// Verify revisions exist.
$this->assertSession()->linkExists('Revisions');
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Submit the form without changing the checkbox.
$edit = [];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->addressEquals($node->toUrl());
$this->assertSession()->linkExists('Revisions');
}
}

View File

@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
/**
* Tests the UI for controlling node revision behavior.
*
* @group node
*/
class NodeRevisionsUiTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* @var \Drupal\user\Entity\User
*/
protected $editor;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users.
$this->editor = $this->drupalCreateUser([
'administer nodes',
'edit any page content',
'view page revisions',
'access user profiles',
]);
}
/**
* Checks that unchecking 'Create new revision' works when editing a node.
*/
public function testNodeFormSaveWithoutRevision(): void {
$this->drupalLogin($this->editor);
$node_storage = $this->container->get('entity_type.manager')->getStorage('node');
// Set page revision setting 'create new revision'. This will mean new
// revisions are created by default when the node is edited.
$type = NodeType::load('page');
$type->setNewRevision(TRUE);
$type->save();
// Create the node.
$node = $this->drupalCreateNode();
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Uncheck the create new revision checkbox and save the node.
$edit = ['revision' => FALSE];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Load the node again and check the revision is the same as before.
$node_storage->resetCache([$node->id()]);
$node_revision = $node_storage->load($node->id(), TRUE);
$this->assertEquals($node->getRevisionId(), $node_revision->getRevisionId(), "After an existing node is saved with 'Create new revision' unchecked, a new revision is not created.");
// Verify the checkbox is checked on the node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->checkboxChecked('edit-revision');
// Submit the form without changing the checkbox.
$edit = [];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Load the node again and check the revision is different from before.
$node_storage->resetCache([$node->id()]);
$node_revision = $node_storage->load($node->id());
$this->assertNotEquals($node->getRevisionId(), $node_revision->getRevisionId(), "After an existing node is saved with 'Create new revision' checked, a new revision is created.");
}
/**
* Checks HTML double escaping of revision logs.
*/
public function testNodeRevisionDoubleEscapeFix(): void {
$this->drupalLogin($this->editor);
$nodes = [];
// Create the node.
$node = $this->drupalCreateNode();
$username = [
'#theme' => 'username',
'#account' => $this->editor,
];
$editor = \Drupal::service('renderer')->renderInIsolation($username);
// Get original node.
$nodes[] = clone $node;
// Create revision with a random title and body and update variables.
$node->title = $this->randomMachineName();
$node->body = [
'value' => $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->setNewRevision();
$revision_log = 'Revision <em>message</em> with markup.';
$node->revision_log->value = $revision_log;
$node->save();
// Make sure we get revision information.
$node = Node::load($node->id());
$nodes[] = clone $node;
$this->drupalGet('node/' . $node->id() . '/revisions');
// Assert the old revision message.
$date = $this->container->get('date.formatter')->format($nodes[0]->revision_timestamp->value, 'short');
$url = new Url('entity.node.revision', ['node' => $nodes[0]->id(), 'node_revision' => $nodes[0]->getRevisionId()]);
$this->assertSession()->responseContains(Link::fromTextAndUrl($date, $url)->toString() . ' by ' . $editor);
// Assert the current revision message.
$date = $this->container->get('date.formatter')->format($nodes[1]->revision_timestamp->value, 'short');
$this->assertSession()->responseContains($nodes[1]->toLink($date)->toString() . ' by ' . $editor . '<p class="revision-log">' . $revision_log . '</p>');
}
/**
* Checks the Revisions tab.
*/
public function testNodeRevisionsTabWithDefaultRevision(): void {
$this->drupalLogin($this->editor);
// Create the node.
$node = $this->drupalCreateNode();
$storage = \Drupal::entityTypeManager()->getStorage($node->getEntityTypeId());
// Create a new revision based on the default revision.
// Revision 2.
$node = $storage->load($node->id());
$node->setNewRevision(TRUE);
$node->save();
// Revision 3.
$node = $storage->load($node->id());
$node->setNewRevision(TRUE);
$node->save();
// Revision 4.
// Trigger translation changes in order to show the revision.
$node = $storage->load($node->id());
$node->setTitle($this->randomString());
$node->isDefaultRevision(FALSE);
$node->setNewRevision(TRUE);
$node->save();
// Revision 5.
$node = $storage->load($node->id());
$node->isDefaultRevision(FALSE);
$node->setNewRevision(TRUE);
$node->save();
$node_id = $node->id();
$this->drupalGet('node/' . $node_id . '/revisions');
// Verify that the latest affected revision having been a default revision
// is displayed as the current one.
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/1/revert');
// The site may be installed in a subdirectory, so check if the URL is
// contained in the retrieved one.
$this->assertSession()->elementAttributeContains('xpath', '//tr[contains(@class, "revision-current")]/td/a[1]', 'href', '/node/1');
// Verify that the default revision can be an older revision than the latest
// one.
// Assert that the revisions with translations changes are shown.
$this->assertSession()->linkByHrefExists('/node/' . $node_id . '/revisions/4/revert');
// Assert that the revisions without translations changes are filtered out:
// 2, 3 and 5.
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/2/revert');
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/3/revert');
$this->assertSession()->linkByHrefNotExists('/node/' . $node_id . '/revisions/5/revert');
}
/**
* Checks the Revisions tab.
*
* Tests two 'Revisions' local tasks are not added by both Node and
* VersionHistoryLocalTasks.
*
* This can be removed after 'entity.node.version_history' local task is
* removed by https://www.drupal.org/project/drupal/issues/3153559.
*
* @covers node_local_tasks_alter
*/
public function testNodeDuplicateRevisionsTab(): void {
$this->drupalPlaceBlock('local_tasks_block');
$this->drupalLogin($this->editor);
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl('edit-form'));
// There must be exactly one 'Revisions' local task.
$xpath = $this->assertSession()->buildXPathQuery('//a[contains(@href, :href)]', [':href' => $node->toUrl('version-history')->toString()]);
$this->assertSession()->elementsCount('xpath', $xpath, 1);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\filter\Entity\FilterFormat;
use Drupal\views\Tests\ViewTestData;
/**
* Ensures that RSS render cache doesn't interfere with other caches.
*
* Create a node, render that node as a teaser in the RSS feed, ensure that
* the RSS teaser render doesn't contain tags from the default theme.
*
* @group node
*/
class NodeRssCacheTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node_test', 'views', 'node_test_views'];
/**
* {@inheritdoc}
*/
public static $testViews = ['test_node_article_feed'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
ViewTestData::createTestViews(static::class, ['node_test_views']);
// Use bypass node access permission here, because the test class uses
// hook_grants_alter() to deny access to everyone on node_access
// queries.
$user = $this->drupalCreateUser([
'bypass node access',
'access content',
'create article content',
]);
$this->drupalLogin($user);
}
/**
* Ensure the RSS teaser render does not interfere with default theme cache.
*/
public function testNodeRssCacheContent(): void {
// Only the plain_text text format is available by default, which escapes
// all HTML.
FilterFormat::create([
'format' => 'full_html',
'name' => 'Full HTML',
'filters' => [],
])->save();
// Create the test node.
$node = $this->drupalCreateNode([
'type' => 'article',
'promote' => 1,
'title' => 'Article Test Title',
'body' => [
'value' => '<p>Article test text.</p>',
'format' => 'full_html',
],
]);
// Render the node in the RSS feed view as a teaser.
$this->drupalGet('test-node-article-feed');
// Render the teaser normally.
$viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('node');
$build = $viewBuilder->view($node, 'teaser');
$output = $this->container->get('renderer')->renderInIsolation($build);
// Teaser must contain an "<article" tag from the stable9 theme.
$this->assertStringContainsString('<article', (string) $output);
}
}

View File

@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Tests $node->save() for saving content.
*
* @group node
*/
class NodeSaveTest extends NodeTestBase {
/**
* A normal logged in user.
*
* @var \Drupal\user\UserInterface
*/
protected $webUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user that is allowed to post; we'll use this to test the submission.
$web_user = $this->drupalCreateUser(['create article content']);
$this->drupalLogin($web_user);
$this->webUser = $web_user;
}
/**
* Checks whether custom node IDs are saved properly during an import operation.
*
* Workflow:
* - first create a piece of content
* - save the content
* - check if node exists
*/
public function testImport(): void {
// Node ID must be a number that is not in the database.
$nids = \Drupal::entityTypeManager()->getStorage('node')->getQuery()
->accessCheck(FALSE)
->sort('nid', 'DESC')
->range(0, 1)
->execute();
$max_nid = reset($nids);
$test_nid = $max_nid + mt_rand(1000, 1000000);
$title = $this->randomMachineName(8);
$node = [
'title' => $title,
'body' => [['value' => $this->randomMachineName(32)]],
'uid' => $this->webUser->id(),
'type' => 'article',
'nid' => $test_nid,
];
/** @var \Drupal\node\NodeInterface $node */
$node = Node::create($node);
$node->enforceIsNew();
$this->assertEquals($this->webUser->id(), $node->getOwnerId());
$node->save();
// Test the import.
$node_by_nid = Node::load($test_nid);
$this->assertNotEmpty($node_by_nid, 'Node load by node ID.');
$node_by_title = $this->drupalGetNodeByTitle($title);
$this->assertNotEmpty($node_by_title, 'Node load by node title.');
}
/**
* Verifies accuracy of the "created" and "changed" timestamp functionality.
*/
public function testTimestamps(): void {
// Use the default timestamps.
$edit = [
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => $this->randomMachineName(8),
];
Node::create($edit)->save();
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertEquals(\Drupal::time()->getRequestTime(), $node->getCreatedTime(), 'Creating a node sets default "created" timestamp.');
$this->assertEquals(\Drupal::time()->getRequestTime(), $node->getChangedTime(), 'Creating a node sets default "changed" timestamp.');
// Store the timestamps.
$created = $node->getCreatedTime();
$node->save();
$node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
$this->assertEquals($created, $node->getCreatedTime(), 'Updating a node preserves "created" timestamp.');
// Programmatically set the timestamps using hook_ENTITY_TYPE_presave().
$node->title = 'testing_node_presave';
$node->save();
$node = $this->drupalGetNodeByTitle('testing_node_presave', TRUE);
$this->assertEquals(280299600, $node->getCreatedTime(), 'Saving a node uses "created" timestamp set in presave hook.');
$this->assertEquals(979534800, $node->getChangedTime(), 'Saving a node uses "changed" timestamp set in presave hook.');
// Programmatically set the timestamps on the node.
$edit = [
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => $this->randomMachineName(8),
// Sun, 19 Nov 1978 05:00:00 GMT.
'created' => 280299600,
// Drupal 1.0 release.
'changed' => 979534800,
];
Node::create($edit)->save();
$node = $this->drupalGetNodeByTitle($edit['title']);
$this->assertEquals(280299600, $node->getCreatedTime(), 'Creating a node programmatically uses programmatically set "created" timestamp.');
$this->assertEquals(979534800, $node->getChangedTime(), 'Creating a node programmatically uses programmatically set "changed" timestamp.');
// Update the timestamps.
$node->setCreatedTime(979534800);
$node->changed = 280299600;
$node->save();
$node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
$this->assertEquals(979534800, $node->getCreatedTime(), 'Updating a node uses user-set "created" timestamp.');
// Allowing setting changed timestamps is required, see
// Drupal\content_translation\ContentTranslationMetadataWrapper::setChangedTime($timestamp)
// for example.
$this->assertEquals(280299600, $node->getChangedTime(), 'Updating a node uses user-set "changed" timestamp.');
}
/**
* Tests node presave and static node load cache.
*
* This test determines changes in hook_ENTITY_TYPE_presave() and verifies
* that the static node load cache is cleared upon save.
*/
public function testDeterminingChanges(): void {
// Initial creation.
$node = Node::create([
'uid' => $this->webUser->id(),
'type' => 'article',
'title' => 'test_changes',
]);
$node->save();
// Update the node without applying changes.
$node->save();
$this->assertEquals('test_changes', $node->label(), 'No changes have been determined.');
// Apply changes.
$node->title = 'updated';
$node->save();
// The hook implementations node_test_node_presave() and
// node_test_node_update() determine changes and change the title.
$this->assertEquals('updated_presave_update', $node->label(), 'Changes have been determined.');
// Test the static node load cache to be cleared.
$node = Node::load($node->id());
$this->assertEquals('updated_presave', $node->label(), 'Static cache has been cleared.');
}
/**
* Tests saving a node on node insert.
*
* This test ensures that a node has been fully saved when
* hook_ENTITY_TYPE_insert() is invoked, so that the node can be saved again
* in a hook implementation without errors.
*
* @see node_test_node_insert()
*/
public function testNodeSaveOnInsert(): void {
// node_test_node_insert() triggers a save on insert if the title equals
// 'new'.
$node = $this->drupalCreateNode(['title' => 'new']);
$this->assertEquals('Node ' . $node->id(), $node->getTitle(), 'Node saved on node insert.');
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests if the syndicate block is available.
*
* @group node
*/
class NodeSyndicateBlockTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a user and log in.
$admin_user = $this->drupalCreateUser(['administer blocks']);
$this->drupalLogin($admin_user);
}
/**
* Tests that the "Syndicate" block is shown when enabled.
*/
public function testSyndicateBlock(): void {
// Place the "Syndicate" block and confirm that it is rendered.
$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"]/*');
// Verify syndicate block title.
$this->assertSession()->pageTextContains('Subscribe to RSS Feed');
// Tests the syndicate block RSS link rendered at non-front pages.
$this->drupalGet('user');
$this->clickLink('Subscribe to');
$this->assertSession()->addressEquals('rss.xml');
}
}

View File

@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\Tests\BrowserTestBase;
/**
* Sets up page and article content types.
*/
abstract class NodeTestBase extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'datetime'];
/**
* The node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $accessHandler;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create Basic page and Article node types.
if ($this->profile != 'standard') {
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
$this->accessHandler = \Drupal::entityTypeManager()->getAccessControlHandler('node');
}
/**
* Asserts that node access correctly grants or denies access.
*
* @param array $ops
* An associative array of the expected node access grants for the node
* and account, with each key as the name of an operation (e.g. 'view',
* 'delete') and each value a Boolean indicating whether access to that
* operation should be granted.
* @param \Drupal\node\NodeInterface $node
* The node object to check.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
*
* @internal
*/
public function assertNodeAccess(array $ops, NodeInterface $node, AccountInterface $account) {
foreach ($ops as $op => $result) {
$this->assertEquals($this->accessHandler->access($node, $op, $account), $result, $this->nodeAccessAssertMessage($op, $result, $node->language()->getId()));
}
}
/**
* Asserts that node create access correctly grants or denies access.
*
* @param string $bundle
* The node bundle to check access to.
* @param bool $result
* Whether access should be granted or not.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account for which to check access.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*
* @internal
*/
public function assertNodeCreateAccess(string $bundle, bool $result, AccountInterface $account, ?string $langcode = NULL) {
$this->assertEquals($this->accessHandler->createAccess($bundle, $account, ['langcode' => $langcode]), $result, $this->nodeAccessAssertMessage('create', $result, $langcode));
}
/**
* Constructs an assert message to display which node access was tested.
*
* @param string $operation
* The operation to check access for.
* @param bool $result
* Whether access should be granted or not.
* @param string|null $langcode
* (optional) The language code indicating which translation of the node
* to check. If NULL, the untranslated (fallback) access is checked.
*
* @return string
* An assert message string which contains information in plain English
* about the node access permission test that was performed.
*/
public function nodeAccessAssertMessage($operation, $result, $langcode = NULL) {
return new FormattableMarkup(
'Node access returns @result with operation %op, language code %langcode.',
[
'@result' => $result ? 'true' : 'false',
'%op' => $operation,
'%langcode' => !empty($langcode) ? $langcode : 'empty',
]
);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Utility\Html;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
/**
* Tests node title.
*
* @group node
*/
class NodeTitleTest extends NodeTestBase {
use CommentTestTrait;
use AssertBreadcrumbTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['comment', 'views', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with permission to bypass access content.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('page_title_block');
$this->adminUser = $this->drupalCreateUser([
'administer nodes',
'create article content',
'create page content',
'post comments',
]);
$this->drupalLogin($this->adminUser);
$this->addDefaultCommentField('node', 'page');
}
/**
* Creates one node and tests if the node title has the correct value.
*/
public function testNodeTitle(): void {
// Create "Basic page" content with title.
// Add the node to the frontpage so we can test if teaser links are
// clickable.
$settings = [
'title' => $this->randomMachineName(8),
'promote' => 1,
];
$node = $this->drupalCreateNode($settings);
// Test <title> tag.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementTextEquals('xpath', '//title', $node->label() . ' | Drupal');
// Test breadcrumb in comment preview.
$this->assertBreadcrumb('comment/reply/node/' . $node->id() . '/comment', [
'' => 'Home',
'node/' . $node->id() => $node->label(),
]);
// Verify that node preview title is equal to node title.
$this->assertSession()->elementTextEquals('xpath', "//article/h2/a/span", $node->label());
// Test node title is clickable on teaser list (/node).
$this->drupalGet('node');
$this->clickLink($node->label());
// Test edge case where node title is set to 0.
$settings = [
'title' => 0,
];
$node = $this->drupalCreateNode($settings);
// Test that 0 appears as <title>.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->titleEquals('0 | Drupal');
// Test that 0 appears in the template <h1>.
$this->assertSession()->elementTextEquals('xpath', '//h1', '0');
// Test edge case where node title contains special characters.
$edge_case_title = 'article\'s "title".';
$settings = [
'title' => $edge_case_title,
];
$node = $this->drupalCreateNode($settings);
// Test that the title appears as <title>. The title will be escaped on the
// the page.
$edge_case_title_escaped = Html::escape($edge_case_title);
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseContains('<title>' . $edge_case_title_escaped . ' | Drupal</title>');
// Test that the title appears as <title> when reloading the node page.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->responseContains('<title>' . $edge_case_title_escaped . ' | Drupal</title>');
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Component\Utility\Html;
/**
* Tests that dangerous tags in the node title are escaped.
*
* @group node
*/
class NodeTitleXSSTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests XSS functionality with a node entity.
*/
public function testNodeTitleXSS(): void {
// Prepare a user to do the stuff.
$web_user = $this->drupalCreateUser([
'create page content',
'edit any page content',
]);
$this->drupalLogin($web_user);
$xss = '<script>alert("xss")</script>';
$title = $xss . $this->randomMachineName();
$edit = [];
$edit['title[0][value]'] = $title;
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Verify that harmful tags are escaped when previewing a node.
$this->assertSession()->responseNotContains($xss);
$settings = ['title' => $title];
$node = $this->drupalCreateNode($settings);
$this->drupalGet('node/' . $node->id());
// Titles should be escaped.
$this->assertSession()->responseContains('<title>' . Html::escape($title) . ' | Drupal</title>');
$this->assertSession()->responseNotContains($xss);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->responseNotContains($xss);
}
}

View File

@@ -0,0 +1,619 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
use Drupal\Tests\language\Traits\LanguageTestTrait;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the Node Translation UI.
*
* @group node
* @group #slow
*/
class NodeTranslationUITest extends ContentTranslationUITestBase {
use LanguageTestTrait;
use CommentTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $defaultCacheContexts = [
'theme',
'timezone',
'url.query_args:_wrapper_format',
'url.site',
'user.permissions',
];
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'block',
'language',
'content_translation',
'node',
'field_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
$this->entityTypeId = 'node';
$this->bundle = 'article';
parent::setUp();
// Create the bundle.
$this->drupalCreateContentType(['type' => 'article', 'title' => 'Article']);
$this->doSetup();
// Ensure the help message is shown even with prefixed paths.
$this->drupalPlaceBlock('help_block', ['region' => 'content']);
// Display the language selector.
static::enableBundleTranslation('node', 'article');
$this->drupalLogin($this->translator);
}
/**
* Tests the basic translation UI.
*/
public function testTranslationUI(): void {
parent::testTranslationUI();
$this->doUninstallTest();
}
/**
* Tests changing the published status on a node without fields.
*/
public function testPublishedStatusNoFields(): void {
// Test changing the published status of an article without fields.
$this->drupalLogin($this->administrator);
// Delete all fields.
$this->drupalGet('admin/structure/types/manage/article/fields');
$this->drupalGet('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete');
$this->submitForm([], 'Delete');
// Add a node.
$default_langcode = $this->langcodes[0];
$values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]];
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
// Add a content translation.
$langcode = 'fr';
$language = ConfigurableLanguage::load($langcode);
$values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
$entity_type_id = $entity->getEntityTypeId();
$add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
$entity->getEntityTypeId() => $entity->id(),
'source' => $default_langcode,
'target' => $langcode,
], ['language' => $language]);
$edit = $this->getEditValues($values, $langcode);
$edit['status[value]'] = FALSE;
$this->drupalGet($add_url);
$this->submitForm($edit, 'Save (this translation)');
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$translation = $entity->getTranslation($langcode);
// Make sure we unpublished the node correctly.
$this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
}
/**
* {@inheritdoc}
*/
protected function getTranslatorPermissions() {
return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
}
/**
* {@inheritdoc}
*/
protected function getEditorPermissions() {
return ['administer nodes', 'create article content'];
}
/**
* {@inheritdoc}
*/
protected function getAdministratorPermissions() {
return array_merge(parent::getAdministratorPermissions(), ['access administration pages', 'administer content types', 'administer node fields', 'access content overview', 'bypass node access', 'administer languages', 'administer themes', 'view the administration theme']);
}
/**
* {@inheritdoc}
*/
protected function getNewEntityValues($langcode) {
return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
}
/**
* {@inheritdoc}
*/
protected function doTestPublishedStatus() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$statuses = [
TRUE,
FALSE,
];
foreach ($statuses as $index => $value) {
// (Un)publish the node translations and check that the translation
// statuses are (un)published accordingly.
foreach ($this->langcodes as $langcode) {
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url, $options);
$this->submitForm([
'status[value]' => $value,
], 'Save' . $this->getFormSubmitSuffix($entity, $langcode));
}
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
foreach ($this->langcodes as $langcode) {
// The node is created as unpublished thus we switch to the published
// status first.
$status = !$index;
$translation = $entity->getTranslation($langcode);
$this->assertEquals($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
}
}
}
/**
* {@inheritdoc}
*/
protected function doTestAuthoringInfo() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$values = [];
// Post different base field information for each translation.
foreach ($this->langcodes as $langcode) {
$user = $this->drupalCreateUser();
$values[$langcode] = [
'uid' => $user->id(),
'created' => \Drupal::time()->getRequestTime() - mt_rand(0, 1000),
'sticky' => (bool) mt_rand(0, 1),
'promote' => (bool) mt_rand(0, 1),
];
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = $this->container->get('date.formatter');
$edit = [
'uid[0][target_id]' => $user->getAccountName(),
'created[0][value][date]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'Y-m-d'),
'created[0][value][time]' => $date_formatter->format($values[$langcode]['created'], 'custom', 'H:i:s'),
'sticky[value]' => $values[$langcode]['sticky'],
'promote[value]' => $values[$langcode]['promote'],
];
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url, $options);
$this->submitForm($edit, $this->getFormSubmitAction($entity, $langcode));
}
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
foreach ($this->langcodes as $langcode) {
$translation = $entity->getTranslation($langcode);
$metadata = $this->manager->getTranslationMetadata($translation);
$this->assertEquals($values[$langcode]['uid'], $metadata->getAuthor()->id(), 'Translation author correctly stored.');
$this->assertEquals($values[$langcode]['created'], $metadata->getCreatedTime(), 'Translation date correctly stored.');
$this->assertEquals($values[$langcode]['sticky'], $translation->isSticky(), 'Sticky of Translation correctly stored.');
$this->assertEquals($values[$langcode]['promote'], $translation->isPromoted(), 'Promoted of Translation correctly stored.');
}
}
/**
* Tests that translation page inherits admin status of edit page.
*/
public function testTranslationLinkTheme(): void {
$this->drupalLogin($this->administrator);
$article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
// 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');
// Verify that translation uses the admin theme if edit is admin.
$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');
// Verify that translation uses the frontend theme if edit is frontend.
$this->assertSession()->responseNotContains('core/themes/claro/css/base/elements.css');
// Assert presence of translation page itself (vs. DisabledBundle below).
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests that no metadata is stored for a disabled bundle.
*/
public function testDisabledBundle(): void {
// Create a bundle that does not have translation enabled.
$disabledBundle = $this->randomMachineName();
$this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]);
// Create a node for each bundle.
$node = $this->drupalCreateNode([
'type' => $this->bundle,
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
// Make sure that nothing was inserted into the {content_translation} table.
$nids = \Drupal::entityQueryAggregate('node')
->aggregate('nid', 'COUNT')
->accessCheck(FALSE)
->condition('type', $this->bundle)
->conditionAggregate('nid', 'COUNT', 2, '>=')
->groupBy('nid')
->execute();
$this->assertCount(0, $nids);
// Ensure the translation tab is not accessible.
$this->drupalGet('node/' . $node->id() . '/translations');
$this->assertSession()->statusCodeEquals(403);
}
/**
* Tests that translations are rendered properly.
*/
public function testTranslationRendering(): void {
// Add a comment field to the article content type.
\Drupal::service('module_installer')->install(['comment']);
$this->addDefaultCommentField('node', 'article');
// Add 'post comments' permission to the authenticated role.
$role = Role::load(RoleInterface::AUTHENTICATED_ID);
$role->grantPermission('post comments')->save();
$default_langcode = $this->langcodes[0];
$values[$default_langcode] = $this->getNewEntityValues($default_langcode);
$this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
$node = \Drupal::entityTypeManager()->getStorage($this->entityTypeId)->load($this->entityId);
$node->setPromoted(TRUE);
// Create translations.
foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) {
$values[$langcode] = $this->getNewEntityValues($langcode);
$translation = $node->addTranslation($langcode, $values[$langcode]);
// Publish and promote the translation to frontpage.
$translation->setPromoted(TRUE);
$translation->setPublished();
}
$node->save();
// Test that the frontpage view displays the correct translations.
\Drupal::service('module_installer')->install(['views'], TRUE);
$this->rebuildContainer();
$this->doTestTranslations('node', $values);
// Enable the translation language renderer.
$view = \Drupal::entityTypeManager()->getStorage('view')->load('frontpage');
$display = &$view->getDisplay('default');
$display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***';
$view->save();
// Need to check from the beginning, including the base_path, in the URL
// since the pattern for the default language might be a substring of
// the strings for other languages.
$base_path = base_path();
// Check the frontpage for 'Read more' links to each translation.
// See also assertTaxonomyPage() in NodeAccessBaseTableTest.
$node_href = 'node/' . $node->id();
foreach ($this->langcodes as $langcode) {
$this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
$num_match_found = 0;
if ($langcode == 'en') {
// Site default language does not have langcode prefix in the URL.
$expected_href = $base_path . $node_href;
}
else {
$expected_href = $base_path . $langcode . '/' . $node_href;
}
$pattern = '|^' . $expected_href . '$|';
foreach ($this->xpath("//a[text()='Read more']") as $link) {
if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
$num_match_found++;
}
}
$this->assertSame(1, $num_match_found, 'There is 1 Read more link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
}
// Check the frontpage for 'Add new comment' links that include the
// language.
$comment_form_href = 'node/' . $node->id() . '#comment-form';
foreach ($this->langcodes as $langcode) {
$this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
$num_match_found = 0;
if ($langcode == 'en') {
// Site default language does not have langcode prefix in the URL.
$expected_href = $base_path . $comment_form_href;
}
else {
$expected_href = $base_path . $langcode . '/' . $comment_form_href;
}
$pattern = '|^' . $expected_href . '$|';
foreach ($this->xpath("//a[text()='Add new comment']") as $link) {
if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
$num_match_found++;
}
}
$this->assertSame(1, $num_match_found, 'There is 1 Add new comment link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
}
// Test that the node page displays the correct translations.
$this->doTestTranslations('node/' . $node->id(), $values);
// Test that the node page has the correct alternate hreflang links.
$this->doTestAlternateHreflangLinks($node);
}
/**
* Tests that the given path displays the correct translation values.
*
* @param string $path
* The path to be tested.
* @param array $values
* The translation values to be found.
*/
protected function doTestTranslations($path, array $values) {
$languages = $this->container->get('language_manager')->getLanguages();
foreach ($this->langcodes as $langcode) {
$this->drupalGet($path, ['language' => $languages[$langcode]]);
$this->assertSession()->pageTextContains($values[$langcode]['title'][0]['value']);
}
}
/**
* Tests that the given path provides the correct alternate hreflang links.
*
* @param \Drupal\node\Entity\Node $node
* The node to be tested.
*/
protected function doTestAlternateHreflangLinks(Node $node) {
$url = $node->toUrl();
$languages = $this->container->get('language_manager')->getLanguages();
$url->setAbsolute();
$urls = [];
$translations = [];
foreach ($this->langcodes as $langcode) {
$language_url = clone $url;
$urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
$translations[$langcode] = $node->getTranslation($langcode);
}
foreach ($this->langcodes as $langcode) {
// Skip unpublished translations.
if ($translations[$langcode]->isPublished()) {
$this->drupalGet($urls[$langcode]);
foreach ($urls as $alternate_langcode => $language_url) {
// Retrieve desired link elements from the HTML head.
$xpath = $this->assertSession()->buildXPathQuery('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]', [
':href' => $language_url->toString(),
':hreflang' => $alternate_langcode,
]);
if ($translations[$alternate_langcode]->isPublished()) {
// Verify that the node translation has the correct alternate
// hreflang link for the alternate langcode.
$this->assertSession()->elementExists('xpath', $xpath);
}
else {
// Verify that the node translation does not have an alternate
// hreflang link for the alternate langcode.
$this->assertSession()->elementNotExists('xpath', $xpath);
}
}
}
}
}
/**
* {@inheritdoc}
*/
protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
if (!$entity->isNew() && $entity->isTranslatable()) {
$translations = $entity->getTranslationLanguages();
if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) {
return ' ' . ($field->isTranslatable() ? '(this translation)' : '(all translations)');
}
}
return '';
}
/**
* Tests uninstalling content_translation.
*/
protected function doUninstallTest() {
// Delete all the nodes so there is no data.
$nodes = Node::loadMultiple();
foreach ($nodes as $node) {
$node->delete();
}
$language_count = count(\Drupal::configFactory()->listAll('language.content_settings.'));
\Drupal::service('module_installer')->uninstall(['content_translation']);
$this->rebuildContainer();
$this->assertCount($language_count, \Drupal::configFactory()->listAll('language.content_settings.'), 'Languages have been fixed rather than deleted during content_translation uninstall.');
}
/**
* {@inheritdoc}
*/
protected function doTestTranslationEdit() {
$storage = $this->container->get('entity_type.manager')
->getStorage($this->entityTypeId);
$storage->resetCache([$this->entityId]);
$entity = $storage->load($this->entityId);
$languages = $this->container->get('language_manager')->getLanguages();
$type_name = node_get_type_label($entity);
foreach ($this->langcodes as $langcode) {
// We only want to test the title for non-english translations.
if ($langcode != 'en') {
$options = ['language' => $languages[$langcode]];
$url = $entity->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertSession()->pageTextContains("Edit {$type_name} {$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]");
}
}
}
/**
* Tests that revision translations are rendered properly.
*/
public function testRevisionTranslationRendering(): void {
$storage = \Drupal::entityTypeManager()->getStorage('node');
// Create a node.
$nid = $this->createEntity(['title' => 'First rev en title'], 'en');
$node = $storage->load($nid);
$original_revision_id = $node->getRevisionId();
// Add a French translation.
$translation = $node->addTranslation('fr');
$translation->title = 'First rev fr title';
$translation->setNewRevision(FALSE);
$translation->save();
// Create a new revision.
$node->title = 'Second rev en title';
$node->setNewRevision(TRUE);
$node->save();
// Get an English view of this revision.
$original_revision = $storage->loadRevision($original_revision_id);
$original_revision_url = $original_revision->toUrl('revision')->toString();
// Should be different from regular node URL.
$this->assertNotSame($original_revision_url, $original_revision->toUrl()->toString());
$this->drupalGet($original_revision_url);
$this->assertSession()->statusCodeEquals(200);
// Contents should be in English, of correct revision.
$this->assertSession()->pageTextContains('First rev en title');
$this->assertSession()->pageTextNotContains('First rev fr title');
// Get a French view.
$url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
// Should have different URL from English.
$this->assertNotSame($url_fr, $original_revision->toUrl()->toString());
$this->assertNotSame($url_fr, $original_revision_url);
$this->drupalGet($url_fr);
$this->assertSession()->statusCodeEquals(200);
// Contents should be in French, of correct revision.
$this->assertSession()->pageTextContains('First rev fr title');
$this->assertSession()->pageTextNotContains('First rev en title');
}
/**
* Tests title is not escaped (but XSS-filtered) for details form element.
*/
public function testDetailsTitleIsNotEscaped(): void {
// Create an image field.
\Drupal::service('module_installer')->install(['image']);
FieldStorageConfig::create([
'entity_type' => 'node',
'field_name' => 'field_image',
'type' => 'image',
])->save();
FieldConfig::create([
'entity_type' => 'node',
'field_name' => 'field_image',
'bundle' => 'article',
'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');
// Enable the display of the image field.
EntityFormDisplay::load('node.article.default')
->setComponent('field_image', ['region' => 'content'])->save();
// Make the image field non-translatable.
static::setFieldTranslatable('node', 'article', 'field_image', FALSE);
// Create a node.
$nid = $this->createEntity(['title' => 'Node with multi-value image field en title'], 'en');
// Add a French translation and assert the title markup is not escaped.
$this->drupalGet("node/$nid/translations/add/en/fr");
$markup = 'Image <span class="translation-entity-all-languages">(all languages)</span>';
$this->assertSession()->assertNoEscaped($markup);
$this->assertSession()->responseContains($markup);
}
/**
* Test that when content is language neutral, it uses interface language.
*
* When language neutral content is displayed on interface language, it should
* consider the interface language for creating the content link.
*/
public function testUrlPrefixOnLanguageNeutralContent(): void {
$this->drupalLogin($this->administrator);
$neutral_langcodes = [
LanguageInterface::LANGCODE_NOT_SPECIFIED,
];
foreach ($neutral_langcodes as $langcode) {
$article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $langcode]);
$this->drupalGet("{$this->langcodes[1]}/admin/content");
$this->assertSession()->linkByHrefExists("{$this->langcodes[1]}/node/{$article->id()}");
$this->drupalGet("{$this->langcodes[2]}/admin/content");
$this->assertSession()->linkByHrefExists("{$this->langcodes[2]}/node/{$article->id()}");
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\Core\Language\LanguageInterface;
/**
* Tests node type initial language settings.
*
* @group node
*/
class NodeTypeInitialLanguageTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['language', 'field_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
'administer node fields',
'administer node form display',
'administer node display',
'administer languages',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the node type initial language defaults, and modifies them.
*
* The default initial language must be the site's default, and the language
* locked option must be on.
*/
public function testNodeTypeInitialLanguageDefaults(): void {
$this->drupalGet('admin/structure/types/manage/article');
$this->assertTrue($this->assertSession()->optionExists('edit-language-configuration-langcode', LanguageInterface::LANGCODE_SITE_DEFAULT)->isSelected());
$this->assertSession()->checkboxNotChecked('edit-language-configuration-language-alterable');
// Tests if the language field cannot be rearranged on the manage fields tab.
$this->drupalGet('admin/structure/types/manage/article/fields');
$this->assertSession()->elementNotExists('xpath', '//*[@id="field-overview"]/*[@id="language"]');
// Verify that language is not selectable on node add page by default.
$this->drupalGet('node/add/article');
$this->assertSession()->fieldNotExists('langcode');
// Adds a new language and set it as default.
$edit = [
'predefined_langcode' => 'hu',
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
$edit = [
'site_default_language' => 'hu',
];
$this->drupalGet('admin/config/regional/language');
$this->submitForm($edit, 'Save configuration');
// Tests the initial language after changing the site default language.
// First unhide the language selector.
$edit = [
'language_configuration[language_alterable]' => TRUE,
];
$this->drupalGet('admin/structure/types/manage/article');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/article');
// Ensure that the language is selectable on node add page when language
// not hidden.
$this->assertSession()->fieldExists('langcode[0][value]');
$this->assertTrue($this->assertSession()->optionExists('edit-langcode-0-value', 'hu')->isSelected());
// Tests if the language field can be rearranged on the manage form display
// tab.
$this->drupalGet('admin/structure/types/manage/article/form-display');
$this->assertSession()->elementExists('xpath', '//*[@id="langcode"]');
// Tests if the language field can be rearranged on the manage display tab.
$this->drupalGet('admin/structure/types/manage/article/display');
$this->assertSession()->elementExists('xpath', '//*[@id="langcode"]');
// Tests if the language field is hidden by default.
$this->assertTrue($this->assertSession()->optionExists('edit-fields-langcode-region', 'hidden')->isSelected());
// Changes the initial language settings.
$edit = [
'language_configuration[langcode]' => 'en',
];
$this->drupalGet('admin/structure/types/manage/article');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/article');
$this->assertTrue($this->assertSession()->optionExists('edit-langcode-0-value', 'en')->isSelected());
}
/**
* Tests language field visibility features.
*/
public function testLanguageFieldVisibility(): void {
// Creates a node to test Language field visibility feature.
$edit = [
'title[0][value]' => $this->randomMachineName(8),
'body[0][value]' => $this->randomMachineName(16),
];
$this->drupalGet('node/add/article');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($edit['title[0][value]']);
$this->assertNotEmpty($node, 'Node found in database.');
// Loads node page and check if Language field is hidden by default.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementNotExists('xpath', '//div[@id="field-language-display"]/div');
// Configures Language field formatter and check if it is saved.
$edit = [
'fields[langcode][type]' => 'language',
'fields[langcode][region]' => 'content',
];
$this->drupalGet('admin/structure/types/manage/article/display');
$this->submitForm($edit, 'Save');
$this->drupalGet('admin/structure/types/manage/article/display');
$this->assertTrue($this->assertSession()->optionExists('edit-fields-langcode-type', 'language')->isSelected());
// Loads node page and check if Language field is shown.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->elementExists('xpath', '//div[@id="field-language-display"]/div');
}
}

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;
use Drupal\Core\Url;
use Drupal\Tests\system\Functional\Menu\AssertBreadcrumbTrait;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Ensures that node type functions work correctly.
*
* @group node
* @group #slow
*/
class NodeTypeTest extends NodeTestBase {
use AssertBreadcrumbTrait;
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['field_ui', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Ensures that node type functions (node_type_get_*) work correctly.
*
* Load available node types and validate the returned data.
*/
public function testNodeTypeGetFunctions(): void {
$node_types = NodeType::loadMultiple();
$node_names = node_type_get_names();
$this->assertTrue(isset($node_types['article']), 'Node type article is available.');
$this->assertTrue(isset($node_types['page']), 'Node type basic page is available.');
$this->assertEquals($node_names['article'], $node_types['article']->label(), 'Correct node type base has been returned.');
$article = NodeType::load('article');
$this->assertEquals($node_types['article'], $article, 'Correct node type has been returned.');
$this->assertEquals($node_types['article']->label(), $article->label(), 'Correct node type name has been returned.');
}
/**
* Tests creating a content type programmatically and via a form.
*/
public function testNodeTypeCreation(): void {
// Create a content type programmatically.
$type = $this->drupalCreateContentType();
$type_exists = (bool) NodeType::load($type->id());
$this->assertTrue($type_exists, 'The new content type has been created in the database.');
// Log in a test user.
$web_user = $this->drupalCreateUser([
'create ' . $type->label() . ' content',
]);
$this->drupalLogin($web_user);
$this->drupalGet('node/add/' . $type->id());
$this->assertSession()->statusCodeEquals(200);
// Create a content type via the user interface.
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
]);
$this->drupalLogin($web_user);
$this->drupalGet('node/add');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:node_type_list');
$this->assertCacheContext('user.permissions');
$elements = $this->cssSelect('dl dt');
$this->assertCount(3, $elements);
$edit = [
'name' => 'foo',
'title_label' => 'title for foo',
'type' => 'foo',
];
$this->drupalGet('admin/structure/types/add');
$this->submitForm($edit, 'Save and manage fields');
// Asserts that form submit redirects to the expected manage fields page.
$this->assertSession()->addressEquals('admin/structure/types/manage/' . $edit['name'] . '/fields');
$type_exists = (bool) NodeType::load('foo');
$this->assertTrue($type_exists, 'The new content type has been created in the database.');
$this->drupalGet('node/add');
$elements = $this->cssSelect('dl dt');
$this->assertCount(4, $elements);
}
/**
* Tests editing a node type using the UI.
*/
public function testNodeTypeEditing(): void {
$assert = $this->assertSession();
$this->drupalPlaceBlock('system_breadcrumb_block');
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
'administer node fields',
]);
$this->drupalLogin($web_user);
$field = FieldConfig::loadByName('node', 'page', 'body');
$this->assertEquals('Body', $field->getLabel(), 'Body field was found.');
// Verify that title and body fields are displayed.
$this->drupalGet('node/add/page');
$assert->pageTextContains('Title');
$assert->pageTextContains('Body');
// Rename the title field.
$edit = [
'title_label' => 'Foo',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add/page');
$assert->pageTextContains('Foo');
$assert->pageTextNotContains('Title');
// Change the name and the description.
$edit = [
'name' => 'Bar',
'description' => 'Lorem ipsum.',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/add');
$assert->pageTextContains('Bar');
$assert->pageTextContains('Lorem ipsum');
$this->clickLink('Bar');
$assert->pageTextContains('Foo');
$assert->pageTextContains('Body');
// Change the name through the API
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = NodeType::load('page');
$node_type->set('name', 'NewBar');
$node_type->save();
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
$bundle_info = \Drupal::service('entity_type.bundle.info');
$node_bundles = $bundle_info->getBundleInfo('node');
$this->assertEquals('NewBar', $node_bundles['page']['label'], 'Node type bundle cache is updated');
// Remove the body field.
$this->drupalGet('admin/structure/types/manage/page/fields/node.page.body/delete');
$this->submitForm([], 'Delete');
// Resave the settings for this type.
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm([], 'Save');
$front_page_path = Url::fromRoute('<front>')->toString();
$this->assertBreadcrumb('admin/structure/types/manage/page/fields', [
$front_page_path => 'Home',
'admin/structure/types' => 'Content types',
'admin/structure/types/manage/page' => 'NewBar',
]);
// Check that the body field doesn't exist.
$this->drupalGet('node/add/page');
$assert->pageTextNotContains('Body');
}
/**
* Tests deleting a content type that still has content.
*/
public function testNodeTypeDeletion(): void {
$this->drupalPlaceBlock('page_title_block');
// Create a content type programmatically.
$type = $this->drupalCreateContentType();
// Log in a test user.
$web_user = $this->drupalCreateUser([
'bypass node access',
'administer content types',
]);
$this->drupalLogin($web_user);
// Add a new node of this type.
$node = $this->drupalCreateNode(['type' => $type->id()]);
// Attempt to delete the content type, which should not be allowed.
$this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
$this->assertSession()->pageTextContains("{$type->label()} is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the {$type->label()} content.");
$this->assertSession()->pageTextNotContains('This action cannot be undone.');
// Delete the node.
$node->delete();
// Attempt to delete the content type, which should now be allowed.
$this->drupalGet('admin/structure/types/manage/' . $type->label() . '/delete');
$this->assertSession()->pageTextContains("Are you sure you want to delete the content type {$type->label()}?");
$this->assertSession()->pageTextContains('This action cannot be undone.');
// Test that a locked node type could not be deleted.
$this->container->get('module_installer')->install(['node_test_config']);
// Lock the default node type.
$locked = \Drupal::state()->get('node.type.locked');
$locked['default'] = 'default';
\Drupal::state()->set('node.type.locked', $locked);
// Call to flush all caches after installing the node_test_config module in
// the same way installing a module through the UI does.
$this->resetAll();
$this->drupalGet('admin/structure/types/manage/default');
$this->assertSession()->linkNotExists('Delete');
$this->drupalGet('admin/structure/types/manage/default/delete');
$this->assertSession()->statusCodeEquals(403);
$this->container->get('module_installer')->uninstall(['node_test_config']);
$this->container = \Drupal::getContainer();
unset($locked['default']);
\Drupal::state()->set('node.type.locked', $locked);
$this->drupalGet('admin/structure/types/manage/default');
$this->clickLink('Delete');
$this->assertSession()->statusCodeEquals(200);
$this->submitForm([], 'Delete');
$this->assertFalse((bool) NodeType::load('default'), 'Node type with machine default deleted.');
}
/**
* Tests operations from Field UI and User modules for content types.
*/
public function testNodeTypeOperations(): void {
// Create an admin user who can only manage node fields.
$admin_user_1 = $this->drupalCreateUser([
'administer content types',
'administer node fields',
'administer permissions',
]);
$this->drupalLogin($admin_user_1);
// Test that the user only sees the actions available to them.
$this->drupalGet('admin/structure/types');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/fields');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/permissions');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/display');
// Create another admin user who can manage node fields display.
$admin_user_2 = $this->drupalCreateUser([
'administer content types',
'administer node display',
]);
$this->drupalLogin($admin_user_2);
// Test that the user only sees the actions available to them.
$this->drupalGet('admin/structure/types');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/fields');
$this->assertSession()->linkByHrefNotExists('admin/structure/types/manage/article/permissions');
$this->assertSession()->linkByHrefExists('admin/structure/types/manage/article/display');
}
/**
* Tests for when there are no content types defined.
*/
public function testNodeTypeNoContentType(): void {
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info */
$bundle_info = \Drupal::service('entity_type.bundle.info');
$this->assertCount(2, $bundle_info->getBundleInfo('node'), 'The bundle information service has 2 bundles for the Node entity type.');
$web_user = $this->drupalCreateUser(['administer content types']);
$this->drupalLogin($web_user);
// Delete 'article' bundle.
$this->drupalGet('admin/structure/types/manage/article/delete');
$this->submitForm([], 'Delete');
// Delete 'page' bundle.
$this->drupalGet('admin/structure/types/manage/page/delete');
$this->submitForm([], 'Delete');
// 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");
$this->assertSession()->linkByHrefExists(Url::fromRoute('node.type_add')->toString());
$bundle_info->clearCachedBundles();
$this->assertCount(0, $bundle_info->getBundleInfo('node'), 'The bundle information service has 0 bundles for the Node entity type.');
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
/**
* Ensures that node types translation work correctly.
*
* Note that the child site is installed in French; therefore, when making
* assertions on translated text it is important to provide a langcode. This
* ensures the asserts pass regardless of the Drupal version.
*
* @group node
*/
class NodeTypeTranslationTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'block',
'config_translation',
'field_ui',
'node',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The default language code to use in this test.
*
* @var array
*/
protected $defaultLangcode = 'fr';
/**
* Languages to enable.
*
* @var array
*/
protected $additionalLangcodes = ['es'];
/**
* Administrator user for tests.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$admin_permissions = [
'administer content types',
'bypass node access',
'administer node fields',
'administer languages',
'administer site configuration',
'administer themes',
'translate configuration',
];
// Create and log in user.
$this->adminUser = $this->drupalCreateUser($admin_permissions);
// Add languages.
foreach ($this->additionalLangcodes as $langcode) {
ConfigurableLanguage::createFromLangcode($langcode)->save();
}
}
/**
* {@inheritdoc}
*
* Install Drupal in a language other than English for this test. This is not
* needed to test the node type translation itself but acts as a regression
* test.
*
* @see https://www.drupal.org/node/2584603
*/
protected function installParameters() {
$parameters = parent::installParameters();
$parameters['parameters']['langcode'] = $this->defaultLangcode;
// Create an empty po file so we don't attempt to download one from
// localize.drupal.org. It does not need to match the version exactly as the
// multi-lingual system will fallback.
\Drupal::service('file_system')->mkdir($this->publicFilesDirectory . '/translations', NULL, TRUE);
file_put_contents($this->publicFilesDirectory . "/translations/drupal-8.0.0.{$this->defaultLangcode}.po", '');
return $parameters;
}
/**
* Tests the node type translation.
*/
public function testNodeTypeTranslation(): void {
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
// Translate the node type name.
$langcode = $this->additionalLangcodes[0];
$translated_name = $langcode . '-' . $name;
$edit = [
"translation[config_names][node.type.$type][name]" => $translated_name,
];
// Edit the title label to avoid having an exception when we save the translation.
$this->drupalGet("admin/structure/types/manage/{$type}/translate/{$langcode}/add");
$this->submitForm($edit, 'Save translation');
// Check the name is translated without admin theme for editing.
$this->drupalGet('admin/appearance');
$this->submitForm(['use_admin_theme' => '0'], 'Save configuration');
$this->drupalGet("$langcode/node/add/$type");
// This is a Spanish page, so ensure the text asserted is translated in
// Spanish and not French by adding the langcode option.
$this->assertSession()->responseContains(t('Create @name', ['@name' => $translated_name], ['langcode' => $langcode]));
// Check the name is translated with admin theme for editing.
$this->drupalGet('admin/appearance');
$this->submitForm(['use_admin_theme' => '1'], 'Save configuration');
$this->drupalGet("$langcode/node/add/$type");
// This is a Spanish page, so ensure the text asserted is translated in
// Spanish and not French by adding the langcode option.
$this->assertSession()->responseContains(t('Create @name', ['@name' => $translated_name], ['langcode' => $langcode]));
}
/**
* Tests the node type title label translation.
*/
public function testNodeTypeTitleLabelTranslation(): void {
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalLogin($this->adminUser);
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
$langcode = $this->additionalLangcodes[0];
// Edit the title label for it to be displayed on the translation form.
$this->drupalGet("admin/structure/types/manage/{$type}");
$this->submitForm(['title_label' => 'Edited title'], 'Save');
// Assert that the title label is displayed on the translation form with the right value.
$this->drupalGet("admin/structure/types/manage/$type/translate/$langcode/add");
$this->assertSession()->pageTextContains('Edited title');
// Translate the title label.
$this->submitForm(["translation[config_names][core.base_field_override.node.$type.title][label]" => 'Translated title'], 'Save translation');
// Assert that the right title label is displayed on the node add form. The
// translations are created in this test; therefore, the assertions do not
// use t(). If t() were used then the correct langcodes would need to be
// provided.
$this->drupalGet("node/add/$type");
$this->assertSession()->pageTextContains('Edited title');
$this->drupalGet("$langcode/node/add/$type");
$this->assertSession()->pageTextContains('Translated title');
// Add an email field.
$this->drupalGet("admin/structure/types/manage/{$type}/fields/add-field");
$this->submitForm([
'new_storage_type' => 'email',
], 'Continue');
$this->submitForm([
'label' => 'Email',
'field_name' => 'email',
], 'Continue');
$this->submitForm([], 'Update settings');
$this->submitForm([], 'Save settings');
$type = $this->randomMachineName(16);
$name = $this->randomString();
$this->drupalCreateContentType(['type' => $type, 'name' => $name]);
// Set tabs.
$this->drupalPlaceBlock('local_tasks_block', ['primary' => TRUE]);
// Change default language.
$this->drupalGet('admin/config/regional/language');
$this->submitForm(['site_default_language' => 'es'], 'Save configuration');
// Try re-using the email field.
$this->drupalGet("es/admin/structure/types/manage/$type/fields/reuse");
$this->submitForm([], 'Re-use');
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet("es/admin/structure/types/manage/$type/fields/node.$type.field_email/translate");
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("The configuration objects have different language codes so they cannot be translated");
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
/**
* Tests the node language extra field display.
*
* @group node
*/
class NodeViewLanguageTest extends NodeTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node', 'datetime', 'language'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the language extra field display.
*/
public function testViewLanguage(): void {
// Add Spanish language.
ConfigurableLanguage::createFromLangcode('es')->save();
// Set language field visible.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'page', 'full')
->setComponent('langcode')
->save();
// Create a node in Spanish.
$node = $this->drupalCreateNode(['langcode' => 'es']);
$this->drupalGet($node->toUrl());
$this->assertSession()->pageTextContains('Spanish');
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
/**
* Tests the node/{node} page.
*
* @group node
* @see \Drupal\node\Controller\NodeController
*/
class NodeViewTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the html head links.
*/
public function testHtmlHeadLinks(): void {
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl());
$element = $this->assertSession()->elementExists('css', 'link[rel="canonical"]');
$this->assertEquals($node->toUrl()->setAbsolute()->toString(), $element->getAttribute('href'));
$element = $this->assertSession()->elementExists('css', 'link[rel="shortlink"]');
$this->assertEquals($node->toUrl('canonical', ['alias' => TRUE])->setAbsolute()->toString(), $element->getAttribute('href'));
}
/**
* Tests the Link header.
*/
public function testLinkHeader(): void {
$node = $this->drupalCreateNode();
$this->drupalGet($node->toUrl());
$this->assertArrayNotHasKey('Link', $this->getSession()->getResponseHeaders());
}
/**
* Tests that we store and retrieve multi-byte UTF-8 characters correctly.
*/
public function testMultiByteUtf8(): void {
$title = '🐝';
// To ensure that the title has multi-byte characters, we compare the byte
// length to the character length.
$this->assertLessThan(strlen($title), mb_strlen($title, 'utf-8'));
$node = $this->drupalCreateNode(['title' => $title]);
$this->drupalGet($node->toUrl());
// Verify that the passed title was returned.
$this->assertSession()->elementTextEquals('xpath', '//h1/span', $title);
}
}

View File

@@ -0,0 +1,552 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\user\RoleInterface;
/**
* Tests the node entity preview functionality.
*
* @group node
*/
class PagePreviewTest extends NodeTestBase {
use EntityReferenceFieldCreationTrait;
use CommentTestTrait;
use TestFileCreationTrait {
getTestFiles as drupalGetTestFiles;
}
/**
* Enable the comment, node and taxonomy modules to test on the preview.
*
* @var array
*/
protected static $modules = [
'node',
'taxonomy',
'comment',
'image',
'file',
'text',
'node_test',
'menu_ui',
];
/**
* The theme to install as the default for testing.
*
* @var string
*/
protected $defaultTheme = 'starterkit_theme';
/**
* The name of the created field.
*
* @var string
*/
protected $fieldName;
/**
* A term.
*
* @var \Drupal\taxonomy\Entity\Term
*/
protected Term $term;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->addDefaultCommentField('node', 'page');
$web_user = $this->drupalCreateUser([
'edit own page content',
'create page content',
'administer menu',
]);
$this->drupalLogin($web_user);
// Add a vocabulary so we can test different view modes.
$vocabulary = Vocabulary::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $this->randomMachineName(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
'help' => '',
]);
$vocabulary->save();
// Add a term to the vocabulary.
$term = Term::create([
'name' => $this->randomMachineName(),
'description' => $this->randomMachineName(),
'vid' => $vocabulary->id(),
'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
]);
$term->save();
$this->term = $term;
// Create an image field.
FieldStorageConfig::create([
'field_name' => 'field_image',
'entity_type' => 'node',
'type' => 'image',
'settings' => [],
'cardinality' => FieldStorageConfig::CARDINALITY_UNLIMITED,
])->save();
$field_config = FieldConfig::create([
'field_name' => 'field_image',
'label' => 'Images',
'entity_type' => 'node',
'bundle' => 'page',
'required' => FALSE,
'settings' => [],
]);
$field_config->save();
// Create a field.
$this->fieldName = $this->randomMachineName();
$handler_settings = [
'target_bundles' => [
$vocabulary->id() => $vocabulary->id(),
],
'auto_create' => TRUE,
];
$this->createEntityReferenceField('node', 'page', $this->fieldName, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
$display_repository = \Drupal::service('entity_display.repository');
$display_repository->getFormDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'entity_reference_autocomplete_tags',
])
->save();
// Show on default display and teaser.
$display_repository->getViewDisplay('node', 'page')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
])
->save();
$display_repository->getViewDisplay('node', 'page', 'teaser')
->setComponent($this->fieldName, [
'type' => 'entity_reference_label',
])
->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent('field_image', [
'type' => 'image_image',
'settings' => [],
])
->save();
$display_repository->getViewDisplay('node', 'page')
->setComponent('field_image')
->save();
// Create a multi-value text field.
$field_storage = FieldStorageConfig::create([
'field_name' => 'field_test_multi',
'entity_type' => 'node',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'type' => 'text',
'settings' => [
'max_length' => 50,
],
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
])->save();
$display_repository->getFormDisplay('node', 'page')
->setComponent('field_test_multi', [
'type' => 'text_textfield',
])
->save();
$display_repository->getViewDisplay('node', 'page')
->setComponent('field_test_multi', [
'type' => 'string',
])
->save();
}
/**
* Checks the node preview functionality.
*/
public function testPagePreview(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
$term_key = $this->fieldName . '[target_id]';
// Fill in node creation form and preview node.
$edit = [];
$edit[$title_key] = '<em>' . $this->randomMachineName(8) . '</em>';
$edit[$body_key] = $this->randomMachineName(16);
$edit[$term_key] = $this->term->getName();
// Upload an image.
$test_image = current($this->drupalGetTestFiles('image', 39325));
$edit['files[field_image_0][]'] = \Drupal::service('file_system')->realpath($test_image->uri);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Upload');
// Add an alt tag and preview the node.
$this->submitForm(['field_image[0][alt]' => 'Picture of llamas'], 'Preview');
// Check that the preview is displaying the title, body and term.
$expected_title = $edit[$title_key] . ' | Drupal';
$this->assertSession()->titleEquals($expected_title);
$this->assertSession()->assertEscaped($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
$this->assertSession()->linkExists('Back to content editing');
// Check that we see the class of the node type on the body element.
$this->assertSession()->elementExists('xpath', "//body[contains(@class, 'page-node-type-page')]");
// Get the UUID.
$url = parse_url($this->getUrl());
$paths = explode('/', $url['path']);
$view_mode = array_pop($paths);
$uuid = array_pop($paths);
// Switch view mode. We'll remove the body from the teaser view mode.
\Drupal::service('entity_display.repository')
->getViewDisplay('node', 'page', 'teaser')
->removeComponent('body')
->save();
$view_mode_edit = ['view_mode' => 'teaser'];
$this->drupalGet('node/preview/' . $uuid . '/full');
$this->submitForm($view_mode_edit, 'Switch');
$this->assertSession()->responseContains('view-mode-teaser');
$this->assertSession()->pageTextNotContains($edit[$body_key]);
// Check that the title, body and term fields are displayed with the
// values after going back to the content edit page.
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
$this->assertSession()->fieldValueEquals('field_image[0][alt]', 'Picture of llamas');
$this->getSession()->getPage()->pressButton('Add another item');
$this->assertSession()->fieldExists('field_test_multi[0][value]');
$this->assertSession()->fieldExists('field_test_multi[1][value]');
// Return to page preview to check everything is as expected.
$this->submitForm([], 'Preview');
$this->assertSession()->titleEquals($expected_title);
$this->assertSession()->assertEscaped($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
$this->assertSession()->linkExists('Back to content editing');
// Assert the content is kept when reloading the page.
$this->drupalGet('node/add/page', ['query' => ['uuid' => $uuid]]);
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
// Save the node - this is a new POST, so we need to upload the image.
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Upload');
$this->submitForm(['field_image[0][alt]' => 'Picture of llamas'], 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
// Check the term was displayed on the saved node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($edit[$term_key]);
// Check the term appears again on the edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key] . ' (' . $this->term->id() . ')');
// Check with two new terms on the edit form, additionally to the existing
// one.
$edit = [];
$new_term1 = $this->randomMachineName(8);
$new_term2 = $this->randomMachineName(8);
$edit[$term_key] = $this->term->getName() . ', ' . $new_term1 . ', ' . $new_term2;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->responseContains('>' . $new_term1 . '<');
$this->assertSession()->responseContains('>' . $new_term2 . '<');
// The first term should be displayed as link, the others not.
$this->assertSession()->linkExists($this->term->getName());
$this->assertSession()->linkNotExists($new_term1);
$this->assertSession()->linkNotExists($new_term2);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check with one more new term, keeping old terms, removing the existing
// one.
$edit = [];
$new_term3 = $this->randomMachineName(8);
$edit[$term_key] = $new_term1 . ', ' . $new_term3 . ', ' . $new_term2;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->responseContains('>' . $new_term1 . '<');
$this->assertSession()->responseContains('>' . $new_term2 . '<');
$this->assertSession()->responseContains('>' . $new_term3 . '<');
$this->assertSession()->pageTextNotContains($this->term->getName());
$this->assertSession()->linkExists($new_term1);
$this->assertSession()->linkExists($new_term2);
$this->assertSession()->linkNotExists($new_term3);
// Check that editing an existing node after it has been previewed and not
// saved doesn't remember the previous changes.
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
// Navigate away from the node without saving.
$this->drupalGet('<front>');
// Go back to the edit form, the title should have its initial value.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($title_key, $node->label());
// Check with required preview.
$node_type = NodeType::load('page');
$node_type->setPreviewMode(DRUPAL_REQUIRED);
$node_type->save();
$this->drupalGet('node/add/page');
$this->assertSession()->responseNotContains('edit-submit');
$this->drupalGet('node/add/page');
$this->submitForm([$title_key => 'Preview'], 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->responseContains('edit-submit');
// Check that destination is remembered when clicking on preview. When going
// back to the edit form and clicking save, we should go back to the
// original destination, if set.
$destination = 'node';
$this->drupalGet($node->toUrl('edit-form'), ['query' => ['destination' => $destination]]);
$this->submitForm([], 'Preview');
$parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
$options = ['absolute' => TRUE, 'query' => ['destination' => $destination]];
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', $parameters, $options));
$this->submitForm(['view_mode' => 'teaser'], 'Switch');
$this->clickLink('Back to content editing');
$this->submitForm([], 'Save');
$this->assertSession()->addressEquals($destination);
// Check that preview page works as expected without a destination set.
$this->drupalGet($node->toUrl('edit-form'));
$this->submitForm([], 'Preview');
$parameters = ['node_preview' => $node->uuid(), 'view_mode_id' => 'full'];
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', $parameters));
$this->submitForm(['view_mode' => 'teaser'], 'Switch');
$this->clickLink('Back to content editing');
$this->submitForm([], 'Save');
$this->assertSession()->addressEquals($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
// Assert multiple items can be added and are not lost when previewing.
$test_image_1 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_1['files[field_image_0][]'] = $file_system->realpath($test_image_1->uri);
$test_image_2 = current($this->drupalGetTestFiles('image', 39325));
$edit_image_2['files[field_image_1][]'] = $file_system->realpath($test_image_2->uri);
$edit['field_image[0][alt]'] = 'Alt 1';
$this->drupalGet('node/add/page');
$this->submitForm($edit_image_1, 'Upload');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->fieldExists('files[field_image_1][]');
$this->submitForm($edit_image_2, 'Upload');
$this->assertSession()->fieldNotExists('files[field_image_1][]');
$title = 'node_test_title';
$example_text_1 = 'example_text_preview_1';
$example_text_2 = 'example_text_preview_2';
$example_text_3 = 'example_text_preview_3';
$this->drupalGet('node/add/page');
$edit = [
'title[0][value]' => $title,
'field_test_multi[0][value]' => $example_text_1,
];
$this->assertSession()->pageTextContains('Storage is not set');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->pageTextContains('Storage is set');
$this->assertSession()->fieldExists('field_test_multi[0][value]');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Basic page ' . $title . ' has been created.');
$node = $this->drupalGetNodeByTitle($title);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->getSession()->getPage()->pressButton('Add another item');
$this->getSession()->getPage()->pressButton('Add another item');
$edit = [
'field_test_multi[1][value]' => $example_text_2,
'field_test_multi[2][value]' => $example_text_3,
];
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals('field_test_multi[0][value]', $example_text_1);
$this->assertSession()->fieldValueEquals('field_test_multi[1][value]', $example_text_2);
$this->assertSession()->fieldValueEquals('field_test_multi[2][value]', $example_text_3);
// Now save the node and make sure all values got saved.
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains($example_text_1);
$this->assertSession()->pageTextContains($example_text_2);
$this->assertSession()->pageTextContains($example_text_3);
// Edit again, change the menu_ui settings and click on preview.
$this->drupalGet('node/' . $node->id() . '/edit');
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Changed title',
];
$this->submitForm($edit, 'Preview');
$this->clickLink('Back to content editing');
$this->assertSession()->checkboxChecked('edit-menu-enabled');
$this->assertSession()->fieldValueEquals('menu[title]', 'Changed title');
// Save, change the title while saving and make sure that it is correctly
// saved.
$edit = [
'menu[enabled]' => TRUE,
'menu[title]' => 'Second title change',
];
$this->submitForm($edit, 'Save');
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('menu[title]', 'Second title change');
}
/**
* Checks the node preview functionality, when using revisions.
*/
public function testPagePreviewWithRevisions(): void {
$title_key = 'title[0][value]';
$body_key = 'body[0][value]';
$term_key = $this->fieldName . '[target_id]';
// Force revision on "Basic page" content.
$node_type = NodeType::load('page');
$node_type->setNewRevision(TRUE);
$node_type->save();
// Fill in node creation form and preview node.
$edit = [];
$edit[$title_key] = $this->randomMachineName(8);
$edit[$body_key] = $this->randomMachineName(16);
$edit[$term_key] = $this->term->id();
$edit['revision_log[0][value]'] = $this->randomString(32);
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Check that the preview is displaying the title, body and term.
$this->assertSession()->titleEquals($edit[$title_key] . ' | Drupal');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->assertSession()->pageTextContains($edit[$body_key]);
$this->assertSession()->pageTextContains($edit[$term_key]);
// Check that the title and body fields are displayed with the correct
// values after going back to the content edit page.
$this->clickLink('Back to content editing');
$this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]);
$this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]);
$this->assertSession()->fieldValueEquals($term_key, $edit[$term_key]);
// Check that the revision log field has the correct value.
$this->assertSession()->fieldValueEquals('revision_log[0][value]', $edit['revision_log[0][value]']);
// Save the node after coming back from the preview page so we can create a
// pending revision for it.
$this->submitForm([], 'Save');
$node = $this->drupalGetNodeByTitle($edit[$title_key]);
// Check that previewing a pending revision of a node works. This can not be
// accomplished through the UI so we have to use API calls.
// @todo Change this test to use the UI when we will be able to create
// pending revisions in core.
// @see https://www.drupal.org/node/2725533
$node->setNewRevision(TRUE);
$node->isDefaultRevision(FALSE);
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
$node_preview_controller = $controller_resolver->getControllerFromDefinition('\Drupal\node\Controller\NodePreviewController::view');
$node_preview_controller($node, 'full');
}
/**
* Checks the node preview accessible for simultaneous node editing.
*/
public function testSimultaneousPreview(): void {
$title_key = 'title[0][value]';
$node = $this->drupalCreateNode([]);
$edit = [$title_key => 'New page title'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$user2 = $this->drupalCreateUser(['edit any page content']);
$this->drupalLogin($user2);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals($title_key, $node->label());
$edit2 = [$title_key => 'Another page title'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit2, 'Preview');
$this->assertSession()->addressEquals(Url::fromRoute('entity.node.preview', ['node_preview' => $node->uuid(), 'view_mode_id' => 'full']));
$this->assertSession()->pageTextContains($edit2[$title_key]);
}
/**
* Tests node preview with dynamic_page_cache and anonymous users.
*/
public function testPagePreviewCache(): void {
\Drupal::service('module_installer')->uninstall(['node_test']);
$this->drupalLogout();
$title_key = 'title[0][value]';
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['create page content', 'access content']);
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->drupalGet('/node/add/page');
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
$this->clickLink('Back to content editing');
$edit = [
$title_key => $this->randomMachineName(8),
];
$this->submitForm($edit, 'Preview');
$this->assertSession()->pageTextContains($edit[$title_key]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional;
use Drupal\node\Entity\Node;
/**
* Create a node and test edit permissions.
*
* @group node
*/
class PageViewTest extends NodeTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests editing a node by users with various access permissions.
*/
public function testPageView(): void {
// Create a node to view.
$node = $this->drupalCreateNode();
$this->assertNotEmpty(Node::load($node->id()), 'Node created.');
// Try to edit with anonymous user.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(403);
// Create a user without permission to edit node.
$web_user = $this->drupalCreateUser(['access content']);
$this->drupalLogin($web_user);
// Attempt to access edit page.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(403);
// Create user with permission to edit node.
$web_user = $this->drupalCreateUser(['bypass node access']);
$this->drupalLogin($web_user);
// Attempt to access edit page.
$this->drupalGet("node/" . $node->id() . "/edit");
$this->assertSession()->statusCodeEquals(200);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class NodeJsonAnonTest extends NodeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class NodeJsonBasicAuthTest extends NodeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
parent::setUpAuthorization($method);
$this->grantPermissionsToTestedRole(['view camelids revisions']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$entity = parent::getExpectedNormalizedEntity();
$entity['revision_log'] = [];
return $entity;
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class NodeJsonCookieTest extends NodeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
use GuzzleHttp\RequestOptions;
abstract class NodeResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'path'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'node';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'revision_timestamp' => NULL,
'revision_uid' => NULL,
'created' => "The 'administer nodes' permission is required.",
'changed' => NULL,
'promote' => "The 'administer nodes' permission is required.",
'sticky' => "The 'administer nodes' permission is required.",
'path' => "The following permissions are required: 'create url aliases' OR 'administer url aliases'.",
];
/**
* @var \Drupal\node\NodeInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['access content']);
break;
case 'POST':
$this->grantPermissionsToTestedRole(['access content', 'create camelids content']);
break;
case 'PATCH':
// Do not grant the 'create url aliases' permission to test the case
// when the path field is protected/not accessible, see
// \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase
// for a positive test.
$this->grantPermissionsToTestedRole(['access content', 'edit any camelids content']);
break;
case 'DELETE':
$this->grantPermissionsToTestedRole(['access content', 'delete any camelids content']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
if (!NodeType::load('camelids')) {
// Create a "Camelids" node type.
NodeType::create([
'name' => 'Camelids',
'type' => 'camelids',
])->save();
}
// Create a "Llama" node.
$node = Node::create(['type' => 'camelids']);
$node->setTitle('Llama')
->setOwnerId(static::$auth ? $this->account->id() : 0)
->setPublished()
->setCreatedTime(123456789)
->setChangedTime(123456789)
->setRevisionCreationTime(123456789)
->set('path', '/llama')
->save();
return $node;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$author = User::load($this->entity->getOwnerId());
return [
'nid' => [
['value' => 1],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
'vid' => [
['value' => 1],
],
'langcode' => [
[
'value' => 'en',
],
],
'type' => [
[
'target_id' => 'camelids',
'target_type' => 'node_type',
'target_uuid' => NodeType::load('camelids')->uuid(),
],
],
'title' => [
[
'value' => 'Llama',
],
],
'status' => [
[
'value' => TRUE,
],
],
'created' => [
[
'value' => (new \DateTime())->setTimestamp(123456789)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'changed' => [
[
'value' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'promote' => [
[
'value' => TRUE,
],
],
'sticky' => [
[
'value' => FALSE,
],
],
'revision_timestamp' => [
[
'value' => (new \DateTime())->setTimestamp(123456789)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
'format' => \DateTime::RFC3339,
],
],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
'default_langcode' => [
[
'value' => TRUE,
],
],
'uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'revision_uid' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'path' => [
[
'alias' => '/llama',
'pid' => 1,
'langcode' => 'en',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'type' => [
[
'target_id' => 'camelids',
],
],
'title' => [
[
'value' => 'Drama llama',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE' || $method == 'POST') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
/**
* Tests PATCHing a node's path with and without 'create url aliases'.
*
* For a positive test, see the similar test coverage for Term.
*
* @see \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath()
*/
public function testPatchPath(): void {
$this->initAuthentication();
$this->provisionEntityResource();
$this->setUpAuthorization('GET');
$this->setUpAuthorization('PATCH');
$url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]);
// GET node's current normalization.
$response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET'));
$normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
// Change node's path alias.
$normalization['path'][0]['alias'] .= 's-rule-the-world';
// Create node PATCH request.
$request_options = [];
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
$request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
$request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
// PATCH request: 403 when creating URL aliases unauthorized. Before
// asserting the 403 response, assert that the stored path alias remains
// unchanged.
$response = $this->request('PATCH', $url, $request_options);
$this->assertSame('/llama', $this->entityStorage->loadUnchanged($this->entity->id())->get('path')->alias);
$this->assertResourceErrorResponse(403, "Access denied on updating field 'path'. " . static::$patchProtectedFieldNames['path'], $response);
// Make sure the role save below properly invalidates cache tags.
$this->refreshVariables();
// Grant permission to create URL aliases.
$this->grantPermissionsToTestedRole(['create url aliases']);
// Repeat PATCH request: 200.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
$updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
$this->assertSame($normalization['path'], $updated_normalization['path']);
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonAnonTest extends NodeTypeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonBasicAuthTest extends NodeTypeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonCookieTest extends NodeTypeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\rest\Functional\EntityResource\ConfigEntityResourceTestBase;
/**
* ResourceTestBase for NodeType entity.
*/
abstract class NodeTypeResourceTestBase extends ConfigEntityResourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'node_type';
/**
* The NodeType entity.
*
* @var \Drupal\node\NodeTypeInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer content types', 'access content']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" node type.
$camelids = NodeType::create([
'name' => 'Camelids',
'type' => 'camelids',
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
]);
$camelids->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
'display_submitted' => TRUE,
'help' => NULL,
'langcode' => 'en',
'name' => 'Camelids',
'new_revision' => TRUE,
'preview_mode' => 1,
'status' => TRUE,
'type' => 'camelids',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
return [];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
return "The 'access content' permission is required.";
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlAnonTest extends NodeTypeResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlBasicAuthTest extends NodeTypeResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeTypeXmlCookieTest extends NodeTypeResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\node\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class NodeXmlAnonTest extends NodeResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
public function testPatchPath(): void {
// Deserialization of the XML format is not supported.
$this->markTestSkipped();
}
}

Some files were not shown because too many files have changed in this diff Show More