Follow-up from #2506479: Replace !placeholder with @placeholder for non URLs in t() in Views, except for t() output that is used as an attribute value.

Problem/Motivation

HTML attributes may contain characters that need to be HTML-escaped to be displayed correctly. This requires the ! placeholder, since otherwise they would be double-escaped.

Proposed resolution

Convert safe strings to plain text before escaping them, keep regular escaping for unsafe strings.

Remaining tasks

Reviews

User interface changes

None

API changes

None

Data model changes

None

Comments

catch created an issue. See original summary.

dawehner’s picture

@catch
Can you specific which one you talk about?

I just see examples of @url and !url pretty much. Used "=\"@\"" and and =\"!\"

catch’s picture

Status: Active » Postponed

@dawehner this is not about placeholders, it's about the return value of t() - for example using that in a title or alt attribute.

See #2506479-45: Replace !placeholder with @placeholder for non URLs in t() in Views, except for t() output that is used as an attribute value.

https://www.drupal.org/files/issues/views-placeholder-2506479.45.interdi... is the list of t() calls that eventually end up as the value of an HTML attribute.

Postponing this on #2509218: Ensure that SafeString objects can be used in non-HTML contexts.

lauriii’s picture

Status: Postponed » Needs work
Issue tags: +Needs issue summary update
dawehner’s picture

Issue summary: View changes
alexpott’s picture

This issue will need to add and test an HTML to attribute output formatter and test it.

lauriii’s picture

Assigned: Unassigned » lauriii

Will be working on this

plach’s picture

@laurii: @alexpott:

Actually @catch suggested in #2509218-142: Ensure that SafeString objects can be used in non-HTML contexts that we don't need and output strategy for HTML attributes, see also the recommended way in the related CR.

catch’s picture

For these cases can't we cover to plain text then stick it into Attributes? Reviewing from phone so haven't revisited the Views code.

dawehner’s picture

