Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / quickedit / src / Tests / QuickEditLoadingTest.php
1 <?php
2
3 namespace Drupal\quickedit\Tests;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Component\Utility\Unicode;
7 use Drupal\block_content\Entity\BlockContent;
8 use Drupal\field\Entity\FieldConfig;
9 use Drupal\field\Entity\FieldStorageConfig;
10 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
11 use Drupal\Core\Url;
12 use Drupal\node\Entity\Node;
13 use Drupal\node\Entity\NodeType;
14 use Drupal\simpletest\WebTestBase;
15 use Drupal\filter\Entity\FilterFormat;
16
17 /**
18  * Tests loading of in-place editing functionality and lazy loading of its
19  * in-place editors.
20  *
21  * @group quickedit
22  */
23 class QuickEditLoadingTest extends WebTestBase {
24
25   /**
26    * Modules to enable.
27    *
28    * @var array
29    */
30   public static $modules = [
31     'contextual',
32     'quickedit',
33     'filter',
34     'node',
35     'image',
36   ];
37
38   /**
39    * An user with permissions to create and edit articles.
40    *
41    * @var \Drupal\user\UserInterface
42    */
43   protected $authorUser;
44
45   /**
46    * A test node.
47    *
48    * @var \Drupal\node\NodeInterface
49    */
50   protected $testNode;
51
52   /**
53    * A author user with permissions to access in-place editor.
54    *
55    * @var \Drupal\user\UserInterface
56    */
57   protected $editorUser;
58
59   protected function setUp() {
60     parent::setUp();
61
62     // Create a text format.
63     $filtered_html_format = FilterFormat::create([
64       'format' => 'filtered_html',
65       'name' => 'Filtered HTML',
66       'weight' => 0,
67       'filters' => [],
68     ]);
69     $filtered_html_format->save();
70
71     // Create a node type.
72     $this->drupalCreateContentType([
73       'type' => 'article',
74       'name' => 'Article',
75     ]);
76
77     // Set the node type to initially not have revisions.
78     // Testing with revisions will be done later.
79     $node_type = NodeType::load('article');
80     $node_type->setNewRevision(FALSE);
81     $node_type->save();
82
83     // Create one node of the above node type using the above text format.
84     $this->testNode = $this->drupalCreateNode([
85       'type' => 'article',
86       'body' => [
87         0 => [
88           'value' => '<p>How are you?</p>',
89           'format' => 'filtered_html',
90         ]
91       ],
92       'revision_log' => $this->randomString(),
93     ]);
94
95     // Create 2 users, the only difference being the ability to use in-place
96     // editing
97     $basic_permissions = ['access content', 'create article content', 'edit any article content', 'use text format filtered_html', 'access contextual links'];
98     $this->authorUser = $this->drupalCreateUser($basic_permissions);
99     $this->editorUser = $this->drupalCreateUser(array_merge($basic_permissions, ['access in-place editing']));
100   }
101
102   /**
103    * Test the loading of Quick Edit when a user doesn't have access to it.
104    */
105   public function testUserWithoutPermission() {
106     $this->drupalLogin($this->authorUser);
107     $this->drupalGet('node/1');
108
109     // Library and in-place editors.
110     $this->assertNoRaw('core/modules/quickedit/js/quickedit.js', 'Quick Edit library not loaded.');
111     $this->assertNoRaw('core/modules/quickedit/js/editors/formEditor.js', "'form' in-place editor not loaded.");
112
113     // HTML annotation and title class does not exist for users without
114     // permission to in-place edit.
115     $this->assertNoRaw('data-quickedit-entity-id="node/1"');
116     $this->assertNoRaw('data-quickedit-field-id="node/1/body/en/full"');
117     $this->assertNoFieldByXPath('//h1[contains(@class, "js-quickedit-page-title")]');
118
119     // Retrieving the metadata should result in an empty 403 response.
120     $post = ['fields[0]' => 'node/1/body/en/full'];
121     $response = $this->drupalPostWithFormat(Url::fromRoute('quickedit.metadata'), 'json', $post);
122     $this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
123     $this->assertResponse(403);
124
125     // Quick Edit's JavaScript would never hit these endpoints if the metadata
126     // was empty as above, but we need to make sure that malicious users aren't
127     // able to use any of the other endpoints either.
128     $post = ['editors[0]' => 'form'] + $this->getAjaxPageStatePostData();
129     $response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
130     $message = Json::encode(['message' => "The 'access in-place editing' permission is required."]);
131     $this->assertIdentical($message, $response);
132     $this->assertResponse(403);
133     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
134     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
135     $this->assertIdentical($message, $response);
136     $this->assertResponse(403);
137     $edit = [];
138     $edit['form_id'] = 'quickedit_field_form';
139     $edit['form_token'] = 'xIOzMjuc-PULKsRn_KxFn7xzNk5Bx7XKXLfQfw1qOnA';
140     $edit['form_build_id'] = 'form-kVmovBpyX-SJfTT5kY0pjTV35TV-znor--a64dEnMR8';
141     $edit['body[0][summary]'] = '';
142     $edit['body[0][value]'] = '<p>Malicious content.</p>';
143     $edit['body[0][format]'] = 'filtered_html';
144     $edit['op'] = t('Save');
145     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $edit, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
146     $this->assertIdentical($message, $response);
147     $this->assertResponse(403);
148     $post = ['nocssjs' => 'true'];
149     $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
150     $this->assertIdentical(Json::encode(['message' => "The 'access in-place editing' permission is required."]), $response);
151     $this->assertResponse(403);
152   }
153
154   /**
155    * Tests the loading of Quick Edit when a user does have access to it.
156    *
157    * Also ensures lazy loading of in-place editors works.
158    */
159   public function testUserWithPermission() {
160     $this->drupalLogin($this->editorUser);
161     $this->drupalGet('node/1');
162
163     // Library and in-place editors.
164     $settings = $this->getDrupalSettings();
165     $libraries = explode(',', $settings['ajaxPageState']['libraries']);
166     $this->assertTrue(in_array('quickedit/quickedit', $libraries), 'Quick Edit library loaded.');
167     $this->assertFalse(in_array('quickedit/quickedit.inPlaceEditor.form', $libraries), "'form' in-place editor not loaded.");
168
169     // HTML annotation and title class must always exist (to not break the
170     // render cache).
171     $this->assertRaw('data-quickedit-entity-id="node/1"');
172     $this->assertRaw('data-quickedit-field-id="node/1/body/en/full"');
173     $this->assertFieldByXPath('//h1[contains(@class, "js-quickedit-page-title")]');
174
175     // There should be only one revision so far.
176     $node = Node::load(1);
177     $vids = \Drupal::entityManager()->getStorage('node')->revisionIds($node);
178     $this->assertIdentical(1, count($vids), 'The node has only one revision.');
179     $original_log = $node->revision_log->value;
180
181     // Retrieving the metadata should result in a 200 JSON response.
182     $htmlPageDrupalSettings = $this->drupalSettings;
183     $post = ['fields[0]' => 'node/1/body/en/full'];
184     $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post);
185     $this->assertResponse(200);
186     $expected = [
187       'node/1/body/en/full' => [
188         'label' => 'Body',
189         'access' => TRUE,
190         'editor' => 'form',
191       ]
192     ];
193     $this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
194     // Restore drupalSettings to build the next requests; simpletest wipes them
195     // after a JSON response.
196     $this->drupalSettings = $htmlPageDrupalSettings;
197
198     // Retrieving the attachments should result in a 200 response, containing:
199     //  1. a settings command with useless metadata: AjaxController is dumb
200     //  2. an insert command that loads the required in-place editors
201     $post = ['editors[0]' => 'form'] + $this->getAjaxPageStatePostData();
202     $response = $this->drupalPost('quickedit/attachments', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
203     $ajax_commands = Json::decode($response);
204     $this->assertIdentical(2, count($ajax_commands), 'The attachments HTTP request results in two AJAX commands.');
205     // First command: settings.
206     $this->assertIdentical('settings', $ajax_commands[0]['command'], 'The first AJAX command is a settings command.');
207     // Second command: insert libraries into DOM.
208     $this->assertIdentical('insert', $ajax_commands[1]['command'], 'The second AJAX command is an append command.');
209     $this->assertTrue(in_array('quickedit/quickedit.inPlaceEditor.form', explode(',', $ajax_commands[0]['settings']['ajaxPageState']['libraries'])), 'The quickedit.inPlaceEditor.form library is loaded.');
210
211     // Retrieving the form for this field should result in a 200 response,
212     // containing only a quickeditFieldForm command.
213     $post = ['nocssjs' => 'true', 'reset' => 'true'] + $this->getAjaxPageStatePostData();
214     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
215     $this->assertResponse(200);
216     $ajax_commands = Json::decode($response);
217     $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
218     $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
219     $this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
220
221     // Prepare form values for submission. drupalPostAjaxForm() is not suitable
222     // for handling pages with JSON responses, so we need our own solution here.
223     $form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
224     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
225
226     if ($form_tokens_found) {
227       $edit = [
228         'body[0][summary]' => '',
229         'body[0][value]' => '<p>Fine thanks.</p>',
230         'body[0][format]' => 'filtered_html',
231         'op' => t('Save'),
232       ];
233       $post = [
234         'form_id' => 'quickedit_field_form',
235         'form_token' => $token_match[1],
236         'form_build_id' => $build_id_match[1],
237       ];
238       $post += $edit + $this->getAjaxPageStatePostData();
239
240       // Submit field form and check response. This should store the updated
241       // entity in PrivateTempStore on the server.
242       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
243       $this->assertResponse(200);
244       $ajax_commands = Json::decode($response);
245       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
246       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
247       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Fine thanks.'), 'Form value saved and printed back.');
248       $this->assertIdentical($ajax_commands[0]['other_view_modes'], [], 'Field was not rendered in any other view mode.');
249
250       // Ensure the text on the original node did not change yet.
251       $this->drupalGet('node/1');
252       $this->assertText('How are you?');
253
254       // Save the entity by moving the PrivateTempStore values to entity storage.
255       $post = ['nocssjs' => 'true'];
256       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
257       $this->assertResponse(200);
258       $ajax_commands = Json::decode($response);
259       $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
260       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditEntitySaved command.');
261       $this->assertIdentical($ajax_commands[0]['data']['entity_type'], 'node', 'Saved entity is of type node.');
262       $this->assertIdentical($ajax_commands[0]['data']['entity_id'], '1', 'Entity id is 1.');
263
264       // Ensure the text on the original node did change.
265       $this->drupalGet('node/1');
266       $this->assertText('Fine thanks.');
267
268       // Ensure no new revision was created and the log message is unchanged.
269       $node = Node::load(1);
270       $vids = \Drupal::entityManager()->getStorage('node')->revisionIds($node);
271       $this->assertIdentical(1, count($vids), 'The node has only one revision.');
272       $this->assertIdentical($original_log, $node->revision_log->value, 'The revision log message is unchanged.');
273
274       // Now configure this node type to create new revisions automatically,
275       // then again retrieve the field form, fill it, submit it (so it ends up
276       // in PrivateTempStore) and then save the entity. Now there should be two
277       // revisions.
278       $node_type = NodeType::load('article');
279       $node_type->setNewRevision(TRUE);
280       $node_type->save();
281
282       // Retrieve field form.
283       $post = ['nocssjs' => 'true', 'reset' => 'true'];
284       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
285       $this->assertResponse(200);
286       $ajax_commands = Json::decode($response);
287       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
288       $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
289       $this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
290
291       // Submit field form.
292       preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match);
293       preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
294       $edit['body[0][value]'] = '<p>kthxbye</p>';
295       $post = [
296         'form_id' => 'quickedit_field_form',
297         'form_token' => $token_match[1],
298         'form_build_id' => $build_id_match[1],
299       ];
300       $post += $edit + $this->getAjaxPageStatePostData();
301       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
302       $this->assertResponse(200);
303       $ajax_commands = Json::decode($response);
304       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
305       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is an quickeditFieldFormSaved command.');
306       $this->assertTrue(strpos($ajax_commands[0]['data'], 'kthxbye'), 'Form value saved and printed back.');
307
308       // Save the entity.
309       $post = ['nocssjs' => 'true'];
310       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
311       $this->assertResponse(200);
312       $ajax_commands = Json::decode($response);
313       $this->assertIdentical(1, count($ajax_commands));
314       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is an quickeditEntitySaved command.');
315       $this->assertEqual($ajax_commands[0]['data'], ['entity_type' => 'node', 'entity_id' => 1], 'Updated entity type and ID returned');
316
317       // Test that a revision was created with the correct log message.
318       $vids = \Drupal::entityManager()->getStorage('node')->revisionIds(Node::load(1));
319       $this->assertIdentical(2, count($vids), 'The node has two revisions.');
320       $revision = node_revision_load($vids[0]);
321       $this->assertIdentical($original_log, $revision->revision_log->value, 'The first revision log message is unchanged.');
322       $revision = node_revision_load($vids[1]);
323       $this->assertIdentical('Updated the <em class="placeholder">Body</em> field through in-place editing.', $revision->revision_log->value, 'The second revision log message was correctly generated by Quick Edit module.');
324     }
325   }
326
327   /**
328    * Test quickedit does not appear for entities with pending revisions.
329    */
330   public function testWithPendingRevision() {
331     $this->drupalLogin($this->editorUser);
332
333     // Verify that the preview is loaded correctly.
334     $this->drupalPostForm('node/add/article', ['title[0][value]' => 'foo'], 'Preview');
335     $this->assertResponse(200);
336     // Verify that quickedit is not active on preview.
337     $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
338     $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
339
340     $this->drupalGet('node/' . $this->testNode->id());
341     $this->assertRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
342     $this->assertRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
343
344     $this->testNode->title = 'Updated node';
345     $this->testNode->setNewRevision(TRUE);
346     $this->testNode->isDefaultRevision(FALSE);
347     $this->testNode->save();
348
349     $this->drupalGet('node/' . $this->testNode->id());
350     $this->assertResponse(200);
351     $this->assertNoRaw('data-quickedit-entity-id="node/' . $this->testNode->id() . '"');
352     $this->assertNoRaw('data-quickedit-field-id="node/' . $this->testNode->id() . '/title/' . $this->testNode->language()->getId() . '/full"');
353   }
354
355   /**
356    * Tests the loading of Quick Edit for the title base field.
357    */
358   public function testTitleBaseField() {
359     $this->drupalLogin($this->editorUser);
360     $this->drupalGet('node/1');
361
362     // Ensure that the full page title is actually in-place editable
363     $node = Node::load(1);
364     $elements = $this->xpath('//h1/span[@data-quickedit-field-id="node/1/title/en/full" and normalize-space(text())=:title]', [':title' => $node->label()]);
365     $this->assertTrue(!empty($elements), 'Title with data-quickedit-field-id attribute found.');
366
367     // Retrieving the metadata should result in a 200 JSON response.
368     $htmlPageDrupalSettings = $this->drupalSettings;
369     $post = ['fields[0]' => 'node/1/title/en/full'];
370     $response = $this->drupalPostWithFormat('quickedit/metadata', 'json', $post);
371     $this->assertResponse(200);
372     $expected = [
373       'node/1/title/en/full' => [
374         'label' => 'Title',
375         'access' => TRUE,
376         'editor' => 'plain_text',
377       ]
378     ];
379     $this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
380     // Restore drupalSettings to build the next requests; simpletest wipes them
381     // after a JSON response.
382     $this->drupalSettings = $htmlPageDrupalSettings;
383
384     // Retrieving the form for this field should result in a 200 response,
385     // containing only a quickeditFieldForm command.
386     $post = ['nocssjs' => 'true', 'reset' => 'true'] + $this->getAjaxPageStatePostData();
387     $response = $this->drupalPost('quickedit/form/' . 'node/1/title/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
388     $this->assertResponse(200);
389     $ajax_commands = Json::decode($response);
390     $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
391     $this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
392     $this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
393
394     // Prepare form values for submission. drupalPostAjaxForm() is not suitable
395     // for handling pages with JSON responses, so we need our own solution
396     // here.
397     $form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
398     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
399
400     if ($form_tokens_found) {
401       $edit = [
402         'title[0][value]' => 'Obligatory question',
403         'op' => t('Save'),
404       ];
405       $post = [
406         'form_id' => 'quickedit_field_form',
407         'form_token' => $token_match[1],
408         'form_build_id' => $build_id_match[1],
409       ];
410       $post += $edit + $this->getAjaxPageStatePostData();
411
412       // Submit field form and check response. This should store the
413       // updated entity in PrivateTempStore on the server.
414       $response = $this->drupalPost('quickedit/form/' . 'node/1/title/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
415       $this->assertResponse(200);
416       $ajax_commands = Json::decode($response);
417       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
418       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
419       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Obligatory question'), 'Form value saved and printed back.');
420
421       // Ensure the text on the original node did not change yet.
422       $this->drupalGet('node/1');
423       $this->assertNoText('Obligatory question');
424
425       // Save the entity by moving the PrivateTempStore values to entity storage.
426       $post = ['nocssjs' => 'true'];
427       $response = $this->drupalPostWithFormat('quickedit/entity/' . 'node/1', 'json', $post);
428       $this->assertResponse(200);
429       $ajax_commands = Json::decode($response);
430       $this->assertIdentical(1, count($ajax_commands), 'The entity submission HTTP request results in one AJAX command.');
431       $this->assertIdentical('quickeditEntitySaved', $ajax_commands[0]['command'], 'The first AJAX command is n quickeditEntitySaved command.');
432       $this->assertIdentical($ajax_commands[0]['data']['entity_type'], 'node', 'Saved entity is of type node.');
433       $this->assertIdentical($ajax_commands[0]['data']['entity_id'], '1', 'Entity id is 1.');
434
435       // Ensure the text on the original node did change.
436       $this->drupalGet('node/1');
437       $this->assertText('Obligatory question');
438     }
439   }
440
441   /**
442    * Tests that Quick Edit doesn't make fields rendered with display options
443    * editable.
444    */
445   public function testDisplayOptions() {
446     $node = Node::load('1');
447     $display_settings = [
448       'label' => 'inline',
449     ];
450     $build = $node->body->view($display_settings);
451     $output = \Drupal::service('renderer')->renderRoot($build);
452     $this->assertFalse(strpos($output, 'data-quickedit-field-id'), 'data-quickedit-field-id attribute not added when rendering field using dynamic display options.');
453   }
454
455   /**
456    * Tests that Quick Edit works with custom render pipelines.
457    */
458   public function testCustomPipeline() {
459     \Drupal::service('module_installer')->install(['quickedit_test']);
460
461     $custom_render_url = 'quickedit/form/node/1/body/en/quickedit_test-custom-render-data';
462     $this->drupalLogin($this->editorUser);
463
464     // Request editing to render results with the custom render pipeline.
465     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
466     $response = $this->drupalPost($custom_render_url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
467     $ajax_commands = Json::decode($response);
468
469     // Prepare form values for submission. drupalPostAJAX() is not suitable for
470     // handling pages with JSON responses, so we need our own solution here.
471     $form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
472     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
473
474     if ($form_tokens_found) {
475       $post = [
476         'form_id' => 'quickedit_field_form',
477         'form_token' => $token_match[1],
478         'form_build_id' => $build_id_match[1],
479         'body[0][summary]' => '',
480         'body[0][value]' => '<p>Fine thanks.</p>',
481         'body[0][format]' => 'filtered_html',
482         'op' => t('Save'),
483       ];
484       // Assume there is another field on this page, which doesn't use a custom
485       // render pipeline, but the default one, and it uses the "full" view mode.
486       $post += ['other_view_modes[]' => 'full'];
487
488       // Submit field form and check response. Should render with the custom
489       // render pipeline.
490       $response = $this->drupalPost($custom_render_url, '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
491       $this->assertResponse(200);
492       $ajax_commands = Json::decode($response);
493       $this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
494       $this->assertIdentical('quickeditFieldFormSaved', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldFormSaved command.');
495       $this->assertTrue(strpos($ajax_commands[0]['data'], 'Fine thanks.'), 'Form value saved and printed back.');
496       $this->assertTrue(strpos($ajax_commands[0]['data'], '<div class="quickedit-test-wrapper">') !== FALSE, 'Custom render pipeline used to render the value.');
497       $this->assertIdentical(array_keys($ajax_commands[0]['other_view_modes']), ['full'], 'Field was also rendered in the "full" view mode.');
498       $this->assertTrue(strpos($ajax_commands[0]['other_view_modes']['full'], 'Fine thanks.'), '"full" version of field contains the form value.');
499     }
500   }
501
502   /**
503    * Tests Quick Edit on a node that was concurrently edited on the full node
504    * form.
505    */
506   public function testConcurrentEdit() {
507     $this->drupalLogin($this->editorUser);
508
509     $post = ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData();
510     $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
511     $this->assertResponse(200);
512     $ajax_commands = Json::decode($response);
513
514     // Prepare form values for submission. drupalPostAJAX() is not suitable for
515     // handling pages with JSON responses, so we need our own solution here.
516     $form_tokens_found = preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match) && preg_match('/\sname="form_build_id" value="([^"]+)"/', $ajax_commands[0]['data'], $build_id_match);
517     $this->assertTrue($form_tokens_found, 'Form tokens found in output.');
518
519     if ($form_tokens_found) {
520       $post = [
521         'nocssjs' => 'true',
522         'form_id' => 'quickedit_field_form',
523         'form_token' => $token_match[1],
524         'form_build_id' => $build_id_match[1],
525         'body[0][summary]' => '',
526         'body[0][value]' => '<p>Fine thanks.</p>',
527         'body[0][format]' => 'filtered_html',
528         'op' => t('Save'),
529       ];
530
531       // Save the node on the regular node edit form.
532       $this->drupalPostForm('node/1/edit', [], t('Save'));
533       // Ensure different save timestamps for field editing.
534       sleep(2);
535
536       // Submit field form and check response. Should throw a validation error
537       // because the node was changed in the meantime.
538       $response = $this->drupalPost('quickedit/form/' . 'node/1/body/en/full', '', $post, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
539       $this->assertResponse(200);
540       $ajax_commands = Json::decode($response);
541       $this->assertIdentical(2, count($ajax_commands), 'The field form HTTP request results in two AJAX commands.');
542       $this->assertIdentical('quickeditFieldFormValidationErrors', $ajax_commands[1]['command'], 'The second AJAX command is a quickeditFieldFormValidationErrors command.');
543       $this->assertTrue(strpos($ajax_commands[1]['data'], 'The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.'), 'Error message returned to user.');
544     }
545   }
546
547   /**
548    * Tests that Quick Edit's data- attributes are present for content blocks.
549    */
550   public function testContentBlock() {
551     \Drupal::service('module_installer')->install(['block_content']);
552
553     // Create and place a content_block block.
554     $block = BlockContent::create([
555       'info' => $this->randomMachineName(),
556       'type' => 'basic',
557       'langcode' => 'en',
558     ]);
559     $block->save();
560     $this->drupalPlaceBlock('block_content:' . $block->uuid());
561
562     // Check that the data- attribute is present.
563     $this->drupalLogin($this->editorUser);
564     $this->drupalGet('');
565     $this->assertRaw('data-quickedit-entity-id="block_content/1"');
566   }
567
568   /**
569    * Tests that Quick Edit can handle an image field.
570    */
571   public function testImageField() {
572     // Add an image field to the content type.
573     FieldStorageConfig::create([
574       'field_name' => 'field_image',
575       'type' => 'image',
576       'entity_type' => 'node',
577     ])->save();
578     FieldConfig::create([
579       'field_name' => 'field_image',
580       'field_type' => 'image',
581       'label' => t('Image'),
582       'entity_type' => 'node',
583       'bundle' => 'article',
584     ])->save();
585     entity_get_form_display('node', 'article', 'default')
586       ->setComponent('field_image', [
587         'type' => 'image_image',
588       ])
589       ->save();
590
591     // Add an image to the node.
592     $this->drupalLogin($this->editorUser);
593     $image = $this->drupalGetTestFiles('image')[0];
594     $this->drupalPostForm('node/1/edit', [
595       'files[field_image_0]' => $image->uri,
596     ], t('Upload'));
597     $this->drupalPostForm(NULL, [
598       'field_image[0][alt]' => 'Vivamus aliquet elit',
599     ], t('Save'));
600
601     // The image field form should load normally.
602     $response = $this->drupalPost('quickedit/form/node/1/field_image/en/full', '', ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData(), ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
603     $this->assertResponse(200);
604     $ajax_commands = Json::decode($response);
605     $this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
606   }
607
608 }