Problem/Motivation

The clean_class Twig filter, which passes the text through Drupal\Component\Utility\Html::getClass(), does not work when used in a Views "Rewrite results" field.

To reproduce,

  • Create an Article node (so that your view has something to list).
  • Create a view that lists content, using fields.
  • Add the field "Content type", select "Rewrite results", and enter {{ type|lower }}. Add the label "Rewrite test (lower)"
  • Add another copy of "Content type" and this time use {{ type|clean_class }}. Add the label "Rewrite test (clean_class)"
  • Save the view and look at the resulting page.

You should see something like this:

<a href=".../node/1">test article</a>
Rewrite test (lower): article
Rewrite test (clean_class):

This shows that the lower filter works, but clean_class does not. The expected result is

<a href=".../node/1">test article</a>
Rewrite test (lower): article
Rewrite test (clean_class): article

According to xdebug, the problem is that Html::getClass() is being passed an object of class Drupal\Core\Render\Markup. Although the @param statement for Html::getClass() declares the parameter to be a string, this is not enforced. For most purposes, this class is just as good as a string because it can be cast to a string, but the problem is that this method caches its results:

  if (!isset(static::$classes[$class])) { ... }

Apparently, PHP does not implicitly cast $class to a string when we try to use it as an array index.

Proposed resolution

The simplest fix is to explicitly cast $class to a string in Html::getClass(). Technically, this is an API change, since this method will now accept anything that can be cast to a string.

Another fix would be to update the Views module so that it passes strings, not objects, to Twig filters. I do not think this would count as an API change.

Note that the clean_class filter is declared in TwigExtension::getFilters():

    return array(
      // ...
      new \Twig_SimpleFilter('clean_class', '\Drupal\Component\Utility\Html::getClass'),
      // ...
    );

As far as I can tell, the second argument to the \Twig_SimpleFilter constructor is supposed to be a callable, but there are no restrictions on its expected argument types.

Remaining tasks

The code change is already RTBC (Comment 12). See the following comments: the issue was moved back to "Needs work" in order to create a follow-up issue.

User interface changes

None.

API changes

This depends on which solution we use.

Data model changes

None.

Comments

benjifisher created an issue. See original summary.

benjifisher’s picture

Status: Active » Needs review
StatusFileSize
new518 bytes

The attached patch casts $class to a string in Drupal\Component\Utility\Html::getClass().

benjifisher’s picture

Looking at the other callbacks used in TwigExtension::getFilters(), I see that many of them explicitly accept non-string arguments.

cilefen’s picture

benjifisher’s picture

Component: views.module » base system
Issue tags: -Needs tests
StatusFileSize
new1.06 KB
new1.57 KB

I am removing the "Needs tests" tag and changing the component to "base system". The latter change is because the fix and the test are for Drupal\Component\Utility\Html.

Based on my previous comments, I think that the simple fix is the right fix, and that we do not have to change the @param comment for Html::getClass().

I have attached two patches. The test-only patch should generate an exception in Drupal\Tests\Component\Utility\HtmlTest and the other should pass.

The last submitted patch, 5: 2837013-clean_class-views-rewrite-5-test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 5: 2837013-clean_class-views-rewrite-5.patch, failed testing.

benjifisher’s picture

Status: Needs work » Needs review
StatusFileSize
new1.39 KB
new1.9 KB

The new test passed, but I am not supposed to use anything in the Drupal\Core namespace in a test for Drupal\Component. So I borrowed a technique from SafeMarkupTest.php and added a class with the same definition as Drupal\Core\Render\Markup.

Once again, I expect that the test-only patch should generate an exception in Drupal\Tests\Component\Utility\HtmlTest and the other test should pass.

The last submitted patch, 8: 2837013-clean_class-views-rewrite-8-test-only.patch, failed testing.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

leslieg’s picture

StatusFileSize
new79.02 KB
new84.7 KB

Patch applies cleanly and works as expected. Before and after screenshots attached
Note - I did uncheck the "Link label to the referenced entity" label in addition to following the steps above to test

before

after

leslieg’s picture

Status: Needs review » Reviewed & tested by the community
cilefen’s picture

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

Html::getClass would need its parameter documentation updated if going with this solution.

benjifisher’s picture

Status: Needs work » Reviewed & tested by the community

@cliefan I thought about that. See Comment #3 above. I found other cases where functions declare @param string in their doc blocks but still implicitly accept objects that can be converted to string.

Closely related to this issue:

Plain PHP:

That is why I decided that we can declare @param $string</a> and still accept an object with an implicit conversionto <code>string.

cilefen’s picture

You've got a point. I can't argue with that. Views code feels the same way obviously.

xjm’s picture

