Coming from #1419016-10: Allow text fields to enforce a specific format

In there we want to be able to reduce the available text_formats in a given Field API text field. I think that could be a general use-case where you want certain textareas on your site to have different available text formats. I.e. you want your content editors to be able to use HTML in your nodes but in comments, that would really be overkill. I think this would be something worth supporting.
User access to all formats should of course always be respected no matter what.

I do not plan to work on this personally, but it shouldn't be too hard, actually. Looking at filter_process_format() the relevant part is this:

  // Get a list of formats that the current user has access to.
  $formats = filter_formats($user);

That would become something like:

  // Get a list of formats that the current user has access to.
  $formats = filter_formats($user);
  // The list of available formats might have been reduced up-front.
  if (isset($element['#formats'])) {
    $formats = array_intersect($element['#formats'], $formats);
  }

(Untested, though.)

Sorry if this is a dupe, but I didn't find anything.

CommentFileSizeAuthor
#77 1423244-77-allowed-formats-d7.patch20.64 KBtstoeckler
#77 1423244-74-77-interdiff.txt4.01 KBtstoeckler
#74 1423244-74-allowed-formats-d7.patch20.71 KBtstoeckler
#68 1423244-58.patch28.21 KBlokapujya
#65 1423244-65-allowed-formats.patch28.27 KBtstoeckler
#65 1423244-63-65-interdiff-2.txt1.04 KBtstoeckler
#65 1423244-63-65-interdiff.txt6.04 KBtstoeckler
#63 1423244-psr4-reroll.patch27.14 KBxjm
#55 1423244-55-allowed-formats.patch27.42 KBtstoeckler
#55 1423244-50-55-interdiff.txt9.19 KBtstoeckler
#50 interdiff.txt6.12 KBWim Leers
#50 1423244-50-allowed-formats.patch26.28 KBWim Leers
#47 1423244-38-allowed-formats_0.patch26.34 KBtstoeckler
#45 1423244-40-allowed-formats-combined.patch28.27 KBtstoeckler
#43 1423244-40-allowed-formats-combined.patch28.27 KBtstoeckler
#39 1423244-38-allowed-formats.patch26.34 KBtstoeckler
#38 1423244-38-allowed-formats.patch0 byteststoeckler
#38 1423244-35-38-interdiff.txt1.07 KBtstoeckler
#35 1423244-35-text-format-allowed-formats.patch26.34 KBtstoeckler
#35 1423244-32-35-interdiff.txt536 byteststoeckler
#32 1423244-32-text-format-allowed-formats.patch26.34 KBtstoeckler
#32 1423244-30-32-interdiff.txt682 byteststoeckler
#30 1423244-30-text-format-allowed-formats.patch27.95 KBtstoeckler
#30 1423244-23-30-interdiff.txt4.16 KBtstoeckler
#23 1423244-23-text-format-allowed-formats.patch26.16 KBtstoeckler
#23 1423244-18-23-interdiff.txt5.1 KBtstoeckler
#18 1423244-18-text-format-allowed-formats.patch21.36 KBtstoeckler
#18 1423244-13-18-interdiff.txt13.73 KBtstoeckler
#13 1423244-13-text-format-allowed-formats.patch20.49 KBtstoeckler
#13 1423244-11-13-interdiff.txt24.5 KBtstoeckler
#11 allowed_formats-1423244-11.patch6.65 KBwebflo
#10 allowed_formats-1423244-10.patch6.97 KBfloretan
#6 allowed_formats-1423244-6.patch6.83 KBfloretan
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

wjaspers’s picture

Although I think introducing the #formats property is great, it might be better to name it #allowed_formats. This will help avoid confusion with the #format property, and make it easier to understand what the property is for.

Crell’s picture

There is Drupal 7-targeted contrib code in #1295248: Allow per-field-instance configuration of allowed formats that I welcome and encourage people to steal for Drupal 8. This is an important feature for any moderately complex Drupal site.

sun’s picture

Title: Introduce a #formats property for the 'text_format' element, to preselect/-reduce available formats » #allowed_formats property for #type 'text_format' to filter available formats

Let's keep this issue focused on the API side of the functionality. A possible UI for this should be tackled in a separate follow-up issue.

I don't see any problems with the feature request itself, but I do have some concerns the implementation will have to address:

  1. Would it make more sense to negate it? I.e., #disallowed_formats?

    We should investigate what the actual real world use-cases of this functionality are.

    Background: From a code and data handling perspective, I'm questioning whether it wouldn't it be easier for modules to only specify what they do not want. Because, precisely specifying what you want means that you have to keep track of all available formats on your own, and implement your own logic to handle the cases of when a new format has been created, or an existing is deleted.

  2. The implementation needs to create/design a new (test) expectation for the special case when all available formats are nuked by the #allowed_formats override and no format is left.

    This also includes the separate case when the currently assigned #format (for some existing text) is no longer available.

    The current widget handles this already, but exclusively on the assumption that these cases mean that the user does not have access to the assigned #format. Reducing the formats as proposed here has nothing to do with user access privileges to formats and the security considerations involved.

    Thus, we not only need expectations, but we also need to implement an appropriate behavior for those cases.