So $display_title is

  public function getDisplayLabel(ViewUI $view, $display_id, $check_changed = TRUE) {
    $display = $view->get('display');
    $title = $display_id == 'default' ? $this->t('Master') : $display[$display_id]['display_title'];
    $title = views_ui_truncate($title, 25);

    if ($check_changed && !empty($view->changed_display[$display_id])) {
      $changed = '*';
      $title = $title . $changed;
    }

    return $title;

So the problem is that t('Master') might have bad strings, which then ends up in the attribute. In the meantime I'll write on a test, just to be sure about it.

lauriii’s picture

StatusFileSize
new3.11 KB

This creates double autoescape for the Enable view mode button. Reproduce by adding a view mode with html inside display name, then disable view mode.

plach’s picture

I think it was View display not view mode

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new1.48 KB

Here is a test coverage. It just proves that there is no problem yet.

plach’s picture

Assigned: lauriii » plach

Having a look to this one

plach’s picture

Assigned: plach » Unassigned
StatusFileSize
new10.94 KB
new10.56 KB

Discussed with @alexpott and @dawehner a slightly different way: since attribute values will always be autoescaped by Twig, we just need to convert safe markup to string and we are done.

plach’s picture

StatusFileSize
new2.84 KB
new8.31 KB

Moar test coverage

plach’s picture

I updated the CR at https://www.drupal.org/node/2574697 to reflect the new approach taken here.

lauriii’s picture

+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -122,6 +124,11 @@ protected function createAttributeValue($name, $value) {
+      if (SafeMarkup::isSafe($value)) {
+        $value = PlainTextOutput::renderFromHtml($value);

Just curious, why are we testing the safeness here?

The last submitted patch, 15: safe_markup-t_views_attr-2571673-15.patch, failed testing.

plach’s picture

Assigned: Unassigned » plach

Failures...

Status: Needs review » Needs work

The last submitted patch, 16: safe_markup-t_views_attr-2571673-16.patch, failed testing.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Form/FormBuilder.php
    +++ b/core/lib/Drupal/Core/Form/FormBuilder.php
    @@ -1307,7 +1307,10 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
    
    @@ -1307,7 +1307,10 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
         // long as $form['#name'] puts the value at the top level of the tree of
         // \Drupal::request()->request data.
         $input = $form_state->getUserInput();
    -    if (isset($input[$element['#name']]) && $input[$element['#name']] == $element['#value']) {
    +    // Browsers submit the unescaped version of the input submit value, but the
    +    // element "value" attribute is supposed to hold plain text, so HTML markup
    +    // should be escaped.
    +    if (isset($input[$element['#name']]) && Html::escape($input[$element['#name']]) == $element['#value']) {
           return TRUE;
    

    We should better expand the unit tests here

  2. +++ b/core/modules/views_ui/src/Tests/DisplayTest.php
    @@ -215,6 +217,35 @@ public function testViewStatus() {
    +      $escaped = substr($input, 0, 25) . '...';
    

    We could just call out to views_ui_truncate()

The last submitted patch, 15: safe_markup-t_views_attr-2571673-15.patch, failed testing.

The last submitted patch, 16: safe_markup-t_views_attr-2571673-16.patch, failed testing.

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new5.31 KB
new11.37 KB

Addressed #22, now working on removing ! support altogether.

plach’s picture

@lauriii:

Just curious, why are we testing the safeness here?

Because safe strings have already been escaped, instead unsafe strings need regular escaping.

plach’s picture

StatusFileSize
new549 bytes
new11.4 KB

Just a small clean-up, removing of ! is being addressed in #2571695: Remove !placeholder and unsafe string return from SafeMarkup::format(). If this is green we should be done here.

plach’s picture

Assigned: plach » Unassigned
dawehner’s picture

StatusFileSize
new13 KB
new3.17 KB

The patch is looking great beside this additional test coverage

pwolanin’s picture

Is there test coverage for the change in FormBuilder?

plach’s picture

Yes, see the FormBuilderTest hunk.

dawehner’s picture

Its green and I consider it as green.

Should the Attribute class explicit document this new behaviour? It has quite some documentation already.

plach’s picture

StatusFileSize
new1.05 KB
new13.82 KB

Added docs. I reviewed the additional test coverage @dawehner provided and this is RTBC on my end.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +D8 Accelerate

Perfect!

xjm’s picture

There are two still !placeholder in Views with this applied:

core/modules/views/src/Plugin/views/filter/StringFilter.php:        'short' => $this->t('!has'),
core/modules/views_ui/src/ViewEditForm.php:            '#value' => $this->t('Duplicate as !type', array('!type' => $label)),

Should those be addressed here or in a separate issue?

plach’s picture

plach’s picture

Status: Reviewed & tested by the community » Needs work

Nope, sorry, one is an attribute.

dawehner’s picture

Given our discussion the second one needs some conversion, the first one is totally fine.

xjm’s picture

So we talked about the first hunk there; it's not a placeholder... there are no $args. It's just actually the string !has.

Not sure how a translator would ever translate that into their own language since it just looks like a placeholder. :P But that's not in scope here.

Second hunk though will need to be addressed.

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new766 bytes
new14.35 KB

Fixed #36.

dawehner’s picture

Status: Needs review » Needs work
+++ b/core/modules/views_ui/src/ViewEditForm.php
@@ -456,7 +456,7 @@ public function getDisplayDetails($view, $display) {
+            '#value' => $this->t('Duplicate as @type', ['@type' => $label]),

so $label is coming from \Drupal\views\Views::fetchPluginNames, which does the following bits:

      if (empty($plugin['no_ui']) && (empty($base) || empty($plugin['base']) || array_intersect($base, $plugin['base']))) {
        $plugins[$id] = (string) $plugin['title'];
      }

which will loose safeness

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new539 bytes
new14.88 KB

Alright

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you!

catch’s picture

  1. +++ b/core/lib/Drupal/Core/Form/FormBuilder.php
    @@ -1307,7 +1307,9 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
         $input = $form_state->getUserInput();
    -    if (isset($input[$element['#name']]) && $input[$element['#name']] == $element['#value']) {
    +    // Browsers submit the unescaped version of the input value, so we need to
    +    // make sure the compared values have the same "encoding".
    +    if (isset($input[$element['#name']]) && Html::decodeEntities($input[$element['#name']]) == Html::decodeEntities($element['#value'])) {
           return TRUE;
    

    Slightly confused why this breaks. If we don't decode entities for both variables, then we decode entities for both variables, why would the former comparison fail?

  2. +++ b/core/lib/Drupal/Core/Template/Attribute.php
    @@ -121,7 +134,13 @@ protected function createAttributeValue($name, $value) {
         return $value;
    

    I'd been hesitant about adding support for this, but in practice this is really straightforward and we have a great way to distinguish between plain text and HTML via SafeStringInterface. Also it's good that AttributeString still uniformly does Html::escape(). So generally glad we got to this point.

dawehner’s picture

Slightly confused why this breaks. If we don't decode entities for both variables, then we decode entities for both variables, why would the former comparison fail?

Does that mean we need to check first for safeness on the $element['#value'] before converting it and otherwise use the raw value?

plach’s picture

Slightly confused why this breaks. If we don't decode entities for both variables, then we decode entities for both variables, why would the former comparison fail?

The comparison may fail if we provide a safe string as $element['#value'] having HTML markup inside (HTML-escaped during the comparison): the value will be sent as unescaped by the browser. For instance:

$element['#value'] = t('Tag @foo', ['@foo' => '<foo>']);
// In the page source appears as "Tag &lt;foo&gt;"
// The browser posts "Tag <foo>"
plach’s picture

Does that mean we need to check first for safeness on the $element['#value'] before converting it and otherwise use the raw value?

I don't think so, I discussed this with @alexpott: the current approach will work in any scenario (as the tests show).

alexpott’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Needs work
StatusFileSize
new6.51 KB
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -1307,7 +1307,9 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
+    // Browsers submit the unescaped version of the input value, so we need to
+    // make sure the compared values have the same "encoding".
+    if (isset($input[$element['#name']]) && Html::decodeEntities($input[$element['#name']]) == Html::decodeEntities($element['#value'])) {

It is not just that browsers submit the unescaped version of the input value is that they basically to a decode entities on the value of a button. <input type="button" value="Cancel <br&gt;"> will make a button that looks like this...

alexpott’s picture

I've tested this on multiple browsers on a Mac:
Chrome Version 45.0.2454.99 (64-bit)
Firefox 40.0.3
Safari Version 8.0.8

catch’s picture

OK so that really needs an inline comment. A bit of googling did not find any reference to browsers decoding entities like this. Apart from that RTBC for me though.

The last submitted patch, 30: 2571673-29.patch, failed testing.

alexpott’s picture

The relevant docs are http://www.w3.org/TR/html401/interact/forms.html#h-17.4 which says that value is CDATA which means :

  • Replace character entities with characters,
  • Ignore line feeds,
  • Replace each carriage return or tab with a single space.

According to http://www.w3.org/TR/html401/types.html#type-cdata

plach’s picture

Status: Needs work » Needs review
StatusFileSize
new1.07 KB
new15.02 KB

Improved docs, thanks for the pointers :)

stefan.r’s picture

the first hunk is used in #2571695: Remove !placeholder and unsafe string return from SafeMarkup::format() as well, maybe we just copypaste from what we did there or wait for that to go in first? It also has tests.

catch’s picture

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -1316,7 +1316,11 @@ protected function buttonWasClicked($element, FormStateInterface &$form_state) {
+    if (isset($input[$element['#name']]) && Html::decodeEntities($input[$element['#name']]) == Html::decodeEntities($element['#value'])) {

Won't input already be entity decoded?

plach’s picture

StatusFileSize
new2.16 KB
new15.02 KB
dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you @plach
This documentation helps people to understand things and well, it seems to be an outstanding bug since forever in form API. If someone cares about it,
them could create a new isssue for backporting this particular change.

(No smiley here)

  • catch committed 0cd35ba on 8.0.x
    Issue #2571673 by plach, dawehner: Convert Views t() usage where it is...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

stefan.r’s picture

See #2575789: Decode entities when determining which button was clicked for the D7 backport, it just needs tests

Status: Fixed » Closed (fixed)

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