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.
- Confirm that the follow-up issue #2851911: Correctly document functions that accept "stringy" inputs satisfies the request made in Comments 18 and following.
- Confirm that the patch in #19 fixes the formatting error pointed out in #16.
User interface changes
None.
API changes
This depends on which solution we use.
Data model changes
None.
Comments
Comment #2
benjifisherThe attached patch casts
$classto a string inDrupal\Component\Utility\Html::getClass().Comment #3
benjifisherLooking at the other callbacks used in
TwigExtension::getFilters(), I see that many of them explicitly accept non-string arguments.Comment #4
cilefen commentedComment #5
benjifisherI 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
@paramcomment forHtml::getClass().I have attached two patches. The test-only patch should generate an exception in
Drupal\Tests\Component\Utility\HtmlTestand the other should pass.Comment #8
benjifisherThe new test passed, but I am not supposed to use anything in the
Drupal\Corenamespace in a test forDrupal\Component. So I borrowed a technique fromSafeMarkupTest.phpand added a class with the same definition asDrupal\Core\Render\Markup.Once again, I expect that the test-only patch should generate an exception in
Drupal\Tests\Component\Utility\HtmlTestand the other test should pass.Comment #11
leslieg commentedPatch 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

Comment #12
leslieg commentedComment #13
cilefen commentedHtml::getClass would need its parameter documentation updated if going with this solution.
Comment #14
benjifisher@cliefan I thought about that. See Comment #3 above. I found other cases where functions declare
@param stringin their doc blocks but still implicitly accept objects that can be converted tostring.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.Comment #15
cilefen commentedYou've got a point. I can't argue with that. Views code feels the same way obviously.
Comment #16
xjmThis 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.
Comment #17
alexpott@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.
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:
Comment #18
xjmAlso adding "Needs followup" to investigate the other places an object-to-string cast is not documented in allowed values, as listed in #14. Thanks!
Comment #19
benjifisher#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 intestHtmlClass().#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
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 stringdeclaration and update the ones that implicitly convert things to string.Comment #21
alexpott@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?
Comment #22
alexpottThis throws the following error:
PHP Fatal error: Uncaught TypeError: Argument 1 passed to a() must be of the type string, object givenSo yeah I think
mixedis the way to go because it is honest and more future proof.Comment #23
benjifisherIn 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.
Comment #24
dawehnerJust moving it into the right queue.
Comment #25
tacituseu commentedIt would work with
{{ type|render|clean_class }}(see: https://www.drupal.org/node/2728915#comment-11207931).Comment #26
benjifisherThe 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.
Comment #27
epophoto commentedI am moving this to RTBC based on #26
The Follow Up issue exists here https://www.drupal.org/node/2851911
Comment #29
lauriiiAs #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.
Comment #31
lauriiiBased 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.