Crell’s picture

Use case:

- Forum post nodes and comments should allow only a super-restricted set of HTML, with Markdown support. That is true for all users regardless of role.

- Page nodes should allow a somewhat more permissive set of tags, but still not img or table. However, users in the Privileged Admin role should also have a second format available that allows img, table, etc.

- Major Event nodes allow the same two formats as Page noes, with the same role-based restriction, but it has WYSIWYG enabled for both formats.

- Major Event also has an "about the location" field that only allows certain HTML tags. This set of allowed tags is different than what forum posts or pages/events allow.

Note: This is not a contrived example. I originally wrote filterbynodetype in Drupal 5 and Drupal 6 because I had very nearly the use case above.

David_Rothstein’s picture

Cross-linking this issue with #784672: Allow text field to enforce a specific text format, which has some patches and previous discussion already.

In theory, it would make sense to create the API first (in this issue) and then consider using it later (in the other issue) - currently the code in the other issue is a bit of a mix of both. So we could postpone that issue on this one, except all the code is currently over there.

floretan’s picture

Status: Active » Needs review
FileSize
6.83 KB

Doing some research in the issue queue, there are a lot more use cases where this functionality is badly needed. With this minor API change, all these use cases could be implemented in contrib can be done without hacky form manipulations.

Here's a patch with test covering the essential cases and a few minor modifications to fix warnings. As sun mentioned in #3, this patch focuses only on the API-level functionality.

Example usage:

// Limit the allowed text format to "full_html".
$form['some_field'] = array(
  '#type' => 'text_format',
  '#format' => 'full_html',
  '#allowed_formats' => array('full_html'),
);

The logic inside filter_process_format() has also been updated to function similarly when the default format is not in the allowed formats as when the default format has been deleted.

Crell’s picture

Status: Needs review » Needs work
Crell’s picture

Status: Needs work » Needs review
tstoeckler’s picture

Status: Needs review » Needs work

Code/logic looks really nice!!! Some minor comments below:

+++ b/core/modules/filter/filter.module
@@ -713,13 +716,26 @@ function filter_process_format($element) {
+  ¶
...
+  ¶

+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
@@ -0,0 +1,101 @@
+    ¶
...
+    ¶
...
+    ¶
...
+    ¶
...
+    );
+    ¶
...
+    ¶
...
+    ¶
+    // The fallback format is discarded, so we have one less option ¶
...
+    ¶
...
+     ¶
...
+    ¶

Trailing whitespace.

