Skip to content

CSS file is deferred when CSS element has defer attribute in default_head_blocks.xml. #37814

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: 2.4-develop
Choose a base branch
from
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions lib/internal/Magento/Framework/View/Page/Config/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -344,43 +344,73 @@ protected function getGroupAttributes($group)
* Add default attributes
*
* @param string $contentType
* @param string $attributes
* @return string
* @param array|null $attributes
* @return array
*/
protected function addDefaultAttributes($contentType, $attributes)
{
$attributesString = '';
$defaultAttributes = [];

if ($contentType === 'js') {
return ' type="text/javascript" ' . $attributes;
$defaultAttributes['type'] = 'text/javascript';
}

if ($contentType === 'css') {
return ' rel="stylesheet" type="text/css" ' . ($attributes ?: ' media="all"');
$defaultAttributes['rel'] = 'stylesheet';
$defaultAttributes['type'] = 'text/css';

if (empty($attributes)) {
$attributes['media'] = 'all';
}

// add defer attributes when defer parameter is exist
if (is_array($attributes) && array_key_exists('defer', $attributes)) {
$defaultAttributes['rel'] = 'preload';
$defaultAttributes['as'] = 'style';
$defaultAttributes['onload'] = 'this.onload=null;this.rel=\'stylesheet\'';
}
}

if ($this->canTypeBeFont($contentType)) {
return 'rel="preload" as="font" crossorigin="anonymous"';
$defaultAttributes['rel'] = 'preload';
$defaultAttributes['as'] = 'font';
$defaultAttributes['crossorigin'] = 'anonymous';
}

return $attributes;
// merge current attributes with default attributes
if ($attributes) {
return array_merge($defaultAttributes, $attributes);
}

return $defaultAttributes;
}

/**
* Returns assets template
*
* @param string $contentType
* @param string|null $attributes
* @param array|null $attributes
* @return string
*/
protected function getAssetTemplate($contentType, $attributes)
{
$attributesString = '';
foreach ($attributes as $name => $value) {
$attributesString .= ' ' . $name . '="' . $this->escaper->escapeHtml($value) . '"';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be escapeHtmlAttr instead of escapeHtml I think?


And in the case of the onload one, it should be escapeJs inside a escapeHtmlAttr according to the docs:

Case: All JavaScript inside attributes must be escaped by escapeJs before escapeHtmlAttr:

Copy link
Author

@nurullah nurullah Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escapeHtmlAttr function escapes slash character and it looks like the below;

image

It's pretty useful for body element attributes but not for head element attributes. Also, before this pull request, the same escapeHtml function was used for the attribute value.

foreach ($attributes as $name => $value) {
$attributesString .= ' ' . $name . '="' . $this->escaper->escapeHtml($value) . '"';
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I know too little about this, would be great if somebody with a little bit of security knowledge could double check this here then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nathanjosiah: if you have a little bit of time, do you have some input here?

Copy link
Contributor

@nathanjosiah nathanjosiah Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the main issue is that escapeHtmlAttr escaped all chars then that is correct behavior even for head.

For example:

<html>
<head>
<style id="foo&#x2F;bar"></style>
</head>
<body>
Test succeeds if this is green
<script>document.getElementById('foo/bar').innerHTML = 'body { color: green; }';</script>
</body>
</html>

The text would be green.

Here is a small demo that shows this off.

Copy link
Contributor

@nathanjosiah nathanjosiah Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

escapeHtmlAttr is appropriate for all attributes including script attributes. You would only need to combine escapeJs with escapeHtmlAttr if there is additional content being injected into the JS. For example from our devdocs

<div
    onclick="<?= $escaper->escapeHtmlAttr('handler("' . $escaper->escapeJs($aString) . '", ' . $escaper->escapeJs($aVar) .')') ?>">
    My DIV
</div>

As far as I can see, this PR is using the escaper methods correctly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this PR is still waiting for your review @hostep. If there are no other problems, can you take this to the next step? Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a lot of expert knowledge on the subject but since somebody forced me to review this (I wasn't able to unassign myself), I approved it anyways.

Copy link
Member

@sky-hub sky-hub Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please be advised that because of too much escaping we end up with this in page source:

image

Copy link
Contributor

@hostep hostep Dec 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sky-hub: and how is that a problem? A browser should be able to decode those without an issue. It's like writing &gt; or &lt; in the html source to display > or <. In this case &#x2F; translates to / for example. It's redundant and will increase the payload size a bit sure, but it shouldn't break anything.

}

switch ($contentType) {
case 'js':
$groupTemplate = '<script ' . $attributes . ' src="%s"></script>' . "\n";
$groupTemplate = '<script ' . $attributesString . ' src="%s"></script>' . "\n";
break;

case 'css':
default:
$groupTemplate = '<link ' . $attributes . ' href="%s" />' . "\n";
$groupTemplate = '<link ' . $attributesString . ' href="%s" />' . "\n";
if (array_key_exists('defer', $attributes)) {
$groupTemplate .= '<noscript><link rel="stylesheet" href="%1$s"></noscript>' . "\n";
}
break;
}
return $groupTemplate;
Expand Down Expand Up @@ -411,7 +441,7 @@ protected function processIeCondition($groupHtml, $group)
protected function renderAssetHtml(\Magento\Framework\View\Asset\PropertyGroup $group)
{
$assets = $this->processMerge($group->getAll(), $group);
$attributes = $this->getGroupAttributes($group);
$attributes = $group->getProperty('attributes');

$result = '';
$template = '';
Expand Down