+++ b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
@@ -88,6 +91,11 @@ public function providerTestCleanCssIdentifier() {
+    // Test Html::getClass() handles Drupal\Component\Render\MarkupInterface input.

This comment is over 80 characters. Could be fixed on commit.

#14 is interesting. I do think the documentation should indeed be updated, and those others fixed in followup issues, but I'll get a third opinion.

alexpott’s picture

Version: 8.4.x-dev » 8.3.x-dev
Status: Reviewed & tested by the community » Needs work

@xjm asked me to look at this. I agree with @xjm this should be documented on the @param of the method so setting back to needs work.

Looking at the patch as a whole, I think the cast to string is okay. We have plenty of places where objects that can be cast to strings work like this - as pointed out by #14 - and fortunately this has nothing to do with safeness. I think it is totally okay if a cleaned css class loses any concept of safeness. I contemplated asking for a stricter type check before casting - ie something like MarkupInterface but I think that that is just unnecessary complexity.

I considered also swapping out the implementation in TwigExtension so only the twig filter does the cast to string but also I think that that is additional complexity for no gain.

Also since this is an API widening - ie. strings are still accepted and now stringy objects are too I think this is a patch safe bug fix.

+++ b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
@@ -88,6 +91,11 @@ public function providerTestCleanCssIdentifier() {
+    $markup = HtmlTestMarkup::create($this->randomMachineName());
+    $string = Html::cleanCssIdentifier(Unicode::strtolower((string) $markup));
+    $this->assertSame(Html::getClass($markup), $string, 'Markup object is converted to CSS class.');

Rather than using random string and asserting against something programmatic let's just hard code a string and assert against that. Also in PHPUnit the expected value comes first in the assert. So the test should be something like:

$markup = HtmlTestMarkup::create('BLAH_CLASS');
$this->assertSame('blah-class', Html::getClass($markup));
xjm’s picture

Issue tags: +Needs followup

Also adding "Needs followup" to investigate the other places an object-to-string cast is not documented in allowed values, as listed in #14. Thanks!

benjifisher’s picture

Status: Needs work » Needs review
Issue tags: -Needs followup
StatusFileSize
new1.44 KB
new2.32 KB
new1.91 KB

#16: I wrapped the comment at 80 characters. That comment was added as part of this patch, so it should certainly be corrected here.
#17: Thanks for the thoughtful comment. I considered some of the same alternatives, and came to the same conclusion. I made the suggested changes to the test, and also updated the other assertSame() call in testHtmlClass().
#18: I just added #2851911: Correctly document functions that accept "stringy" inputs, so I am removing the "Needs followup" tag.

The @param lines in the doc block now read

   * @param mixed $class
   *   The class name to clean. It can be a string or anything that can be cast
   *   to string.

Is that about what you had in mind?

Of course I will defer to your decision, but I am surprised that you think this is the right way to document "stringy" parameters since (as I said in #14) there are core PHP functions that behave the same way and declare their arguments as @param string. It may take a lot of work to track down every @param string declaration and update the ones that implicitly convert things to string.

The last submitted patch, 19: 2837013-clean_class-views-rewrite-19-test-only.patch, failed testing.

alexpott’s picture

@benjifisher good point re stringy params. Tricky. The problem is when we only support PHP7 and use primitive typecasting this is going to become more issue. I wonder what happens in that case - does the String typehint force a cast or throw an error?

alexpott’s picture

declare(strict_types=1);

function a(string $blah) {
  var_dump($blah);
}

class b {
  public function __toString() {
    return 'Hello world';
  }
}

$b = new b();
a($b);

This throws the following error:
PHP Fatal error: Uncaught TypeError: Argument 1 passed to a() must be of the type string, object given
So yeah I think mixed is the way to go because it is honest and more future proof.

benjifisher’s picture

In other words, it is (as I feared) a big job, but we will have to do it and we may as well start here.

It looks like the testbot approves of the patch: only the test-only patch fails, as intended. FWIW, the failures in #8 are timeout errors; I think that counts as a random testbot failure.

dawehner’s picture

Component: base system » theme system

Just moving it into the right queue.

tacituseu’s picture

It would work with {{ type|render|clean_class }} (see: https://www.drupal.org/node/2728915#comment-11207931).

benjifisher’s picture

Issue summary: View changes
Issue tags: +Baltimore2017, +Novice

The code changes have already been reviewed and approved. All that remains is to confirm that I added the follow-up issue as requested: see comments 17, 18, 19. I think that counts as a novice task.

I am also updating the issue summary.

epophoto’s picture

Status: Needs review » Reviewed & tested by the community

I am moving this to RTBC based on #26

The Follow Up issue exists here https://www.drupal.org/node/2851911

  • lauriii committed d2a5fe7 on 8.4.x
    Issue #2837013 by benjifisher, leslieg, alexpott: clean_class Twig...
lauriii’s picture

Status: Reviewed & tested by the community » Patch (to be ported)

As #17 states, I think casting to string is fine. It is acceptable that a cleaned up CSS class loses the state of safeness because special characters are being replaced anyway. Also, I have no other concerns since the string is going from safe to unsafe, meaning it will be escaped again.

I removed an unused use statement in the test file on the commit.

Committed d2a5fe7 and pushed to 8.4.x. Thanks!

This could be also potentially backported into 8.3.x.

  • lauriii committed 4b369f3 on 8.3.x
    Issue #2837013 by benjifisher, leslieg, alexpott: clean_class Twig...
lauriii’s picture

Status: Patch (to be ported) » Fixed

Based on #17 and a private conversation with @xjm this is safe to be backported to 8.3.x.

Cherry-picked 4b369f3 and pushed to 8.3.x.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.