+++ b/core/modules/filter/filter.module
@@ -713,13 +716,26 @@ function filter_process_format($element) {
+  	$element['#format'] = filter_default_format($user);
...
+  	// The fallback format should always be allowed.
+  	$element['#allowed_formats'][] = filter_fallback_format();
+  	
+  	// Check if the default format is in the list of allowed formats.
+  	$format_allowed = in_array($element['#format'], $element['#allowed_formats']);
+  	
...
+  	$format_allowed = true;

Tabs.
The last one should also be TRUE not true.

floretan’s picture

Status: Needs work » Needs review
FileSize
6.97 KB

Sorry for the whitespace stuff, I hadn't set things up properly in a new environment. Here's the patch rerolled with both changes.

webflo’s picture

Issue summary: View changes
Issue tags: +SprintWeekend2014, +D8MA
FileSize
6.65 KB

Reroll

Status: Needs review » Needs work

The last submitted patch, 11: allowed_formats-1423244-11.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
24.5 KB
20.49 KB

Here we go.

This should pass.

I will comment on the patch below.

tstoeckler’s picture

  1. +++ b/core/modules/filter/filter.module
    @@ -666,11 +666,11 @@ function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE,
    - *     the current user can select for this element. If NULL or not set, all
    - *     text formats enabled for the current user will be allowed.
    ...
    + *     available for this element. If omitted, all text formats enabled for the
    + *     current user will be allowed.
    ...
    + *   - #allowed_formats: (optional) An array of text format IDs that are
    ...
    + *     default format for the current user will be used.
    ...
    + *   - #format: (optional) The text format ID to preselect. If omitted, the
    ...
    - *   - #format: (optional) The text format ID to preselect. If NULL or not set,
    - *     the default format for the current user will be used.
    - *   - #allowed_formats: (optional) An array of IDs for the text format that
    

    I found the 'that the user can select' part a bit confusing, because that's not necessarily TRUE depending on access to the formats.

  2. +++ b/core/modules/filter/filter.module
    @@ -709,6 +709,7 @@ function filter_process_format($element) {
    +  $element['value'] += array('#default_value' => '');
    
    @@ -805,7 +810,7 @@ function filter_process_format($element) {
    +    $element['value']['#value'] = $element['value']['#default_value'];
    ...
    -    $element['value']['#value'] = isset($element['value']['#default_value']) ? $element['value']['#default_value'] : '';
    

    I just found it makes sense to initialize the element properly so we can avoid the check below.

  3. +++ b/core/modules/filter/filter.module
    @@ -719,28 +720,31 @@ function filter_process_format($element) {
    -    $format_allowed = in_array($element['#format'], $element['#allowed_formats']);
    -
    ...
    -  else {
    -    // If formats are not restricted, we allow the current format.
    -    // The current format could still be revoked if the user doesn't
    -    // have the necessary permissions.
    -    $format_allowed = TRUE;
    
    @@ -788,11 +792,12 @@ function filter_process_format($element) {
    +  $format_allowed = !isset($element['#allowed_formats']) || in_array($element['#format'], $element['#allowed_formats']);
    

    I moved the $format_allowed check to where all of the other booleans are being initialized.

  4. +++ b/core/modules/filter/filter.module
    @@ -719,28 +720,31 @@ function filter_process_format($element) {
    +    // $element['#allowed_formats'] explicitly.
    ...
    +    // certain text formats to be used for certain text areas. In case the
    ...
    +    // We do not add the fallback format here to allow the use-case of forcing
    ...
    -    $element['#allowed_formats'][] = filter_fallback_format();
    ...
    -    // The fallback format should always be allowed.
    ...
    +    // fallback format is supposed to be allowed as well, it must be added to
    

    I tried to explain in the comment why I disagree with the previous code.

  5. +++ b/core/modules/filter/filter.module
    @@ -719,28 +720,31 @@ function filter_process_format($element) {
    -  }
    -
    ...
    -    $element['#format'] = filter_default_format($user);
    ...
    -  if (!isset($element['#format'])) {
    ...
    -  // Use the default format for this user if none was selected.
    ...
    +    }
    ...
    +      // If the user does not have access to any of the allowed formats, set
    +      // the default format to NULL.
    +      $element['#format'] = NULL;
    ...
    +    else {
    ...
    +    }
    ...
    +      $element['#format'] = reset($formats)->format;
    ...
    +      // weight.
    ...
    +    if (!empty($formats)) {
    ...
    +  if (!isset($element['#format'])) {
    ...
    +      // If no text format was selected, use the allowed format with the highest
    ...
       }
    

    The default format needs to depend on the available options of the select. Also the = NULL allows to skip isset() checks below (and is semantically correct as well).

  6. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -26,76 +47,222 @@ public static function getInfo() {
    -    drupal_process_form($form_id, $form, $form_state);
    ...
    -    drupal_prepare_form($form_id, $form, $form_state);
    ...
    +    $this->drupalGet('filter-test/text-format');
    

    So instead of processing the form array manually I turned this into an actual web test. We cannot cleanly unit test filter_process_format() anyway, and in this case I think it makes sense to check that the correct markup makes it's way on to the page as this is very much access-related.

    The test coverage is conclusive in terms of the configuration of #type 'text_format'.
    It is not conclusive, yet IMO sufficient in terms of testing different permission combinations.
    It also does not test the 'always_show_fallback_choice' setting. I think we're good here, though, we can always improve.

  7. +++ b/core/modules/filter/tests/filter_test/filter_test.routing.yml
    @@ -0,0 +1,8 @@
    \ No newline at end of file
    
    +++ b/core/modules/filter/tests/filter_test/lib/Drupal/filter_test/Form/FilterTestFormatForm.php
    @@ -0,0 +1,117 @@
    +} ¶
    \ No newline at end of file
    

    Damn... :-(

  8. +++ b/core/modules/filter/tests/filter_test/filter_test.info.yml
    @@ -4,4 +4,4 @@ description: 'Tests filter hooks and functions.'
    +#hidden: true
    ...
    -hidden: true
    

    Oops

Status: Needs review » Needs work

The last submitted patch, 13: 1423244-13-text-format-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 13: 1423244-13-text-format-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
13.73 KB
21.36 KB

The test fails in FilterAPITest were introduced by the default config I added to filter_test module.

tstoeckler’s picture

Btw, this blocks #2144413: Config translation does not support text elements with a format and thus any sensible translation of configuration that involves formatted text. This includes empty/footer/header text from views, for example.

Also this is only an API addition, not a BC-break in any way.

tstoeckler’s picture

tstoeckler’s picture

Status: Needs review » Needs work

The last submitted patch, 18: 1423244-18-text-format-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
5.1 KB
26.16 KB

This should pass. This time I ran all filter tests.

tstoeckler’s picture

  1. +++ b/core/modules/filter/filter.module
    @@ -719,9 +723,28 @@ function filter_process_format($element) {
    +    // Check if the default format is in the list of allowed formats.
    +    $formats = array_intersect_key($formats, array_flip($element['#allowed_formats']));
    

    This comment is no longer accurate.

  2. +++ b/core/modules/filter/filter.module
    @@ -719,9 +723,28 @@ function filter_process_format($element) {
    +      // If no text format was selected, use the allowed format with the highest
    +      // weight.
    +      $element['#format'] = reset($formats)->format;
    

    The comment should explain that this is equivalent to calling filter_default_format()

  3. +++ b/core/modules/filter/filter.module
    @@ -719,9 +723,28 @@ function filter_process_format($element) {
    +      // If the user does not have access to any of the allowed formats, set
    +      // the default format to NULL.
    +      $element['#format'] = NULL;
    

    This comment should probably explain *why* we are doing this.

  4. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,267 @@
    +    $options[] = '';
    

    This could use a comment.

  5. +++ b/core/modules/filter/tests/filter_test/filter_test.routing.yml
    @@ -0,0 +1,8 @@
    \ No newline at end of file
    

    Oh no!

tstoeckler’s picture

Before I re-roll for that, though, this could really use some reviews on the architecture/approach!

dawehner’s picture

I do not plan to work on this personally, but it shouldn't be too hard, actually

Yeah

In general I totally agree that the basic usecase is to limit the available formats, instead of disallow what you don't want.
While people want to have maybe something more HTML-ish/BB-ish/markdown-ish in a forum, they do not need that in a contact form.
One an architectual perspective it could help a module like better_formats to have the separate process method to deal with the #allowed_formats but given
the amount of logic in the lower part of the function, this does not seem to be feasible.

  1. +++ b/core/modules/filter/filter.module
    @@ -719,9 +723,28 @@ function filter_process_format($element) {
    +      // If the user does not have access to any of the allowed formats, set
    +      // the default format to NULL.
    +      $element['#format'] = NULL;
    

    Does that automatically falls back to the fallback format again, or in other words the comment should not say what the code is doing.
    Oh I see you commented the same, removed the other ones which aren't really helpful.

  2. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,267 @@
    +    $this->drupalLogin($this->adminUser);
    ...
    +    $this->doFilterFormTestAsAdmin();
    ...
    +    $this->drupalLogin($this->webUser);
    ...
    +    $this->doFilterFormTestAsNonAdmin();
    

    It is a bit odd to have the drupalLogin calls outside of the methods which talk about working "asAdmin"/"asNonAdmin" but I cannot think about a better way without hardcoding the path in the actual test method.

  3. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,267 @@
    +  protected function assertNoSelect($id) {
    ...
    +  protected function assertOptions($id, array $expected_options, $selected) {
    ...
    +  protected function assertRequiredSelect($id, array $options) {
    ...
    +  protected function assertDisabledTextarea($id) {
    

    We could think about moving up assertRequiredSelect and assertOptions, they seem to be quite useful.

Crell’s picture

  1. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
    @@ -32,36 +32,6 @@ function setUp() {
    @@ -72,13 +42,17 @@ function testCheckMarkup() {
    
    @@ -72,13 +42,17 @@ function testCheckMarkup() {
         $expected_filtered_text = "Text with evil content and a URL: <a href=\"http://drupal.org\">http://drupal.org</a>!";
         $expected_filter_text_without_html_generators = "Text with evil content and a URL: http://drupal.org!";
     
    +    $actual_filtered_text = check_markup($text, 'filtered_html', '', FALSE, array());
    +    $this->verbose("Actual:<pre>$actual_filtered_text</pre>Expected:<pre>$expected_filtered_text</pre>");
         $this->assertIdentical(
    -      check_markup($text, 'filtered_html', '', FALSE, array()),
    

    This is very likely for a separate issue, but the filter logic is just begging to be moved to PHPUnit tests.

  2. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,267 @@
    +/**
    + * Tests form elements provided by Filter module.
    + */
    +class FilterFormTest extends WebTestBase {
    +
    

    Or maybe is it relevant now. Can we not test the filter selection logic at the class level? If not, it means the class is not properly written. We should be able to just poke it and check the arrays that come back, no?

Thank you for picking this up; I have wanted per-node-type or per-field-instance format selection since Drupal 4.7. :-) This gets us a step closer to that.

tstoeckler’s picture

Thanks for the reviews, yay!!

#26.1 Any suggestions for a better comment? :-)
#26.2 Yeah, was unsure about that as well, will move those inside.
#26.3 I totally agree, but I'd like to discuss that in a follow-up so as to avoid a bikeshed.

#27.1 In core we only ever test classes with PHPUnit. It's of course possible to test functions as well, but...
#27.2 Again, what class? Also, since this is security-related I think it makes sense to have "integration testing" that the form element shows up as expected. But of course, in theory there should be unit tests as well.

Crell’s picture

Dur. Crap, you're right, I was reading too fast; The filter system still needs to be OOPified, but that's out of scope for this issue.

tstoeckler’s picture

Here's a re-roll. Fixes #24 and #26. I quickly tested that this works with editor.module as well. I.e. if there is only a single format, CKEditor gets instantiated properly.

Status: Needs review » Needs work

The last submitted patch, 30: 1423244-30-text-format-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
682 bytes
26.34 KB

Reading those error messages, you just gotta love our config schema system. This should fix that.

Status: Needs review » Needs work

The last submitted patch, 32: 1423244-32-text-format-allowed-formats.patch, failed testing.

The last submitted patch, 32: 1423244-32-text-format-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
536 bytes
26.34 KB

tstoeckler--

tstoeckler’s picture

Yay, anyone want to RTBC so we can finally knock this sucker out?

tim.plunkett’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/filter/filter.module
    @@ -512,9 +516,20 @@ function filter_process_format($element) {
    -  if (!isset($element['#format'])) {
    -    $element['#format'] = filter_default_format($user);
    ...
    +  if (isset($element['#allowed_formats'])) {
    ...
    +  if (!isset($element['#format']) && !empty($formats)) {
    

    In HEAD, there was no way to get past these lines without an $element['#format'] being set.

    Now with this patch, I could have $element['#allowed_formats'] = array();, and then $element['#format'] would be empty.

  2. +++ b/core/modules/filter/filter.module
    @@ -512,9 +516,20 @@ function filter_process_format($element) {
    +    $formats = array_intersect_key($formats, array_flip($element['#allowed_formats']));
    

    One solution would be to add += array(filter_default_format($user)) here. At least I think that would work.

  3. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAPITest.php
    @@ -23,7 +23,7 @@ class FilterAPITest extends EntityUnitTestBase {
    -      'name' => 'API',
    +      'name' => 'Filter API',
    

    Um, okay.

  4. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    /** @var \Drupal\filter\Entity\FilterFormat $filter_test_format */
    ...
    +    /** @var \Drupal\filter\Entity\FilterFormat $filtered_html_format */
    ...
    +    /** @var \Drupal\filter\Entity\FilterFormat $full_html_format */
    
    +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php
    @@ -44,19 +44,8 @@ function setUp() {
    +    /** @var \Drupal\filter\Entity\FilterFormat $filtered_html_format */
    

    These are entirely unnecessary here, but if you're going to bother with @var, at least use the interface not the class.

  5. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterSecurityTest.php
    @@ -101,8 +90,8 @@ function testDisableFilterModule() {
       function testSkipSecurityFilters() {
    -    $text = "Text with some disallowed tags: <script />, <em><object>unicorn</object></em>, <i><table></i>.";
    -    $expected_filtered_text = "Text with some disallowed tags: , <em>unicorn</em>, .";
    +    $text = "Text with some disallowed tags: <script />, <p><object>unicorn</object></p>, <i><table></i>.";
    +    $expected_filtered_text = "Text with some disallowed tags: , <p>unicorn</p>, .";
    

    Why bother changing this? It makes me uncomfortable to mess with a test called "testSkipSecurityFilters" unless there is a good reason.

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
1.07 KB
0 bytes

1. That's how it's supposed to work. If you set #allowed_formats to an empty array, there will be no way to edit the text. ...because now formats are allowed. How else could it work?!

2. See above. The whole point of this issue is that you can decide for yourself which formats you want and we respect it and do not override it.

4. Updated to use the interface. I'm using getPermissionName() so the typehint is useful, actually.

5. So all of the filter tests currently are a littel schizophrenic as filter_test installs some text formats upon install and then they add additional ones in their test classes. This makes things incredibly hard to grok as it's very unclear which formats exist at which point. Therefore I moved all of the filter formats to default config of the filter_test format. That at least makes it consistent. It surely could be refactored to be better, but it's certainly an improvement over head.

This has the effect that FilterSecurityTest used to have a filter format that allowed < em > tags, but now it has a similar one, which allows < p >. We could just as well use all other tests to use < em > instead of < p >, but I really don't see the point, TBH. If you look at the actual assertions it's very clear what's happening there. It's asserting that unallowed markup gets stripped out. The only difference is that the allowed markup used to be < em > and is now < p >.

tstoeckler’s picture

FileSize
26.34 KB

Oops, empty patch file.

Status: Needs review » Needs work

The last submitted patch, 39: 1423244-38-allowed-formats.patch, failed testing.

tstoeckler’s picture

Status: Needs work » Postponed

The test fail is due to #2234771: drupal_get_profile() should not fall back to 'standard' so this is now blocked on that :-/

tstoeckler’s picture

tstoeckler’s picture

Status: Postponed » Needs review
FileSize
28.27 KB

Uploading a combined patch to (hopefully :-)) verify my claim.

tstoeckler’s picture

Dear testbot, please test my patch?!

tstoeckler’s picture

Maybe re-uploading helps?

tstoeckler’s picture

Status: Needs review » Postponed

Yup, I thought so.

tstoeckler’s picture

Status: Postponed » Needs review
FileSize
26.34 KB

Re-uploading #39. That should now pass as the linked issue got in in the meantime.

Wim Leers’s picture

Wim Leers’s picture

Wim Leers’s picture

Status: Needs review » Needs work
FileSize
26.28 KB
6.12 KB

Nice work! :)

Manual testing revealed no regressions compared to D8 HEAD. However, tests do NOT pass locally, so requeued for testing.

I have found a lot of small things. The typos I fixed in this reroll, but still quite a few for you to address

  1. +++ b/core/modules/filter/filter.module
    @@ -499,6 +502,7 @@ function filter_process_format($element) {
    +  $element['value'] += array('#default_value' => '');
    

    Why is this necessary?

  2. +++ b/core/modules/filter/filter.module
    @@ -512,9 +516,20 @@ function filter_process_format($element) {
    +    // weight. This is equivalent to calling filter_default_format().
    +    $element['#format'] = reset($formats)->format;
    

    It is indeed equivalent. But AFAICT, as of this patch, we don't have any uses of filter_default_format() left. Why not remove it? :)

  3. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +class FilterFormTest extends WebTestBase {
    

    Any reason why this can't be a DUTB test?

  4. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +      'name' => 'Filter form element',
    

    "Text format form element"?

  5. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    // 'filtered_html' as the default value in this case.
    

    'filter_test'?

  6. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    $this->assertRequiredSelect('edit-all-formats-default-missing-format--2', $formats);
    

    I found it *very* confusing that for the previous cases, you would call assertOptions(), but in this case, you'd call assertRequiredSelect(), but not assert the options. After looking at the implementation details, it turns out that does happen, but the name is just a bit misleading. I think renaming it to assertRequiredSelectAndOptions() would clear that up completely :)

  7. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    // default. This happens to be 'filter_test'.
    

    'filtered_html'?

  8. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    // 'full_html' as the default value in this case.
    

    'filter_test'?

  9. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    // The user only has access to the 'filter_test' format, so when no default
    +    // is given that is preselected and the text format select is hidden.
    +    $this->assertNoSelect('edit-restricted-formats-no-default-format--2');
    

    This should then still assert that the input[type=hidden] for the text format has a value of "filter_test".

  10. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    $this->assertNoSelect('edit-single-format-default-format--2');
    

    Here too, there should be a verification of the input[type=hidden] value!

  11. +++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormTest.php
    @@ -0,0 +1,270 @@
    +    // If the select has a missing or disallowed format make the administrator
    +    // explicitly choose the format.
    

    Wrong comment.

  12. +++ b/core/modules/filter/tests/filter_test/lib/Drupal/filter_test/Form/FilterTestFormatForm.php
    @@ -0,0 +1,117 @@
    +      '#format' => 'basic_html',
    

    basic_html is undefined; filter_test only creates the filtered_html and full_html text formats, so you'll want to change this to filtered_html.

Plus, I found one part of filter_process_format() that I think needs some extra attention:

  // If multiple text formats are available, remove the fallback. The
  // "always_show_fallback_choice" is a hidden variable that has no UI. It
  // defaults to false.
  if (!\Drupal::config('filter.settings')->get('always_show_fallback_choice')) {
    $fallback_format = filter_fallback_format();
    if ($element['#format'] !== $fallback_format && count($formats) > 1) {
      unset($formats[$fallback_format]);
    }
  }

Particularly: count($formats) > 1. Thanks to #allow_formats, $formats can easily contain only one text format, which would make this kick in at undesired moments. I suggest changing the outer if-condition to:

  // If multiple text formats are available, remove the fallback. The
  // "always_show_fallback_choice" is a hidden variable that has no UI. It
  // defaults to false. However, whenever #allowed_formats is set, don't
  // remove the fallback, because the developer has meticulously
  // configured the list of allowed text formats.
  if (!\Drupal::config('filter.settings')->get('always_show_fallback_choice') && !isset($element['#allowed_formats'])) {

The last submitted patch, 47: 1423244-38-allowed-formats_0.patch, failed testing.

Gábor Hojtsy’s picture

Status: Needs work » Needs review

Queuing new patch for testing too.

Status: Needs review » Needs work

The last submitted patch, 50: 1423244-50-allowed-formats.patch, failed testing.

Wim Leers’s picture

Same failures, as expected :)

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
9.19 KB
27.42 KB

@WimLeers: Thanks for the excellent review. Very much appreciated!

The test failures were because the patch still added some default text formats to the config directory instead of the config/install. Should be green now.

Regarding your remarks:

1. Added an inline comment. Hope it's sufficient.

2. PHPStorm still found 15 usages for me, so I'd rather not tackle that here.

3. It uses drupalGet(), xpath() and getAllOptions() which are all on WebTestBase. In a perfect world, yes, this would be a DUBT, but currently I don't see how that would be easily possible.

4.-8. Fixed

9.-10. Sorry, I don't know which hidden input element you are referring to. I checked both the code and the actual output and couldn't find anything related.

11.-12. Fixed

Regarding the fallback format stuff: You are correct that this was problematic. I went with your suggestion. I expanded and reformatted the comment, however. Hope you like it. :-)

Gábor Hojtsy’s picture

Category: Feature request » Task

This is required to #2144413: Config translation does not support text elements with a format which is required to provide complete and definite schema support for views, so this not a feature, its a blocker task.

Wim Leers’s picture

Status: Needs review » Needs work

Almost there :)

9 & 10: on a fresh D8 install, grant the anonymous user permission to post comments, then go to the comment form as an anonymous users and inspect the comment form HTML. You'll find a input[type=hidden] with the text format.

  1. +++ b/core/modules/filter/filter.module
    @@ -484,6 +484,7 @@ function filter_process_format($element) {
    +  // Make sure the #default_value key is set, so we can use it below.
    

    I can't find this usage below?

  2. +++ b/core/modules/filter/filter.module
    @@ -513,10 +514,15 @@ function filter_process_format($element) {
    +  // 3. The fallback format is not the default format.
    

    I don't see this condition expressed in the if-test.

    Is this wrong, or is it implied but am I overlooking it?

tstoeckler’s picture

Status: Needs work » Needs review

Re #57:

0. I followed your steps exactly and there's no input[type=hidden]. See the attached screenshot for proof. I really would have been surprised to see it, too, because I can't see it being generated anywhere in the code?!

1. It's not in the patch, it's in line 585 with the patch applied. You can actually trigger an Undefined index error in 8.x as well. So strictly speaking this is an unrelated bug fix / DX improvement. I'd like to keep it, but if you insist, I can rip that line back out.

2. That's the $element['#format'] !== $fallback_format part in the inner if ()-statement.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

0. I can't reproduce this myself. I must have confused this with something else.

1. Ok.

2. You're right. Thanks for clarifying — that should've been documented before, but it wasn't — my fault.

I don't have any other complaints. Especially because 90% of this patch is test coverage that we should've already had, and after carefully reviewing all added tests, I could not find any flaws. Even if this were to introduce a regression, the tests will make it trivial to fix.

Great work — thanks! :)

David_Rothstein’s picture

This looks great. Any reason not to backport it? I consider this a security improvement since people are going to try to modify the list of options anyway, and without this it's very difficult to do correctly (see #1295248: Allow per-field-instance configuration of allowed formats).

+ *   - #allowed_formats: (optional) An array of text format IDs that are
+ *     available for this element. If omitted, all text formats that the current
+ *     user has access to will be allowed.

It would be good to add something to the documentation to make clear that including a text format in #allowed_formats has no effect unless the user also has access to it (i.e. that it can only be used to restrict the default list, not add to it).

The tests look really good overall. They partially duplicate some of the existing tests in FilterFormatAccessTest.php (so ideally they'd be combined), but the method of testing for these new ones looks a bit cleaner, and a bit of duplicate testing doesn't hurt anyone :)

David_Rothstein’s picture

Issue tags: +Needs backport to D7

Meant to add the backport tag (optimistically assuming we can backport it).

tstoeckler’s picture

Well you're the gatekeeper, so if you say we can, we can. :-)

Technically, filter_process_format() hasn't changed that much so shouldn't be very hard actually.

xjm’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/filter/src/Tests/FilterFormTest.php
@@ -0,0 +1,271 @@
+  protected function doFilterFormTestAsNonAdmin() {
+    $this->drupalLogin($this->webUser);
+    $this->drupalGet('filter-test/text-format');
+
+    // Test a text format element with all formats. Only formats the user has
+    // access to are shown.
+    $formats = array('filtered_html', 'filter_test');
+    // If no default is given, the format with the lowest weight becomes the
+    // default. This happens to be 'filtered_html'.
+    $this->assertOptions('edit-all-formats-no-default-format--2', $formats, 'filtered_html');
+    // \Drupal\filter_test\Form\FilterTestFormatForm::buildForm() uses
+    // 'filter_test' as the default value in this case.
+    $this->assertOptions('edit-all-formats-default-format--2', $formats, 'filter_test');
+    // If a missing format is given as default, administers must select a valid
+    // replacement format.
+    $this->assertDisabledTextarea('edit-all-formats-default-missing-value');
+
+    // Test a text format element with a predefined list of formats.
+    // The user only has access to the 'filter_test' format, so when no default
+    // is given that is preselected and the text format select is hidden.
+    $this->assertNoSelect('edit-restricted-formats-no-default-format--2');
+    // When the format that the user does not have access to is preselected, the
+    // textarea should be disabled.
+    $this->assertDisabledTextarea('edit-restricted-formats-default-value');
+    $this->assertDisabledTextarea('edit-restricted-formats-default-missing-value');
+    $this->assertDisabledTextarea('edit-restricted-formats-default-disallowed-value');
+
+    // Test a text format element with a fixed format.
+    $formats = array('filtered_html');
+    // When there is only a single option there is no point in choosing.
+    $this->assertNoSelect('edit-single-format-no-default-format--2');
+    $this->assertNoSelect('edit-single-format-default-format--2');
+    // If the select has a missing or disallowed format make sure the textarea
+    // is disabled.
+    $this->assertDisabledTextarea('edit-single-format-default-missing-value');
+    $this->assertDisabledTextarea('edit-single-format-default-disallowed-value');
+  }
+

Should we not also be asserting on enabled text areas? Also the final $formats = array('filtered_html'); creates a variable that is never used - are we testing what we think we're testing?

tstoeckler’s picture

Status: Needs work » Needs review
FileSize
6.04 KB
1.04 KB
28.27 KB

Thanks for catching the unused variable. Removed that. The asserts are correct, though. It is just that I started each batch of assertions with $formats = ... and then forgot to remove it in the one place where we actually don't need it (because no select is shown).

Also added a bunch of $this->assertEnabledTextarea(). More assertions can never hurt.

Two interdiffs because I merged 8.x in between.

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

All the changes look good to me, and address all of @alexpott's points.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs reroll

Needs a reroll

git ac https://drupal.org/files/issues/1423244-65-allowed-formats.patch
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 28944  100 28944    0     0  66635      0 --:--:-- --:--:-- --:--:-- 79298
error: patch failed: core/modules/filter/src/Tests/FilterAPITest.php:105
error: core/modules/filter/src/Tests/FilterAPITest.php: patch does not apply
error: patch failed: core/modules/filter/src/Tests/FilterSecurityTest.php:101
error: core/modules/filter/src/Tests/FilterSecurityTest.php: patch does not apply
lokapujya’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
28.21 KB

Rerolled.

lokapujya’s picture

+++ b/core/modules/filter/src/Tests/FilterFormTest.php
@@ -0,0 +1,302 @@
+    return $select;

Should the return value be documented or is it obvious enough?

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the reroll! Same number of insertions and deletions + looks the same + green, so back to RTBC per #66.

  • Commit 7922db6 on 8.x by alexpott:
    Issue #1423244 by tstoeckler, floretan, Wim Leers, lokapujya, xjm,...
alexpott’s picture

Version: 8.x-dev » 7.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

Committed 7922db6 and pushed to 8.x. Thanks!

Let's handle the assert return value in a new followup issue.

+++ b/core/modules/filter/src/Tests/FilterFormTest.php
@@ -0,0 +1,302 @@
+  /**
+   * Makes sure that no select element with the given ID exists on the page.
+   *
+   * @param string $id
+   *   The HTML ID of the select element.
+   */
+  protected function assertNoSelect($id) {
...
+  /**
+   * Asserts that a select element has the correct options.
+   *
+   * @param string $id
+   *   The HTML ID of the select element.
+   * @param array $expected_options
+   *   An array of option values.
+   * @param string $selected
+   *   The value of the selected option.
+   */
+  protected function assertOptions($id, array $expected_options, $selected) {
...
+  /**
+   * Asserts that there is a select element with the given ID that is required.
+   *
+   * @param string $id
+   *   The HTML ID of the select element.
+   * @param array $options
+   *   An array of option values that are contained in the select element
+   *   besides the "- Select -" option.
+   */
+  protected function assertRequiredSelectAndOptions($id, array $options) {
...
+  /**
+   * Asserts that a textarea with a given ID exists and is not disabled.
+   *
+   * @param string $id
+   *   The HTML ID of the textarea.
+   */
+  protected function assertEnabledTextarea($id) {
...
+  /**
+   * Asserts that a textarea with a given ID has been disabled from editing.
+   *
+   * @param string $id
+   *   The HTML ID of the textarea.
+   */
+  protected function assertDisabledTextarea($id) {

All assertSomething() functions should return a bool indicating pass or fail.

   * @return
   *   TRUE if the assertion succeeded, FALSE otherwise.
lokapujya’s picture

tstoeckler’s picture

Status: Patch (to be ported) » Needs review
Issue tags: -Needs backport to D7
FileSize
20.71 KB

Backported this to D7.

Had to change the following things:

  • Move format filtering code around slightly to account for slightly different code structure in D7.
  • The fallback format (plain_text) is not removed in D7 so that hunk is no longer relevant. This also means that some assertions in the test had to be changed to also include plain_text.
  • Use hook_menu() and a form function instead of *.routing.yml and a form class.
  • Use fieldset instead of details in the test form.
  • Create the needed text formats manually instead of using the shipped configuration.
  • Clear the permissions cache in the test. This was not needed for the D8 patch due to
  • Adapt assertRequiredSelectAndOptions() for the fact that required select elements do not actually receive a required attribute in D7 (??!?!)
tstoeckler’s picture

Oh this also includes my patch from #2284383-9: All assertSomething() functions should return a bool indicating pass or fail.. Forgot to mention that.

tstoeckler’s picture

tstoeckler’s picture

Issue tags: +PHP 5.2, +Needs manual testing
FileSize
4.01 KB
20.64 KB

Now with (hopefully) 100% more PHP 5.2 compatibilty and 100% less incorrect comments.

As I managed to majorly break this apparently, this needs to be manually tested on a PHP 5.2 environment. Specifically it should be verified that FilterFormTest comes back green.

Liam Morland’s picture

  • alexpott committed 7922db6 on 8.3.x
    Issue #1423244 by tstoeckler, floretan, Wim Leers, lokapujya, xjm,...

  • alexpott committed 7922db6 on 8.3.x
    Issue #1423244 by tstoeckler, floretan, Wim Leers, lokapujya, xjm,...

  • alexpott committed 7922db6 on 8.4.x
    Issue #1423244 by tstoeckler, floretan, Wim Leers, lokapujya, xjm,...

  • alexpott committed 7922db6 on 8.4.x
    Issue #1423244 by tstoeckler, floretan, Wim Leers, lokapujya, xjm,...