Version 1
[yaffs-website] / web / core / modules / filter / tests / src / Kernel / FilterAPITest.php
1 <?php
2
3 namespace Drupal\Tests\filter\Kernel;
4
5 use Drupal\Core\Language\LanguageInterface;
6 use Drupal\Core\Session\AnonymousUserSession;
7 use Drupal\Core\TypedData\OptionsProviderInterface;
8 use Drupal\Core\TypedData\DataDefinition;
9 use Drupal\filter\Entity\FilterFormat;
10 use Drupal\filter\Plugin\DataType\FilterFormat as FilterFormatDataType;
11 use Drupal\filter\Plugin\FilterInterface;
12 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
13 use Symfony\Component\Validator\ConstraintViolationListInterface;
14
15 /**
16  * Tests the behavior of the API of the Filter module.
17  *
18  * @group filter
19  */
20 class FilterAPITest extends EntityKernelTestBase {
21
22   public static $modules = ['system', 'filter', 'filter_test', 'user'];
23
24   protected function setUp() {
25     parent::setUp();
26
27     $this->installConfig(['system', 'filter', 'filter_test']);
28   }
29
30   /**
31    * Tests that the filter order is respected.
32    */
33   public function testCheckMarkupFilterOrder() {
34     // Create crazy HTML format.
35     $crazy_format = FilterFormat::create([
36       'format' => 'crazy',
37       'name' => 'Crazy',
38       'weight' => 1,
39       'filters' => [
40         'filter_html_escape' => [
41           'weight' => 10,
42           'status' => 1,
43         ],
44         'filter_html' => [
45           'weight' => -10,
46           'status' => 1,
47           'settings' => [
48             'allowed_html' => '<p>',
49           ],
50         ],
51       ]
52     ]);
53     $crazy_format->save();
54
55     $text = "<p>Llamas are <not> awesome!</p>";
56     $expected_filtered_text = "&lt;p&gt;Llamas are  awesome!&lt;/p&gt;";
57
58     $this->assertEqual(check_markup($text, 'crazy'), $expected_filtered_text, 'Filters applied in correct order.');
59   }
60
61   /**
62    * Tests the ability to apply only a subset of filters.
63    */
64   public function testCheckMarkupFilterSubset() {
65     $text = "Text with <marquee>evil content and</marquee> a URL: https://www.drupal.org!";
66     $expected_filtered_text = "Text with evil content and a URL: <a href=\"https://www.drupal.org\">https://www.drupal.org</a>!";
67     $expected_filter_text_without_html_generators = "Text with evil content and a URL: https://www.drupal.org!";
68
69     $actual_filtered_text = check_markup($text, 'filtered_html', '', []);
70     $this->verbose("Actual:<pre>$actual_filtered_text</pre>Expected:<pre>$expected_filtered_text</pre>");
71     $this->assertEqual(
72       $actual_filtered_text,
73       $expected_filtered_text,
74       'Expected filter result.'
75     );
76     $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', [FilterInterface::TYPE_MARKUP_LANGUAGE]);
77     $this->verbose("Actual:<pre>$actual_filtered_text_without_html_generators</pre>Expected:<pre>$expected_filter_text_without_html_generators</pre>");
78     $this->assertEqual(
79       $actual_filtered_text_without_html_generators,
80       $expected_filter_text_without_html_generators,
81       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters.'
82     );
83     // Related to @see FilterSecurityTest.php/testSkipSecurityFilters(), but
84     // this check focuses on the ability to filter multiple filter types at once.
85     // Drupal core only ships with these two types of filters, so this is the
86     // most extensive test possible.
87     $actual_filtered_text_without_html_generators = check_markup($text, 'filtered_html', '', [FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE]);
88     $this->verbose("Actual:<pre>$actual_filtered_text_without_html_generators</pre>Expected:<pre>$expected_filter_text_without_html_generators</pre>");
89     $this->assertEqual(
90       $actual_filtered_text_without_html_generators,
91       $expected_filter_text_without_html_generators,
92       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.'
93     );
94   }
95
96   /**
97    * Tests the following functions for a variety of formats:
98    *   - \Drupal\filter\Entity\FilterFormatInterface::getHtmlRestrictions()
99    *   - \Drupal\filter\Entity\FilterFormatInterface::getFilterTypes()
100    */
101   public function testFilterFormatAPI() {
102     // Test on filtered_html.
103     $filtered_html_format = FilterFormat::load('filtered_html');
104     $this->assertIdentical(
105       $filtered_html_format->getHtmlRestrictions(),
106       [
107         'allowed' => [
108           'p' => FALSE,
109           'br' => FALSE,
110           'strong' => FALSE,
111           'a' => ['href' => TRUE, 'hreflang' => TRUE],
112           '*' => ['style' => FALSE, 'on*' => FALSE, 'lang' => TRUE, 'dir' => ['ltr' => TRUE, 'rtl' => TRUE]],
113         ],
114       ],
115       'FilterFormatInterface::getHtmlRestrictions() works as expected for the filtered_html format.'
116     );
117     $this->assertIdentical(
118       $filtered_html_format->getFilterTypes(),
119       [FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE],
120       'FilterFormatInterface::getFilterTypes() works as expected for the filtered_html format.'
121     );
122
123     // Test on full_html.
124     $full_html_format = FilterFormat::load('full_html');
125     $this->assertIdentical(
126       $full_html_format->getHtmlRestrictions(),
127       FALSE, // Every tag is allowed.
128       'FilterFormatInterface::getHtmlRestrictions() works as expected for the full_html format.'
129     );
130     $this->assertIdentical(
131       $full_html_format->getFilterTypes(),
132       [],
133       'FilterFormatInterface::getFilterTypes() works as expected for the full_html format.'
134     );
135
136     // Test on stupid_filtered_html, where nothing is allowed.
137     $stupid_filtered_html_format = FilterFormat::create([
138       'format' => 'stupid_filtered_html',
139       'name' => 'Stupid Filtered HTML',
140       'filters' => [
141         'filter_html' => [
142           'status' => 1,
143           'settings' => [
144             'allowed_html' => '', // Nothing is allowed.
145           ],
146         ],
147       ],
148     ]);
149     $stupid_filtered_html_format->save();
150     $this->assertIdentical(
151       $stupid_filtered_html_format->getHtmlRestrictions(),
152       ['allowed' => []], // No tag is allowed.
153       'FilterFormatInterface::getHtmlRestrictions() works as expected for the stupid_filtered_html format.'
154     );
155     $this->assertIdentical(
156       $stupid_filtered_html_format->getFilterTypes(),
157       [FilterInterface::TYPE_HTML_RESTRICTOR],
158       'FilterFormatInterface::getFilterTypes() works as expected for the stupid_filtered_html format.'
159     );
160
161     // Test on very_restricted_html, where there's two different filters of the
162     // FilterInterface::TYPE_HTML_RESTRICTOR type, each restricting in different ways.
163     $very_restricted_html_format = FilterFormat::create([
164       'format' => 'very_restricted_html',
165       'name' => 'Very Restricted HTML',
166       'filters' => [
167         'filter_html' => [
168           'status' => 1,
169           'settings' => [
170             'allowed_html' => '<p> <br> <a href> <strong>',
171           ],
172         ],
173         'filter_test_restrict_tags_and_attributes' => [
174           'status' => 1,
175           'settings' => [
176             'restrictions' => [
177               'allowed' => [
178                 'p' => TRUE,
179                 'br' => FALSE,
180                 'a' => ['href' => TRUE],
181                 'em' => TRUE,
182               ],
183             ]
184           ],
185         ],
186       ]
187     ]);
188     $very_restricted_html_format->save();
189     $this->assertIdentical(
190       $very_restricted_html_format->getHtmlRestrictions(),
191       [
192         'allowed' => [
193           'p' => FALSE,
194           'br' => FALSE,
195           'a' => ['href' => TRUE],
196           '*' => ['style' => FALSE, 'on*' => FALSE, 'lang' => TRUE, 'dir' => ['ltr' => TRUE, 'rtl' => TRUE]],
197         ],
198       ],
199       'FilterFormatInterface::getHtmlRestrictions() works as expected for the very_restricted_html format.'
200     );
201     $this->assertIdentical(
202       $very_restricted_html_format->getFilterTypes(),
203       [FilterInterface::TYPE_HTML_RESTRICTOR],
204       'FilterFormatInterface::getFilterTypes() works as expected for the very_restricted_html format.'
205     );
206
207     // Test on nonsensical_restricted_html, where the allowed attribute values
208     // contain asterisks, which do not have any meaning, but which we also
209     // cannot prevent because configuration can be modified outside of forms.
210     $nonsensical_restricted_html = FilterFormat::create([
211       'format' => 'nonsensical_restricted_html',
212       'name' => 'Nonsensical Restricted HTML',
213       'filters' => [
214         'filter_html' => [
215           'status' => 1,
216           'settings' => [
217             'allowed_html' => '<a> <b class> <c class="*"> <d class="foo bar-* *">',
218           ],
219         ],
220       ]
221     ]);
222     $nonsensical_restricted_html->save();
223     $this->assertIdentical(
224       $nonsensical_restricted_html->getHtmlRestrictions(),
225       [
226         'allowed' => [
227           'a' => FALSE,
228           'b' => ['class' => TRUE],
229           'c' => ['class' => TRUE],
230           'd' => ['class' => ['foo' => TRUE, 'bar-*' => TRUE]],
231           '*' => ['style' => FALSE, 'on*' => FALSE, 'lang' => TRUE, 'dir' => ['ltr' => TRUE, 'rtl' => TRUE]],
232         ],
233       ],
234       'FilterFormatInterface::getHtmlRestrictions() works as expected for the nonsensical_restricted_html format.'
235     );
236     $this->assertIdentical(
237       $very_restricted_html_format->getFilterTypes(),
238       [FilterInterface::TYPE_HTML_RESTRICTOR],
239       'FilterFormatInterface::getFilterTypes() works as expected for the very_restricted_html format.'
240     );
241   }
242
243   /**
244    * Tests the 'processed_text' element.
245    *
246    * check_markup() is a wrapper for the 'processed_text' element, for use in
247    * simple scenarios; the 'processed_text' element has more advanced features:
248    * it lets filters attach assets, associate cache tags and define
249    * #lazy_builder callbacks.
250    * This test focuses solely on those advanced features.
251    */
252   public function testProcessedTextElement() {
253     FilterFormat::create([
254       'format' => 'element_test',
255       'name' => 'processed_text element test format',
256       'filters' => [
257         'filter_test_assets' => [
258           'weight' => -1,
259           'status' => TRUE,
260         ],
261         'filter_test_cache_tags' => [
262           'weight' => 0,
263           'status' => TRUE,
264         ],
265         'filter_test_cache_contexts' => [
266           'weight' => 0,
267           'status' => TRUE,
268         ],
269         'filter_test_cache_merge' => [
270           'weight' => 0,
271           'status' => TRUE,
272         ],
273         'filter_test_placeholders' => [
274           'weight' => 1,
275           'status' => TRUE,
276         ],
277         // Run the HTML corrector filter last, because it has the potential to
278         // break the placeholders added by the filter_test_placeholders filter.
279         'filter_htmlcorrector' => [
280           'weight' => 10,
281           'status' => TRUE,
282         ],
283       ],
284     ])->save();
285
286     $build = [
287       '#type' => 'processed_text',
288       '#text' => '<p>Hello, world!</p>',
289       '#format' => 'element_test',
290     ];
291     drupal_render_root($build);
292
293     // Verify the attachments and cacheability metadata.
294     $expected_attachments = [
295       // The assets attached by the filter_test_assets filter.
296       'library' => [
297         'filter/caption',
298       ],
299       // The placeholders attached that still need to be processed.
300       'placeholders' => [],
301     ];
302     $this->assertEqual($expected_attachments, $build['#attached'], 'Expected attachments present');
303     $expected_cache_tags = [
304       // The cache tag set by the processed_text element itself.
305       'config:filter.format.element_test',
306       // The cache tags set by the filter_test_cache_tags filter.
307       'foo:bar',
308       'foo:baz',
309       // The cache tags set by the filter_test_cache_merge filter.
310       'merge:tag',
311     ];
312     $this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
313     $expected_cache_contexts = [
314       // The cache context set by the filter_test_cache_contexts filter.
315       'languages:' . LanguageInterface::TYPE_CONTENT,
316       // The default cache contexts for Renderer.
317       'languages:' . LanguageInterface::TYPE_INTERFACE,
318       'theme',
319       // The cache tags set by the filter_test_cache_merge filter.
320       'user.permissions',
321     ];
322     $this->assertEqual($expected_cache_contexts, $build['#cache']['contexts'], 'Expected cache contexts present.');
323     $expected_markup = '<p>Hello, world!</p><p>This is a dynamic llama.</p>';
324     $this->assertEqual($expected_markup, $build['#markup'], 'Expected #lazy_builder callback has been applied.');
325   }
326
327   /**
328    * Tests the function of the typed data type.
329    */
330   public function testTypedDataAPI() {
331     $definition = DataDefinition::create('filter_format');
332     $data = \Drupal::typedDataManager()->create($definition);
333
334     $this->assertTrue($data instanceof OptionsProviderInterface, 'Typed data object implements \Drupal\Core\TypedData\OptionsProviderInterface');
335
336     $filtered_html_user = $this->createUser(['uid' => 2], [
337       FilterFormat::load('filtered_html')->getPermissionName(),
338     ]);
339
340     // Test with anonymous user.
341     $user = new AnonymousUserSession();
342     \Drupal::currentUser()->setAccount($user);
343
344     $expected_available_options = [
345       'filtered_html' => 'Filtered HTML',
346       'full_html' => 'Full HTML',
347       'filter_test' => 'Test format',
348       'plain_text' => 'Plain text',
349     ];
350
351     $available_values = $data->getPossibleValues();
352     $this->assertEqual($available_values, array_keys($expected_available_options));
353     $available_options = $data->getPossibleOptions();
354     $this->assertEqual($available_options, $expected_available_options);
355
356     $allowed_values = $data->getSettableValues($user);
357     $this->assertEqual($allowed_values, ['plain_text']);
358     $allowed_options = $data->getSettableOptions($user);
359     $this->assertEqual($allowed_options, ['plain_text' => 'Plain text']);
360
361     $data->setValue('foo');
362     $violations = $data->validate();
363     $this->assertFilterFormatViolation($violations, 'foo');
364
365     // Make sure the information provided by a violation is correct.
366     $violation = $violations[0];
367     $this->assertEqual($violation->getRoot(), $data, 'Violation root is filter format.');
368     $this->assertEqual($violation->getPropertyPath(), '', 'Violation property path is correct.');
369     $this->assertEqual($violation->getInvalidValue(), 'foo', 'Violation contains invalid value.');
370
371     $data->setValue('plain_text');
372     $violations = $data->validate();
373     $this->assertEqual(count($violations), 0, "No validation violation for format 'plain_text' found");
374
375     // Anonymous doesn't have access to the 'filtered_html' format.
376     $data->setValue('filtered_html');
377     $violations = $data->validate();
378     $this->assertFilterFormatViolation($violations, 'filtered_html');
379
380     // Set user with access to 'filtered_html' format.
381     \Drupal::currentUser()->setAccount($filtered_html_user);
382     $violations = $data->validate();
383     $this->assertEqual(count($violations), 0, "No validation violation for accessible format 'filtered_html' found.");
384
385     $allowed_values = $data->getSettableValues($filtered_html_user);
386     $this->assertEqual($allowed_values, ['filtered_html', 'plain_text']);
387     $allowed_options = $data->getSettableOptions($filtered_html_user);
388     $expected_allowed_options = [
389       'filtered_html' => 'Filtered HTML',
390       'plain_text' => 'Plain text',
391     ];
392     $this->assertEqual($allowed_options, $expected_allowed_options);
393   }
394
395   /**
396    * Tests that FilterFormat::preSave() only saves customized plugins.
397    */
398   public function testFilterFormatPreSave() {
399     /** @var \Drupal\filter\FilterFormatInterface $crazy_format */
400     $crazy_format = FilterFormat::create([
401       'format' => 'crazy',
402       'name' => 'Crazy',
403       'weight' => 1,
404       'filters' => [
405         'filter_html_escape' => [
406           'weight' => 10,
407           'status' => 1,
408         ],
409         'filter_html' => [
410           'weight' => -10,
411           'status' => 1,
412           'settings' => [
413             'allowed_html' => '<p>',
414           ],
415         ],
416       ]
417     ]);
418     $crazy_format->save();
419     // Use config to directly load the configuration and check that only enabled
420     // or customized plugins are saved to configuration.
421     $filters = $this->config('filter.format.crazy')->get('filters');
422     $this->assertEqual(['filter_html_escape', 'filter_html'], array_keys($filters));
423
424     // Disable a plugin to ensure that disabled plugins with custom settings are
425     // stored in configuration.
426     $crazy_format->setFilterConfig('filter_html_escape', ['status' => FALSE]);
427     $crazy_format->save();
428     $filters = $this->config('filter.format.crazy')->get('filters');
429     $this->assertEqual(['filter_html_escape', 'filter_html'], array_keys($filters));
430
431     // Set the settings as per default to ensure that disable plugins in this
432     // state are not stored in configuration.
433     $crazy_format->setFilterConfig('filter_html_escape', ['weight' => -10]);
434     $crazy_format->save();
435     $filters = $this->config('filter.format.crazy')->get('filters');
436     $this->assertEqual(['filter_html'], array_keys($filters));
437   }
438
439   /**
440    * Checks if an expected violation exists in the given violations.
441    *
442    * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
443    *   The violations to assert.
444    * @param mixed $invalid_value
445    *   The expected invalid value.
446    */
447   public function assertFilterFormatViolation(ConstraintViolationListInterface $violations, $invalid_value) {
448     $filter_format_violation_found = FALSE;
449     foreach ($violations as $violation) {
450       if ($violation->getRoot() instanceof FilterFormatDataType && $violation->getInvalidValue() === $invalid_value) {
451         $filter_format_violation_found = TRUE;
452         break;
453       }
454     }
455     $this->assertTrue($filter_format_violation_found, format_string('Validation violation for invalid value "%invalid_value" found', ['%invalid_value' => $invalid_value]));
456   }
457
458   /**
459    * Tests that filter format dependency removal works.
460    *
461    * Ensure that modules providing filter plugins are required when the plugin
462    * is in use, and that only disabled plugins are removed from format
463    * configuration entities rather than the configuration entities being
464    * deleted.
465    *
466    * @see \Drupal\filter\Entity\FilterFormat::onDependencyRemoval()
467    * @see filter_system_info_alter()
468    */
469   public function testDependencyRemoval() {
470     $this->installSchema('user', ['users_data']);
471     $filter_format = FilterFormat::load('filtered_html');
472
473     // Disable the filter_test_restrict_tags_and_attributes filter plugin but
474     // have custom configuration so that the filter plugin is still configured
475     // in filtered_html the filter format.
476     $filter_config = [
477       'weight' => 20,
478       'status' => 0,
479     ];
480     $filter_format->setFilterConfig('filter_test_restrict_tags_and_attributes', $filter_config)->save();
481     // Use the get method to match the assert after the module has been
482     // uninstalled.
483     $filters = $filter_format->get('filters');
484     $this->assertTrue(isset($filters['filter_test_restrict_tags_and_attributes']), 'The filter plugin filter_test_restrict_tags_and_attributes is configured by the filtered_html filter format.');
485
486     drupal_static_reset('filter_formats');
487     \Drupal::entityManager()->getStorage('filter_format')->resetCache();
488     $module_data = _system_rebuild_module_data();
489     $this->assertFalse(isset($module_data['filter_test']->info['required']), 'The filter_test module is required.');
490
491     // Verify that a dependency exists on the module that provides the filter
492     // plugin since it has configuration for the disabled plugin.
493     $this->assertEqual(['module' => ['filter_test']], $filter_format->getDependencies());
494
495     // Uninstall the module.
496     \Drupal::service('module_installer')->uninstall(['filter_test']);
497
498     // Verify the filter format still exists but the dependency and filter is
499     // gone.
500     \Drupal::entityManager()->getStorage('filter_format')->resetCache();
501     $filter_format = FilterFormat::load('filtered_html');
502     $this->assertEqual([], $filter_format->getDependencies());
503     // Use the get method since the FilterFormat::filters() method only returns
504     // existing plugins.
505     $filters = $filter_format->get('filters');
506     $this->assertFalse(isset($filters['filter_test_restrict_tags_and_attributes']), 'The filter plugin filter_test_restrict_tags_and_attributes is not configured by the filtered_html filter format.');
507   }
508
509 }