strip_tags(): Argument #1 ($string) must be of type string, Drupal\filter\Render\FilteredMarkup given in strip_tags()

Created on 10 January 2025, 5 days ago

Problem/Motivation

The striptags filter throws a TypeError when rendering a processed text item.

Steps to reproduce

1. Entity should have text field. Let's say this is a media and the field name is description.
2. Run the replacePlaceholders() methods on this field, with the striptags filter applied.
3. The call will throw.

Example:

Assume we have a media with ID 1 that has a field named field_description.

    /** @var \Drupal\typed_data\PlaceholderResolver $placeholderResolver */
    $placeholderResolver = \Drupal::service('typed_data.placeholder_resolver');

    $result = $placeholderResolver->replacePlaceHolders('{{ media.field_description|striptags }}', [
      'media' => Media::load(1)?->getTypedData(),
    ]);

Proposed resolution

When using strict_types=1, PHP does not automatically cast an object to a string, even when it has a __toString() method. The solution is to manually cast the $value to a string.

🐛 Bug report
Status

Active

Version

2.1

Component

Data filters

Created by

🇸🇮Slovenia miha.wagner

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @miha.wagner
  • Pipeline finished with Success
    5 days ago
    Total: 253s
    #391912
  • 🇺🇸United States tr Cascadia

    OK. Can you add a few asserts to the DataFilterTest to test this use case? Right now it just tests passing a literal string. This should be enhanced to check a Stringable object.

  • 🇸🇮Slovenia miha.wagner

    I've added a test case to test with a $value of FilteredMarkup. Though this begs the question, if the value is being cast to a string via an explicit cast, maybe it would also make sense to check if the value implements __toString() or implements \Stringable in the canFilter method?

  • Pipeline finished with Success
    4 days ago
    Total: 5492s
    #392782
  • 🇺🇸United States tr Cascadia

    Thanks for the test. As expected, the new test passes with your changes, and also as expected the test-only (without the other changes) fails (see the test output). That's a good sign.

    Note: you didn't use the $htmlString variable you defined...

    Yes, I intended that canFilter() should be changed, if necessary. But canFilter() is checking on the core Drupal data type via the DataDefinition, not on the PHP data type. I think the core 'string' data type should be sufficient here.

    But one thing about the test:

    $data = $this->typedDataManager->create(DataDefinition::create('string'), FilteredMarkup::create('<b>Test <em>striptags</em> filter</b>'));
    
    $this->assertEquals('Test striptags filter', $filter->filter($data->getDataDefinition(), $data->getValue(), []))
    

    Looking at this and the other data filter tests makes me question a few things about how all the data filters are implemented. Your original issue seems to indicate that there's a bigger problem here that needs more than just a type cast to fix.

    In this case, I would have expected that $data is of the 'string' data type, and that when initialized with a FilteredMarkup, there would be some internal casting or coercion to change the FilteredMarkup into a string data type. But that doesn't seem to be the case because $data->getValue() returns a FilteredMarkup instead of a string. That's part of the core string DataType, and suggests to me maybe we should be using $data->getCastedValue() in all these data filters AND in PlaceholderResolver rather than $data->getValue().

    In all the tests, filter() is called with $data->getDataDefinition() and $data->getValue(), but there is nothing that prevents the caller passing two parameters that have nothing to do with each other, like $first_data->getDataDefinition() and $second_data->getValue(). The only place filter() is called is inside PlaceholderResolver, where these two values are always taken from the same variable $data, but that's not guaranteed if filter() is called from any other place or any other module.

    Likewise, filtersTo() is returning a 'string' DataDefinition object. So what I would expect filter() to return is a TypedData object of that type. But instead filter() in this case is returning a simple PHP string. But that's probably best left for a different issue, because changing the return type of filter() could have a lot of consequences.

    So the above patch is doing a cast inside of filter($definition, $value), which is making the assumptions that:
    1) $definition is of type 'string'
    2) $value can be cast to a 'string'

    Instead, I think ALL the data filters should be doing something like this:

    public function filter(DataDefinitionInterface $definition, $value, array $arguments, ?BubbleableMetadata $bubbleable_metadata = NULL) {
      $data = $this->typedDataManager->create($definition, $value) ;
      return strip_tags($data->getCastedValue());
    }

    You see what I am doing? Instead of assuming that $definition and $value are related to each other, I force $value to be treated like $definition. Do you think this makes sense?

  • Pipeline finished with Failed
    2 days ago
    Total: 152s
    #394364
  • Pipeline finished with Success
    2 days ago
    Total: 154s
    #394368
  • 🇸🇮Slovenia miha.wagner

    Yes that makes sense. I've gone ahead and changed that. If the other filters will also be doing it like this then I thought it
    made sense to do the preparing of the value in the base class. What do you think about that change?

Production build 0.71.5 2024