Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / tracker / tests / src / Functional / TrackerTest.php
1 <?php
2
3 namespace Drupal\Tests\tracker\Functional;
4
5 use Drupal\comment\CommentInterface;
6 use Drupal\comment\Tests\CommentTestTrait;
7 use Drupal\Core\Cache\Cache;
8 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
9 use Drupal\Core\Session\AccountInterface;
10 use Drupal\field\Entity\FieldStorageConfig;
11 use Drupal\node\Entity\Node;
12 use Drupal\Tests\BrowserTestBase;
13 use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
14
15 /**
16  * Create and delete nodes and check for their display in the tracker listings.
17  *
18  * @group tracker
19  */
20 class TrackerTest extends BrowserTestBase {
21
22   use CommentTestTrait;
23   use AssertPageCacheContextsAndTagsTrait;
24
25   /**
26    * Modules to enable.
27    *
28    * @var array
29    */
30   public static $modules = ['block', 'comment', 'tracker', 'history', 'node_test'];
31
32   /**
33    * The main user for testing.
34    *
35    * @var \Drupal\user\UserInterface
36    */
37   protected $user;
38
39   /**
40    * A second user that will 'create' comments and nodes.
41    *
42    * @var \Drupal\user\UserInterface
43    */
44   protected $otherUser;
45
46   protected function setUp() {
47     parent::setUp();
48
49     $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
50
51     $permissions = ['access comments', 'create page content', 'post comments', 'skip comment approval'];
52     $this->user = $this->drupalCreateUser($permissions);
53     $this->otherUser = $this->drupalCreateUser($permissions);
54     $this->addDefaultCommentField('node', 'page');
55     user_role_grant_permissions(AccountInterface::ANONYMOUS_ROLE, [
56       'access content',
57       'access user profiles',
58     ]);
59     $this->drupalPlaceBlock('local_tasks_block', ['id' => 'page_tabs_block']);
60     $this->drupalPlaceBlock('local_actions_block', ['id' => 'page_actions_block']);
61   }
62
63   /**
64    * Tests for the presence of nodes on the global tracker listing.
65    */
66   public function testTrackerAll() {
67     $this->drupalLogin($this->user);
68
69     $unpublished = $this->drupalCreateNode([
70       'title' => $this->randomMachineName(8),
71       'status' => 0,
72     ]);
73     $published = $this->drupalCreateNode([
74       'title' => $this->randomMachineName(8),
75       'status' => 1,
76     ]);
77
78     $this->drupalGet('activity');
79     $this->assertNoText($unpublished->label(), 'Unpublished node does not show up in the tracker listing.');
80     $this->assertText($published->label(), 'Published node shows up in the tracker listing.');
81     $this->assertLink(t('My recent content'), 0, 'User tab shows up on the global tracker page.');
82
83     // Assert cache contexts, specifically the pager and node access contexts.
84     $this->assertCacheContexts(['languages:language_interface', 'route', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.query_args.pagers:0', 'user.node_grants:view', 'user']);
85     // Assert cache tags for the action/tabs blocks, visible node, and node list
86     // cache tag.
87     $expected_tags = Cache::mergeTags($published->getCacheTags(), $published->getOwner()->getCacheTags());
88     // Because the 'user.permissions' cache context is being optimized away.
89     $role_tags = [];
90     foreach ($this->user->getRoles() as $rid) {
91       $role_tags[] = "config:user.role.$rid";
92     }
93     $expected_tags = Cache::mergeTags($expected_tags, $role_tags);
94     $block_tags = [
95       'block_view',
96       'config:block.block.page_actions_block',
97       'config:block.block.page_tabs_block',
98       'config:block_list',
99     ];
100     $expected_tags = Cache::mergeTags($expected_tags, $block_tags);
101     $additional_tags = [
102       'node_list',
103       'rendered',
104     ];
105     $expected_tags = Cache::mergeTags($expected_tags, $additional_tags);
106     $this->assertCacheTags($expected_tags);
107
108     // Delete a node and ensure it no longer appears on the tracker.
109     $published->delete();
110     $this->drupalGet('activity');
111     $this->assertNoText($published->label(), 'Deleted node does not show up in the tracker listing.');
112
113     // Test proper display of time on activity page when comments are disabled.
114     // Disable comments.
115     FieldStorageConfig::loadByName('node', 'comment')->delete();
116     $node = $this->drupalCreateNode([
117       // This title is required to trigger the custom changed time set in the
118       // node_test module. This is needed in order to ensure a sufficiently
119       // large 'time ago' interval that isn't numbered in seconds.
120       'title' => 'testing_node_presave',
121       'status' => 1,
122     ]);
123
124     $this->drupalGet('activity');
125     $this->assertText($node->label(), 'Published node shows up in the tracker listing.');
126     $this->assertText(\Drupal::service('date.formatter')->formatTimeDiffSince($node->getChangedTime()), 'The changed time was displayed on the tracker listing.');
127   }
128
129   /**
130    * Tests for the presence of nodes on a user's tracker listing.
131    */
132   public function testTrackerUser() {
133     $this->drupalLogin($this->user);
134
135     $unpublished = $this->drupalCreateNode([
136       'title' => $this->randomMachineName(8),
137       'uid' => $this->user->id(),
138       'status' => 0,
139     ]);
140     $my_published = $this->drupalCreateNode([
141       'title' => $this->randomMachineName(8),
142       'uid' => $this->user->id(),
143       'status' => 1,
144     ]);
145     $other_published_no_comment = $this->drupalCreateNode([
146       'title' => $this->randomMachineName(8),
147       'uid' => $this->otherUser->id(),
148       'status' => 1,
149     ]);
150     $other_published_my_comment = $this->drupalCreateNode([
151       'title' => $this->randomMachineName(8),
152       'uid' => $this->otherUser->id(),
153       'status' => 1,
154     ]);
155     $comment = [
156       'subject[0][value]' => $this->randomMachineName(),
157       'comment_body[0][value]' => $this->randomMachineName(20),
158     ];
159     $this->drupalPostForm('comment/reply/node/' . $other_published_my_comment->id() . '/comment', $comment, t('Save'));
160
161     $this->drupalGet('user/' . $this->user->id() . '/activity');
162     $this->assertNoText($unpublished->label(), "Unpublished nodes do not show up in the user's tracker listing.");
163     $this->assertText($my_published->label(), "Published nodes show up in the user's tracker listing.");
164     $this->assertNoText($other_published_no_comment->label(), "Another user's nodes do not show up in the user's tracker listing.");
165     $this->assertText($other_published_my_comment->label(), "Nodes that the user has commented on appear in the user's tracker listing.");
166
167     // Assert cache contexts.
168     $this->assertCacheContexts(['languages:language_interface', 'route', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
169     // Assert cache tags for the visible nodes (including owners) and node list
170     // cache tag.
171     $expected_tags = Cache::mergeTags($my_published->getCacheTags(), $my_published->getOwner()->getCacheTags());
172     $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getCacheTags());
173     $expected_tags = Cache::mergeTags($expected_tags, $other_published_my_comment->getOwner()->getCacheTags());
174     // Because the 'user.permissions' cache context is being optimized away.
175     $role_tags = [];
176     foreach ($this->user->getRoles() as $rid) {
177       $role_tags[] = "config:user.role.$rid";
178     }
179     $expected_tags = Cache::mergeTags($expected_tags, $role_tags);
180     $block_tags = [
181       'block_view',
182       'config:block.block.page_actions_block',
183       'config:block.block.page_tabs_block',
184       'config:block_list',
185     ];
186     $expected_tags = Cache::mergeTags($expected_tags, $block_tags);
187     $additional_tags = [
188       'node_list',
189       'rendered',
190     ];
191     $expected_tags = Cache::mergeTags($expected_tags, $additional_tags);
192
193     $this->assertCacheTags($expected_tags);
194     $this->assertCacheContexts(['languages:language_interface', 'route', 'theme', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT, 'url.query_args.pagers:0', 'user', 'user.node_grants:view']);
195
196     $this->assertLink($my_published->label());
197     $this->assertNoLink($unpublished->label());
198     // Verify that title and tab title have been set correctly.
199     $this->assertText('Activity', 'The user activity tab has the name "Activity".');
200     $this->assertTitle(t('@name | @site', ['@name' => $this->user->getUsername(), '@site' => $this->config('system.site')->get('name')]), 'The user tracker page has the correct page title.');
201
202     // Verify that unpublished comments are removed from the tracker.
203     $admin_user = $this->drupalCreateUser(['post comments', 'administer comments', 'access user profiles']);
204     $this->drupalLogin($admin_user);
205     $this->drupalPostForm('comment/1/edit', ['status' => CommentInterface::NOT_PUBLISHED], t('Save'));
206     $this->drupalGet('user/' . $this->user->id() . '/activity');
207     $this->assertNoText($other_published_my_comment->label(), 'Unpublished comments are not counted on the tracker listing.');
208
209     // Test escaping of title on user's tracker tab.
210     \Drupal::service('module_installer')->install(['user_hooks_test']);
211     Cache::invalidateTags(['rendered']);
212     \Drupal::state()->set('user_hooks_test_user_format_name_alter', TRUE);
213     $this->drupalGet('user/' . $this->user->id() . '/activity');
214     $this->assertEscaped('<em>' . $this->user->id() . '</em>');
215
216     \Drupal::state()->set('user_hooks_test_user_format_name_alter_safe', TRUE);
217     Cache::invalidateTags(['rendered']);
218     $this->drupalGet('user/' . $this->user->id() . '/activity');
219     $this->assertNoEscaped('<em>' . $this->user->id() . '</em>');
220     $this->assertRaw('<em>' . $this->user->id() . '</em>');
221   }
222
223   /**
224    * Tests the metadata for the "new"/"updated" indicators.
225    */
226   public function testTrackerHistoryMetadata() {
227     $this->drupalLogin($this->user);
228
229     // Create a page node.
230     $edit = [
231       'title' => $this->randomMachineName(8),
232     ];
233     $node = $this->drupalCreateNode($edit);
234
235     // Verify that the history metadata is present.
236     $this->drupalGet('activity');
237     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->getChangedTime());
238     $this->drupalGet('activity/' . $this->user->id());
239     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->getChangedTime());
240     $this->drupalGet('user/' . $this->user->id() . '/activity');
241     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->getChangedTime());
242
243     // Add a comment to the page, make sure it is created after the node by
244     // sleeping for one second, to ensure the last comment timestamp is
245     // different from before.
246     $comment = [
247       'subject[0][value]' => $this->randomMachineName(),
248       'comment_body[0][value]' => $this->randomMachineName(20),
249     ];
250     sleep(1);
251     $this->drupalPostForm('comment/reply/node/' . $node->id() . '/comment', $comment, t('Save'));
252     // Reload the node so that comment.module's hook_node_load()
253     // implementation can set $node->last_comment_timestamp for the freshly
254     // posted comment.
255     $node = Node::load($node->id());
256
257     // Verify that the history metadata is updated.
258     $this->drupalGet('activity');
259     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->get('comment')->last_comment_timestamp);
260     $this->drupalGet('activity/' . $this->user->id());
261     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->get('comment')->last_comment_timestamp);
262     $this->drupalGet('user/' . $this->user->id() . '/activity');
263     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->get('comment')->last_comment_timestamp);
264
265     // Log out, now verify that the metadata is still there, but the library is
266     // not.
267     $this->drupalLogout();
268     $this->drupalGet('activity');
269     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->get('comment')->last_comment_timestamp, FALSE);
270     $this->drupalGet('user/' . $this->user->id() . '/activity');
271     $this->assertHistoryMetadata($node->id(), $node->getChangedTime(), $node->get('comment')->last_comment_timestamp, FALSE);
272   }
273
274   /**
275    * Tests for ordering on a users tracker listing when comments are posted.
276    */
277   public function testTrackerOrderingNewComments() {
278     $this->drupalLogin($this->user);
279
280     $node_one = $this->drupalCreateNode([
281       'title' => $this->randomMachineName(8),
282     ]);
283
284     $node_two = $this->drupalCreateNode([
285       'title' => $this->randomMachineName(8),
286     ]);
287
288     // Now get otherUser to track these pieces of content.
289     $this->drupalLogin($this->otherUser);
290
291     // Add a comment to the first page.
292     $comment = [
293       'subject[0][value]' => $this->randomMachineName(),
294       'comment_body[0][value]' => $this->randomMachineName(20),
295     ];
296     $this->drupalPostForm('comment/reply/node/' . $node_one->id() . '/comment', $comment, t('Save'));
297
298     // If the comment is posted in the same second as the last one then Drupal
299     // can't tell the difference, so we wait one second here.
300     sleep(1);
301
302     // Add a comment to the second page.
303     $comment = [
304       'subject[0][value]' => $this->randomMachineName(),
305       'comment_body[0][value]' => $this->randomMachineName(20),
306     ];
307     $this->drupalPostForm('comment/reply/node/' . $node_two->id() . '/comment', $comment, t('Save'));
308
309     // We should at this point have in our tracker for otherUser:
310     // 1. node_two
311     // 2. node_one
312     // Because that's the reverse order of the posted comments.
313
314     // Now we're going to post a comment to node_one which should jump it to the
315     // top of the list.
316
317     $this->drupalLogin($this->user);
318     // If the comment is posted in the same second as the last one then Drupal
319     // can't tell the difference, so we wait one second here.
320     sleep(1);
321
322     // Add a comment to the second page.
323     $comment = [
324       'subject[0][value]' => $this->randomMachineName(),
325       'comment_body[0][value]' => $this->randomMachineName(20),
326     ];
327     $this->drupalPostForm('comment/reply/node/' . $node_one->id() . '/comment', $comment, t('Save'));
328
329     // Switch back to the otherUser and assert that the order has swapped.
330     $this->drupalLogin($this->otherUser);
331     $this->drupalGet('user/' . $this->otherUser->id() . '/activity');
332     // This is a cheeky way of asserting that the nodes are in the right order
333     // on the tracker page.
334     // It's almost certainly too brittle.
335     $pattern = '/' . preg_quote($node_one->getTitle()) . '.+' . preg_quote($node_two->getTitle()) . '/s';
336     $this->verbose($pattern);
337     $this->assertPattern($pattern, 'Most recently commented on node appears at the top of tracker');
338   }
339
340   /**
341    * Tests that existing nodes are indexed by cron.
342    */
343   public function testTrackerCronIndexing() {
344     $this->drupalLogin($this->user);
345
346     // Create 3 nodes.
347     $edits = [];
348     $nodes = [];
349     for ($i = 1; $i <= 3; $i++) {
350       $edits[$i] = [
351         'title' => $this->randomMachineName(),
352       ];
353       $nodes[$i] = $this->drupalCreateNode($edits[$i]);
354     }
355
356     // Add a comment to the last node as other user.
357     $this->drupalLogin($this->otherUser);
358     $comment = [
359       'subject[0][value]' => $this->randomMachineName(),
360       'comment_body[0][value]' => $this->randomMachineName(20),
361     ];
362     $this->drupalPostForm('comment/reply/node/' . $nodes[3]->id() . '/comment', $comment, t('Save'));
363
364     // Start indexing backwards from node 3.
365     \Drupal::state()->set('tracker.index_nid', 3);
366
367     // Clear the current tracker tables and rebuild them.
368     db_delete('tracker_node')
369       ->execute();
370     db_delete('tracker_user')
371       ->execute();
372     tracker_cron();
373
374     $this->drupalLogin($this->user);
375
376     // Fetch the user's tracker.
377     $this->drupalGet('activity/' . $this->user->id());
378
379     // Assert that all node titles are displayed.
380     foreach ($nodes as $i => $node) {
381       $this->assertText($node->label(), format_string('Node @i is displayed on the tracker listing pages.', ['@i' => $i]));
382     }
383
384     // Fetch the site-wide tracker.
385     $this->drupalGet('activity');
386
387     // Assert that all node titles are displayed.
388     foreach ($nodes as $i => $node) {
389       $this->assertText($node->label(), format_string('Node @i is displayed on the tracker listing pages.', ['@i' => $i]));
390     }
391   }
392
393   /**
394    * Tests that publish/unpublish works at admin/content/node.
395    */
396   public function testTrackerAdminUnpublish() {
397     \Drupal::service('module_installer')->install(['views']);
398     \Drupal::service('router.builder')->rebuild();
399     $admin_user = $this->drupalCreateUser(['access content overview', 'administer nodes', 'bypass node access']);
400     $this->drupalLogin($admin_user);
401
402     $node = $this->drupalCreateNode([
403       'title' => $this->randomMachineName(),
404     ]);
405
406     // Assert that the node is displayed.
407     $this->drupalGet('activity');
408     $this->assertText($node->label(), 'A node is displayed on the tracker listing pages.');
409
410     // Unpublish the node and ensure that it's no longer displayed.
411     $edit = [
412       'action' => 'node_unpublish_action',
413       'node_bulk_form[0]' => $node->id(),
414     ];
415     $this->drupalPostForm('admin/content', $edit, t('Apply to selected items'));
416
417     $this->drupalGet('activity');
418     $this->assertText(t('No content available.'), 'A node is displayed on the tracker listing pages.');
419   }
420
421   /**
422    * Passes if the appropriate history metadata exists.
423    *
424    * Verify the data-history-node-id, data-history-node-timestamp and
425    * data-history-node-last-comment-timestamp attributes, which are used by the
426    * drupal.tracker-history library to add the appropriate "new" and "updated"
427    * indicators, as well as the "x new" replies link to the tracker.
428    * We do this in JavaScript to prevent breaking the render cache.
429    *
430    * @param int $node_id
431    *   A node ID, that must exist as a data-history-node-id attribute
432    * @param int $node_timestamp
433    *   A node timestamp, that must exist as a data-history-node-timestamp
434    *   attribute.
435    * @param int $node_last_comment_timestamp
436    *   A node's last comment timestamp, that must exist as a
437    *   data-history-node-last-comment-timestamp attribute.
438    * @param bool $library_is_present
439    *   Whether the drupal.tracker-history library should be present or not.
440    */
441   public function assertHistoryMetadata($node_id, $node_timestamp, $node_last_comment_timestamp, $library_is_present = TRUE) {
442     $settings = $this->getDrupalSettings();
443     $this->assertIdentical($library_is_present, isset($settings['ajaxPageState']) && in_array('tracker/history', explode(',', $settings['ajaxPageState']['libraries'])), 'drupal.tracker-history library is present.');
444     $this->assertIdentical(1, count($this->xpath('//table/tbody/tr/td[@data-history-node-id="' . $node_id . '" and @data-history-node-timestamp="' . $node_timestamp . '"]')), 'Tracker table cell contains the data-history-node-id and data-history-node-timestamp attributes for the node.');
445     $this->assertIdentical(1, count($this->xpath('//table/tbody/tr/td[@data-history-node-last-comment-timestamp="' . $node_last_comment_timestamp . '"]')), 'Tracker table cell contains the data-history-node-last-comment-timestamp attribute for the node.');
446   }
447
448 }