Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / content_moderation / tests / src / Kernel / ContentModerationStateTest.php
1 <?php
2
3 namespace Drupal\Tests\content_moderation\Kernel;
4
5 use Drupal\content_moderation\Entity\ContentModerationState;
6 use Drupal\Core\Entity\EntityInterface;
7 use Drupal\Core\Entity\EntityPublishedInterface;
8 use Drupal\Core\Entity\EntityStorageException;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\entity_test\Entity\EntityTestRev;
11 use Drupal\KernelTests\KernelTestBase;
12 use Drupal\language\Entity\ConfigurableLanguage;
13 use Drupal\node\Entity\Node;
14 use Drupal\node\Entity\NodeType;
15 use Drupal\workflows\Entity\Workflow;
16
17 /**
18  * Tests links between a content entity and a content_moderation_state entity.
19  *
20  * @group content_moderation
21  */
22 class ContentModerationStateTest extends KernelTestBase {
23
24   /**
25    * {@inheritdoc}
26    */
27   public static $modules = [
28     'entity_test',
29     'node',
30     'block',
31     'block_content',
32     'media',
33     'media_test_source',
34     'image',
35     'file',
36     'field',
37     'content_moderation',
38     'user',
39     'system',
40     'language',
41     'content_translation',
42     'text',
43     'workflows',
44   ];
45
46   /**
47    * @var \Drupal\Core\Entity\EntityTypeManager
48    */
49   protected $entityTypeManager;
50
51   /**
52    * {@inheritdoc}
53    */
54   protected function setUp() {
55     parent::setUp();
56
57     $this->installSchema('node', 'node_access');
58     $this->installEntitySchema('node');
59     $this->installEntitySchema('user');
60     $this->installEntitySchema('entity_test_rev');
61     $this->installEntitySchema('entity_test_no_bundle');
62     $this->installEntitySchema('entity_test_mulrevpub');
63     $this->installEntitySchema('block_content');
64     $this->installEntitySchema('media');
65     $this->installEntitySchema('file');
66     $this->installEntitySchema('content_moderation_state');
67     $this->installConfig('content_moderation');
68     $this->installSchema('file', 'file_usage');
69     $this->installConfig(['field', 'system', 'image', 'file', 'media']);
70
71     $this->entityTypeManager = $this->container->get('entity_type.manager');
72   }
73
74   /**
75    * Tests basic monolingual content moderation through the API.
76    *
77    * @dataProvider basicModerationTestCases
78    */
79   public function testBasicModeration($entity_type_id) {
80     $entity = $this->createEntity($entity_type_id);
81     if ($entity instanceof EntityPublishedInterface) {
82       $entity->setUnpublished();
83     }
84     $entity->save();
85     $entity = $this->reloadEntity($entity);
86     $this->assertEquals('draft', $entity->moderation_state->value);
87
88     $entity->moderation_state->value = 'published';
89     $entity->save();
90
91     $entity = $this->reloadEntity($entity);
92     $this->assertEquals('published', $entity->moderation_state->value);
93
94     // Change the state without saving the node.
95     $content_moderation_state = ContentModerationState::load(1);
96     $content_moderation_state->set('moderation_state', 'draft');
97     $content_moderation_state->setNewRevision(TRUE);
98     $content_moderation_state->save();
99
100     $entity = $this->reloadEntity($entity, 3);
101     $this->assertEquals('draft', $entity->moderation_state->value);
102     if ($entity instanceof EntityPublishedInterface) {
103       $this->assertFalse($entity->isPublished());
104     }
105
106     // Get the default revision.
107     $entity = $this->reloadEntity($entity);
108     if ($entity instanceof EntityPublishedInterface) {
109       $this->assertTrue((bool) $entity->isPublished());
110     }
111     $this->assertEquals(2, $entity->getRevisionId());
112
113     $entity->moderation_state->value = 'published';
114     $entity->save();
115
116     $entity = $this->reloadEntity($entity, 4);
117     $this->assertEquals('published', $entity->moderation_state->value);
118
119     // Get the default revision.
120     $entity = $this->reloadEntity($entity);
121     if ($entity instanceof EntityPublishedInterface) {
122       $this->assertTrue((bool) $entity->isPublished());
123     }
124     $this->assertEquals(4, $entity->getRevisionId());
125
126     // Update the node to archived which will then be the default revision.
127     $entity->moderation_state->value = 'archived';
128     $entity->save();
129
130     // Revert to the previous (published) revision.
131     $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
132     $previous_revision = $entity_storage->loadRevision(4);
133     $previous_revision->isDefaultRevision(TRUE);
134     $previous_revision->setNewRevision(TRUE);
135     $previous_revision->save();
136
137     // Get the default revision.
138     $entity = $this->reloadEntity($entity);
139     $this->assertEquals('published', $entity->moderation_state->value);
140     if ($entity instanceof EntityPublishedInterface) {
141       $this->assertTrue($entity->isPublished());
142     }
143
144     // Set an invalid moderation state.
145     $this->setExpectedException(EntityStorageException::class);
146     $entity->moderation_state->value = 'foobar';
147     $entity->save();
148   }
149
150   /**
151    * Test cases for basic moderation test.
152    */
153   public function basicModerationTestCases() {
154     return [
155       'Nodes' => [
156         'node',
157       ],
158       'Block content' => [
159         'block_content',
160       ],
161       'Media' => [
162         'media',
163       ],
164       'Test entity - revisions, data table, and published interface' => [
165         'entity_test_mulrevpub',
166       ],
167       'Entity Test with revisions' => [
168         'entity_test_rev',
169       ],
170       'Entity without bundle' => [
171         'entity_test_no_bundle',
172       ],
173     ];
174   }
175
176   /**
177    * Tests removal of content moderation state entity field data.
178    *
179    * @dataProvider basicModerationTestCases
180    */
181   public function testContentModerationStateDataRemoval($entity_type_id) {
182     // Test content moderation state deletion.
183     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
184     $entity = $this->createEntity($entity_type_id);
185     $entity->save();
186     $entity = $this->reloadEntity($entity);
187     $entity->delete();
188     $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
189     $this->assertFalse($content_moderation_state);
190
191     // Test content moderation state revision deletion.
192     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity2 */
193     $entity2 = $this->createEntity($entity_type_id);
194     $entity2->save();
195     $revision = clone $entity2;
196     $revision->isDefaultRevision(FALSE);
197     $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
198     $this->assertTrue($content_moderation_state);
199     $entity2 = $this->reloadEntity($entity2);
200     $entity2->setNewRevision(TRUE);
201     $entity2->save();
202     $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
203     $entity_storage->deleteRevision($revision->getRevisionId());
204     $content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
205     $this->assertFalse($content_moderation_state);
206     $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity2);
207     $this->assertTrue($content_moderation_state);
208
209     // Test content moderation state translation deletion.
210     if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
211       /** @var \Drupal\Core\Entity\ContentEntityInterface $entity3 */
212       $entity3 = $this->createEntity($entity_type_id);
213       $langcode = 'it';
214       ConfigurableLanguage::createFromLangcode($langcode)
215         ->save();
216       $entity3->save();
217       $translation = $entity3->addTranslation($langcode, ['title' => 'Titolo test']);
218       // Make sure we add values for all of the required fields.
219       if ($entity_type_id == 'block_content') {
220         $translation->info = $this->randomString();
221       }
222       $translation->save();
223       $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
224       $this->assertTrue($content_moderation_state->hasTranslation($langcode));
225       $entity3->removeTranslation($langcode);
226       $entity3->save();
227       $content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
228       $this->assertFalse($content_moderation_state->hasTranslation($langcode));
229     }
230   }
231
232   /**
233    * Tests basic multilingual content moderation through the API.
234    */
235   public function testMultilingualModeration() {
236     // Enable French.
237     ConfigurableLanguage::createFromLangcode('fr')->save();
238     $node_type = NodeType::create([
239       'type' => 'example',
240     ]);
241     $node_type->save();
242
243     $workflow = Workflow::load('editorial');
244     $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
245     $workflow->save();
246
247     $english_node = Node::create([
248       'type' => 'example',
249       'title' => 'Test title',
250     ]);
251     // Revision 1 (en).
252     $english_node
253       ->setUnpublished()
254       ->save();
255     $this->assertEquals('draft', $english_node->moderation_state->value);
256     $this->assertFalse($english_node->isPublished());
257
258     // Create a French translation.
259     $french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
260     $french_node->setUnpublished();
261     // Revision 1 (fr).
262     $french_node->save();
263     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
264     $this->assertEquals('draft', $french_node->moderation_state->value);
265     $this->assertFalse($french_node->isPublished());
266
267     // Move English node to create another draft.
268     $english_node = $this->reloadEntity($english_node);
269     $english_node->moderation_state->value = 'draft';
270     // Revision 2 (en, fr).
271     $english_node->save();
272     $english_node = $this->reloadEntity($english_node);
273     $this->assertEquals('draft', $english_node->moderation_state->value);
274
275     // French node should still be in draft.
276     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
277     $this->assertEquals('draft', $french_node->moderation_state->value);
278
279     // Publish the French node.
280     $french_node->moderation_state->value = 'published';
281     // Revision 3 (en, fr).
282     $french_node->save();
283     $french_node = $this->reloadEntity($french_node)->getTranslation('fr');
284     $this->assertTrue($french_node->isPublished());
285     $this->assertEquals('published', $french_node->moderation_state->value);
286     $this->assertTrue($french_node->isPublished());
287     $english_node = $french_node->getTranslation('en');
288     $this->assertEquals('draft', $english_node->moderation_state->value);
289
290     // Publish the English node.
291     $english_node->moderation_state->value = 'published';
292     // Revision 4 (en, fr).
293     $english_node->save();
294     $english_node = $this->reloadEntity($english_node);
295     $this->assertTrue($english_node->isPublished());
296
297     // Move the French node back to draft.
298     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
299     $this->assertTrue($french_node->isPublished());
300     $french_node->moderation_state->value = 'draft';
301     // Revision 5 (en, fr).
302     $french_node->save();
303     $french_node = $this->reloadEntity($english_node, 5)->getTranslation('fr');
304     $this->assertFalse($french_node->isPublished());
305     $this->assertTrue($french_node->getTranslation('en')->isPublished());
306
307     // Republish the French node.
308     $french_node->moderation_state->value = 'published';
309     // Revision 6 (en, fr).
310     $french_node->save();
311     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
312     $this->assertTrue($french_node->isPublished());
313
314     // Change the EN state without saving the node.
315     $content_moderation_state = ContentModerationState::load(1);
316     $content_moderation_state->set('moderation_state', 'draft');
317     $content_moderation_state->setNewRevision(TRUE);
318     // Revision 7 (en, fr).
319     $content_moderation_state->save();
320     $english_node = $this->reloadEntity($french_node, $french_node->getRevisionId() + 1);
321
322     $this->assertEquals('draft', $english_node->moderation_state->value);
323     $french_node = $this->reloadEntity($english_node)->getTranslation('fr');
324     $this->assertEquals('published', $french_node->moderation_state->value);
325
326     // This should unpublish the French node.
327     $content_moderation_state = ContentModerationState::load(1);
328     $content_moderation_state = $content_moderation_state->getTranslation('fr');
329     $content_moderation_state->set('moderation_state', 'draft');
330     $content_moderation_state->setNewRevision(TRUE);
331     // Revision 8 (en, fr).
332     $content_moderation_state->save();
333
334     $english_node = $this->reloadEntity($english_node, $english_node->getRevisionId());
335     $this->assertEquals('draft', $english_node->moderation_state->value);
336     $french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
337     $this->assertEquals('draft', $french_node->moderation_state->value);
338     // Switching the moderation state to an unpublished state should update the
339     // entity.
340     $this->assertFalse($french_node->isPublished());
341
342     // Get the default english node.
343     $english_node = $this->reloadEntity($english_node);
344     $this->assertTrue($english_node->isPublished());
345     $this->assertEquals(6, $english_node->getRevisionId());
346   }
347
348   /**
349    * Tests moderation when the moderation_state field has a config override.
350    */
351   public function testModerationWithFieldConfigOverride() {
352     NodeType::create([
353       'type' => 'test_type',
354     ])->save();
355
356     $workflow = Workflow::load('editorial');
357     $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
358     $workflow->save();
359
360     $fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
361     $field_config = $fields['moderation_state']->getConfig('test_type');
362     $field_config->setLabel('Field Override!');
363     $field_config->save();
364
365     $node = Node::create([
366       'title' => 'Test node',
367       'type' => 'test_type',
368     ]);
369     $node->save();
370     $this->assertFalse($node->isPublished());
371     $this->assertEquals('draft', $node->moderation_state->value);
372
373     $node->moderation_state = 'published';
374     $node->save();
375     $this->assertTrue($node->isPublished());
376     $this->assertEquals('published', $node->moderation_state->value);
377   }
378
379   /**
380    * Tests that entities with special languages can be moderated.
381    */
382   public function testModerationWithSpecialLanguages() {
383     $workflow = Workflow::load('editorial');
384     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
385     $workflow->save();
386
387     // Create a test entity.
388     $entity = EntityTestRev::create([
389       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
390     ]);
391     $entity->save();
392     $this->assertEquals('draft', $entity->moderation_state->value);
393
394     $entity->moderation_state->value = 'published';
395     $entity->save();
396
397     $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
398   }
399
400   /**
401    * Tests that a non-translatable entity type with a langcode can be moderated.
402    */
403   public function testNonTranslatableEntityTypeModeration() {
404     $workflow = Workflow::load('editorial');
405     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
406     $workflow->save();
407
408     // Check that the tested entity type is not translatable.
409     $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
410     $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
411
412     // Create a test entity.
413     $entity = EntityTestRev::create();
414     $entity->save();
415     $this->assertEquals('draft', $entity->moderation_state->value);
416
417     $entity->moderation_state->value = 'published';
418     $entity->save();
419
420     $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
421   }
422
423   /**
424    * Tests that a non-translatable entity type without a langcode can be
425    * moderated.
426    */
427   public function testNonLangcodeEntityTypeModeration() {
428     // Unset the langcode entity key for 'entity_test_rev'.
429     $entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
430     $keys = $entity_type->getKeys();
431     unset($keys['langcode']);
432     $entity_type->set('entity_keys', $keys);
433     \Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
434
435     // Update the entity type in order to remove the 'langcode' field.
436     \Drupal::entityDefinitionUpdateManager()->applyUpdates();
437
438     $workflow = Workflow::load('editorial');
439     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
440     $workflow->save();
441
442     // Check that the tested entity type is not translatable and does not have a
443     // 'langcode' entity key.
444     $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
445     $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
446     $this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
447
448     // Create a test entity.
449     $entity = EntityTestRev::create();
450     $entity->save();
451     $this->assertEquals('draft', $entity->moderation_state->value);
452
453     $entity->moderation_state->value = 'published';
454     $entity->save();
455
456     $this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
457   }
458
459   /**
460    * Tests the dependencies of the workflow when using content moderation.
461    */
462   public function testWorkflowDependencies() {
463     $node_type = NodeType::create([
464       'type' => 'example',
465     ]);
466     $node_type->save();
467
468     $workflow = Workflow::load('editorial');
469     // Test both a config and non-config based bundle and entity type.
470     $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
471     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
472     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
473     $workflow->save();
474
475     $this->assertEquals([
476       'module' => [
477         'content_moderation',
478         'entity_test',
479       ],
480       'config' => [
481         'node.type.example',
482       ],
483     ], $workflow->getDependencies());
484
485     $this->assertEquals([
486       'entity_test_no_bundle',
487       'entity_test_rev',
488       'node'
489     ], $workflow->getTypePlugin()->getEntityTypes());
490
491     // Delete the node type and ensure it is removed from the workflow.
492     $node_type->delete();
493     $workflow = Workflow::load('editorial');
494     $entity_types = $workflow->getTypePlugin()->getEntityTypes();
495     $this->assertFalse(in_array('node', $entity_types));
496
497     // Uninstall entity test and ensure it's removed from the workflow.
498     $this->container->get('config.manager')->uninstall('module', 'entity_test');
499     $workflow = Workflow::load('editorial');
500     $entity_types = $workflow->getTypePlugin()->getEntityTypes();
501     $this->assertEquals([], $entity_types);
502   }
503
504   /**
505    * Test the content moderation workflow dependencies for non-config bundles.
506    */
507   public function testWorkflowNonConfigBundleDependencies() {
508     // Create a bundle not based on any particular configuration.
509     entity_test_create_bundle('test_bundle');
510
511     $workflow = Workflow::load('editorial');
512     $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test', 'test_bundle');
513     $workflow->save();
514
515     // Ensure the bundle is correctly added to the workflow.
516     $this->assertEquals([
517       'module' => [
518         'content_moderation',
519         'entity_test',
520       ],
521     ], $workflow->getDependencies());
522     $this->assertEquals([
523       'test_bundle',
524     ], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
525
526     // Delete the test bundle to ensure the workflow entity responds
527     // appropriately.
528     entity_test_delete_bundle('test_bundle');
529
530     $workflow = Workflow::load('editorial');
531     $this->assertEquals([], $workflow->getTypePlugin()->getBundlesForEntityType('entity_test'));
532     $this->assertEquals([
533       'module' => [
534         'content_moderation',
535       ],
536     ], $workflow->getDependencies());
537   }
538
539   /**
540    * Creates an entity.
541    *
542    * The entity will have required fields populated and the corresponding bundle
543    * will be enabled for content moderation.
544    *
545    * @param string $entity_type_id
546    *   The entity type ID.
547    *
548    * @return \Drupal\Core\Entity\ContentEntityInterface
549    *   The created entity.
550    */
551   protected function createEntity($entity_type_id) {
552     $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
553
554     $bundle_id = $entity_type_id;
555     // Set up a bundle entity type for the specified entity type, if needed.
556     if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) {
557       $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
558       $bundle_entity_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
559
560       $bundle_id = 'example';
561       if (!$bundle_entity_storage->load($bundle_id)) {
562         $bundle_entity = $bundle_entity_storage->create([
563           $bundle_entity_type->getKey('id') => 'example',
564         ]);
565         if ($entity_type_id == 'media') {
566           $bundle_entity->set('source', 'test');
567           $bundle_entity->save();
568           $source_field = $bundle_entity->getSource()->createSourceField($bundle_entity);
569           $source_field->getFieldStorageDefinition()->save();
570           $source_field->save();
571           $bundle_entity->set('source_configuration', [
572             'source_field' => $source_field->getName(),
573           ]);
574         }
575         $bundle_entity->save();
576       }
577     }
578
579     $workflow = Workflow::load('editorial');
580     $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
581     $workflow->save();
582
583     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
584     $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
585     $entity = $entity_storage->create([
586       $entity_type->getKey('label') => 'Test title',
587       $entity_type->getKey('bundle') => $bundle_id,
588     ]);
589     // Make sure we add values for all of the required fields.
590     if ($entity_type_id == 'block_content') {
591       $entity->info = $this->randomString();
592     }
593     return $entity;
594   }
595
596   /**
597    * Reloads the entity after clearing the static cache.
598    *
599    * @param \Drupal\Core\Entity\EntityInterface $entity
600    *   The entity to reload.
601    * @param int|bool $revision_id
602    *   The specific revision ID to load. Defaults FALSE and just loads the
603    *   default revision.
604    *
605    * @return \Drupal\Core\Entity\EntityInterface
606    *   The reloaded entity.
607    */
608   protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
609     $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
610     $storage->resetCache([$entity->id()]);
611     if ($revision_id) {
612       return $storage->loadRevision($revision_id);
613     }
614     return $storage->load($entity->id());
615   }
616
617 }