MHTML: fix some issues with styles
This CL makes two changes: 1. For <style> nodes that have unmodified stylesheets, do not modify the <style> node. Because round-tripping from text to CSS rules back to text is lossy, this sometimes avoids problems. See crbug.com/40804066 for why it's lossy. 2. For <style> nodes that have been modified, serialize them inline as a <style> node rather than as a <link> node. Serializing styles into another resource changes the baseURL, which will break how relative URLs are interpreted. Bug: 363289333 Change-Id: Id165d21e5ab61f41b60d27e3c030d24da7832615 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5883007 Reviewed-by: Min Qin <qinmin@chromium.org> Reviewed-by: Daniel Cheng <dcheng@chromium.org> Commit-Queue: Dan H <harringtond@chromium.org> Cr-Commit-Position: refs/heads/main@{#1373409}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
8ecbee8bec
commit
3436d753b0
content
browser
download
test
third_party/blink/renderer
core
editing
serializers
frame
modules
content_extraction
@ -78,7 +78,18 @@ const char kGetPageInfoScript[] = R"js(
|
||||
const styleKeys = ['font-family', 'line-height', 'display'];
|
||||
function elementStyles(el) {
|
||||
const styles = window.getComputedStyle(el);
|
||||
return Object.fromEntries(styleKeys.map(name => [name, styles[name]]));
|
||||
const result = Object.fromEntries(styleKeys
|
||||
.map(name => [name, styles[name]])
|
||||
.filter(v=>v[1]));
|
||||
// add background-image, but only the file name because the full path will
|
||||
// change in the saved page.
|
||||
let m = styles.backgroundImage.match(/url\((.*)\)/);
|
||||
if (m) {
|
||||
const url = m[1];
|
||||
const parts = url.split('/');
|
||||
result['backgroundImageFile'] = parts[parts.length-1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function isVisible(el) {
|
||||
const styles = window.getComputedStyle(el);
|
||||
@ -157,8 +168,7 @@ struct CompareResult {
|
||||
// A dummy WebContentsDelegate which tracks the results of a find operation.
|
||||
class FindTrackingDelegate : public WebContentsDelegate {
|
||||
public:
|
||||
explicit FindTrackingDelegate(const std::string& search)
|
||||
: search_(search), matches_(-1) {}
|
||||
explicit FindTrackingDelegate(const std::string& search) : search_(search) {}
|
||||
|
||||
FindTrackingDelegate(const FindTrackingDelegate&) = delete;
|
||||
FindTrackingDelegate& operator=(const FindTrackingDelegate&) = delete;
|
||||
@ -197,7 +207,7 @@ class FindTrackingDelegate : public WebContentsDelegate {
|
||||
|
||||
private:
|
||||
std::string search_;
|
||||
int matches_;
|
||||
int matches_ = -1;
|
||||
base::RunLoop run_loop_;
|
||||
};
|
||||
|
||||
@ -374,7 +384,6 @@ class RespondAndDisconnectMockWriter
|
||||
~RespondAndDisconnectMockWriter() override = default;
|
||||
};
|
||||
|
||||
|
||||
class MHTMLGenerationTest : public ContentBrowserTest,
|
||||
public testing::WithParamInterface<bool> {
|
||||
public:
|
||||
@ -986,7 +995,8 @@ IN_PROC_BROWSER_TEST_P(MHTMLGenerationImprovedTest, Styles) {
|
||||
|
||||
CompareOptions options;
|
||||
options.expected_number_of_frames = 1;
|
||||
options.expected_substrings = {"hidden1", "hidden4"};
|
||||
options.expected_substrings = {"hidden1", "hidden4",
|
||||
"This should show if inline CSS is escaped."};
|
||||
options.forbidden_substrings = {"hidden2", "hidden3"};
|
||||
CompareResult result = TestOriginalVsSavedPage(
|
||||
embedded_test_server()->GetURL("/mhtml/styles.html"), params, options);
|
||||
|
@ -7355,6 +7355,8 @@ data/media/webrtc_test_common.js
|
||||
data/media/webrtc_test_utilities.js
|
||||
data/mhtml/custom_element_defined.html
|
||||
data/mhtml/custom_element_defined_in_frame.html
|
||||
data/mhtml/imported_imported.css
|
||||
data/mhtml/imported_inline.css
|
||||
data/mhtml/style.css
|
||||
data/mhtml/styles.html
|
||||
data/midi/request_midi_access.html
|
||||
|
14
content/test/data/mhtml/imported_imported.css
Normal file
14
content/test/data/mhtml/imported_imported.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Copyright 2024 The Chromium Authors
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file. */
|
||||
|
||||
/*
|
||||
This style won't serialize correctly because the spec does not allow it, see
|
||||
crbug.com/40804066. Because this style element is not modified by JS, Chrome
|
||||
should leave it alone rather than serialize a replacement.
|
||||
*/
|
||||
#font-c {
|
||||
--font-sans: sans;
|
||||
font: normal 600 60px var(--font-sans);
|
||||
line-height: 30px;
|
||||
}
|
15
content/test/data/mhtml/imported_inline.css
Normal file
15
content/test/data/mhtml/imported_inline.css
Normal file
@ -0,0 +1,15 @@
|
||||
/* Copyright 2024 The Chromium Authors
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file. */
|
||||
|
||||
@import url("imported_imported.css");
|
||||
/*
|
||||
This style won't serialize correctly because the spec does not allow it, see
|
||||
crbug.com/40804066. Because this style element is not modified by JS, Chrome
|
||||
should leave it alone rather than serialize a replacement.
|
||||
*/
|
||||
#font-b {
|
||||
--font-sans: sans;
|
||||
font: normal 600 60px var(--font-sans);
|
||||
line-height: 30px;
|
||||
}
|
@ -1,15 +1,38 @@
|
||||
<head>
|
||||
<style id="inlinestyle1">
|
||||
|
||||
/* An inline style can't contain the end style tag. This escaped url string
|
||||
evaluates to the style end tag, so it should remain escaped in the serialized
|
||||
MHTML page. */
|
||||
p#p-end-style-tag {
|
||||
display: none;
|
||||
background-image: url("\3C/style>");
|
||||
}
|
||||
p#p-end-style-tag {
|
||||
display: revert;
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<style>
|
||||
@import url("imported_inline.css");
|
||||
#hidden1 {
|
||||
display: block;
|
||||
}
|
||||
#hidden2 {
|
||||
display: none;
|
||||
}
|
||||
/*
|
||||
This style won't serialize correctly because the spec does not allow it, see
|
||||
crbug.com/40804066. Because this style element is not modified by JS, Chrome
|
||||
should leave it alone rather than serialize a replacement.
|
||||
*/
|
||||
#font-a {
|
||||
--font-sans: sans;
|
||||
font: normal 600 60px var(--font-sans);
|
||||
line-height: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -26,6 +49,28 @@
|
||||
<!--Hidden by first inline style, Shown by <link>.-->
|
||||
<div id="hidden4">hidden4</div>
|
||||
|
||||
|
||||
|
||||
<div id="font-a">font-a</div>
|
||||
<!--Style affected by imported_inline.css.
|
||||
TODO(crbug.com/363289333): This element's style won't be correct yet
|
||||
in the serialized mhtml. We should write imported_inline.css without
|
||||
any changes.
|
||||
|
||||
<div id="font-b">font-b</div>
|
||||
-->
|
||||
|
||||
<!--Style affected by imported_imported.css.
|
||||
TODO(crbug.com/363289333): This element's style won't be correct yet
|
||||
in the serialized mhtml. We should write imported_inline.css without
|
||||
any changes.
|
||||
|
||||
<div id="font-c">font-b</div>
|
||||
-->
|
||||
|
||||
<div id="font-d">font-d</div>
|
||||
|
||||
<p id="p-end-style-tag">This should show if inline CSS is escaped.</p>
|
||||
<script>
|
||||
// Ensure we serialize the modified stylesheet.
|
||||
const sheet1 = document.getElementById("inlinestyle1").sheet;
|
||||
@ -35,6 +80,15 @@
|
||||
sheet1.insertRule(`#hidden4 {
|
||||
display: none;
|
||||
}`);
|
||||
// Ensure a style appended like this isn't re-serialized incorrectly.
|
||||
const sheet2 = document.createElement("style");
|
||||
sheet2.innerText = `
|
||||
#hidden-by-appended-style {
|
||||
--font-sans: sans;
|
||||
font: normal 600 60px var(--font-sans);
|
||||
line-height: 30px;
|
||||
}`;
|
||||
document.head.appendChild(sheet2);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
@ -183,15 +183,15 @@ void MarkupAccumulator::AppendStartMarkup(const Node& node) {
|
||||
|
||||
void MarkupAccumulator::AppendCustomAttributes(const Element&) {}
|
||||
|
||||
MarkupAccumulator::EmitChoice MarkupAccumulator::WillProcessAttribute(
|
||||
MarkupAccumulator::EmitAttributeChoice MarkupAccumulator::WillProcessAttribute(
|
||||
const Element& element,
|
||||
const Attribute& attribute) const {
|
||||
return EmitChoice::kEmit;
|
||||
return EmitAttributeChoice::kEmit;
|
||||
}
|
||||
|
||||
MarkupAccumulator::EmitChoice MarkupAccumulator::WillProcessElement(
|
||||
MarkupAccumulator::EmitElementChoice MarkupAccumulator::WillProcessElement(
|
||||
const Element& element) {
|
||||
return EmitChoice::kEmit;
|
||||
return EmitElementChoice::kEmit;
|
||||
}
|
||||
|
||||
AtomicString MarkupAccumulator::AppendElement(const Element& element) {
|
||||
@ -210,7 +210,8 @@ AtomicString MarkupAccumulator::AppendElement(const Element& element) {
|
||||
AppendAttribute(element, Attribute(html_names::kIsAttr, is_value));
|
||||
}
|
||||
for (const auto& attribute : attributes) {
|
||||
if (EmitChoice::kEmit == WillProcessAttribute(element, attribute)) {
|
||||
if (EmitAttributeChoice::kEmit ==
|
||||
WillProcessAttribute(element, attribute)) {
|
||||
AppendAttribute(element, attribute);
|
||||
}
|
||||
}
|
||||
@ -226,7 +227,8 @@ AtomicString MarkupAccumulator::AppendElement(const Element& element) {
|
||||
if (!EqualIgnoringNullity(attribute.Value(), element.namespaceURI()))
|
||||
continue;
|
||||
}
|
||||
if (EmitChoice::kEmit == WillProcessAttribute(element, attribute)) {
|
||||
if (EmitAttributeChoice::kEmit ==
|
||||
WillProcessAttribute(element, attribute)) {
|
||||
AppendAttribute(element, attribute);
|
||||
}
|
||||
}
|
||||
@ -631,7 +633,8 @@ void MarkupAccumulator::SerializeNodesWithNamespaces(
|
||||
}
|
||||
|
||||
const auto& target_element = To<Element>(target_node);
|
||||
if (WillProcessElement(target_element) == EmitChoice::kIgnore) {
|
||||
EmitElementChoice emit_choice = WillProcessElement(target_element);
|
||||
if (emit_choice == EmitElementChoice::kIgnore) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -644,33 +647,37 @@ void MarkupAccumulator::SerializeNodesWithNamespaces(
|
||||
bool has_end_tag =
|
||||
!(SerializeAsHTML() && ElementCannotHaveEndTag(target_element));
|
||||
if (has_end_tag) {
|
||||
const Node* parent = &target_element;
|
||||
if (auto* template_element =
|
||||
DynamicTo<HTMLTemplateElement>(target_element)) {
|
||||
// Declarative shadow roots that are currently being parsed will have a
|
||||
// null content() - don't serialize contents in this case.
|
||||
parent = template_element->content();
|
||||
}
|
||||
|
||||
// Traverses the shadow tree.
|
||||
std::pair<ShadowRoot*, Element*> auxiliary_pair =
|
||||
GetShadowTree(target_element);
|
||||
if (ShadowRoot* auxiliary_tree = auxiliary_pair.first) {
|
||||
Element* enclosing_element = auxiliary_pair.second;
|
||||
AtomicString enclosing_element_prefix;
|
||||
if (enclosing_element)
|
||||
enclosing_element_prefix = AppendElement(*enclosing_element);
|
||||
for (const Node& child : Strategy::ChildrenOf(*auxiliary_tree))
|
||||
SerializeNodesWithNamespaces<Strategy>(child, kIncludeNode);
|
||||
if (enclosing_element) {
|
||||
WillCloseSyntheticTemplateElement(*auxiliary_tree);
|
||||
AppendEndTag(*enclosing_element, enclosing_element_prefix);
|
||||
if (emit_choice != EmitElementChoice::kEmitButIgnoreChildren) {
|
||||
const Node* parent = &target_element;
|
||||
if (auto* template_element =
|
||||
DynamicTo<HTMLTemplateElement>(target_element)) {
|
||||
// Declarative shadow roots that are currently being parsed will have a
|
||||
// null content() - don't serialize contents in this case.
|
||||
parent = template_element->content();
|
||||
}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
for (const Node& child : Strategy::ChildrenOf(*parent)) {
|
||||
SerializeNodesWithNamespaces<Strategy>(child, kIncludeNode);
|
||||
// Traverses the shadow tree.
|
||||
std::pair<ShadowRoot*, Element*> auxiliary_pair =
|
||||
GetShadowTree(target_element);
|
||||
if (ShadowRoot* auxiliary_tree = auxiliary_pair.first) {
|
||||
Element* enclosing_element = auxiliary_pair.second;
|
||||
AtomicString enclosing_element_prefix;
|
||||
if (enclosing_element) {
|
||||
enclosing_element_prefix = AppendElement(*enclosing_element);
|
||||
}
|
||||
for (const Node& child : Strategy::ChildrenOf(*auxiliary_tree)) {
|
||||
SerializeNodesWithNamespaces<Strategy>(child, kIncludeNode);
|
||||
}
|
||||
if (enclosing_element) {
|
||||
WillCloseSyntheticTemplateElement(*auxiliary_tree);
|
||||
AppendEndTag(*enclosing_element, enclosing_element_prefix);
|
||||
}
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
for (const Node& child : Strategy::ChildrenOf(*parent)) {
|
||||
SerializeNodesWithNamespaces<Strategy>(child, kIncludeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,12 +65,22 @@ class CORE_EXPORT MarkupAccumulator {
|
||||
CORE_EXPORT String SerializeNodes(const Node&, ChildrenOnly);
|
||||
|
||||
protected:
|
||||
// Determines whether an element or attribute is emitted as markup.
|
||||
enum class EmitChoice {
|
||||
// Determines whether an attribute is emitted as markup.
|
||||
enum class EmitAttributeChoice {
|
||||
// Emit the attribute as markup.
|
||||
kEmit,
|
||||
// Do not emit the attribute.
|
||||
kIgnore,
|
||||
};
|
||||
|
||||
// Determines whether an element is emitted as markup.
|
||||
enum class EmitElementChoice {
|
||||
// Emit it as markup.
|
||||
kEmit,
|
||||
// Do not emit it or any children (for elements).
|
||||
// Do not emit the element or any children.
|
||||
kIgnore,
|
||||
// Emit the element, but not its children.
|
||||
kEmitButIgnoreChildren,
|
||||
};
|
||||
|
||||
// Returns serialized prefix. It should be passed to AppendEndTag().
|
||||
@ -81,7 +91,7 @@ class CORE_EXPORT MarkupAccumulator {
|
||||
// This is called just before emitting markup for `element`. Derived classes
|
||||
// may emit markup here, i.e., if they want to provide a substitute for this
|
||||
// element.
|
||||
virtual EmitChoice WillProcessElement(const Element& element);
|
||||
virtual EmitElementChoice WillProcessElement(const Element& element);
|
||||
// Called just before closing a <template> element used to serialize a
|
||||
// shadow root. `auxiliary_tree` is the shadow root that has just been
|
||||
// serialized into the <template> element.
|
||||
@ -127,8 +137,8 @@ class CORE_EXPORT MarkupAccumulator {
|
||||
AtomicString GeneratePrefix(const AtomicString& new_namespace);
|
||||
|
||||
virtual void AppendCustomAttributes(const Element&);
|
||||
virtual EmitChoice WillProcessAttribute(const Element&,
|
||||
const Attribute&) const;
|
||||
virtual EmitAttributeChoice WillProcessAttribute(const Element&,
|
||||
const Attribute&) const;
|
||||
|
||||
// Returns a shadow tree that needs also to be serialized. The ShadowRoot is
|
||||
// returned as the 1st element in the pair, and can be null if no shadow tree
|
||||
|
@ -125,6 +125,33 @@ void AppendLinkElement(StringBuilder& markup, const KURL& url) {
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace internal {
|
||||
// TODO(crbug.com/363289333): Try to add this functionality to wtf::String.
|
||||
String ReplaceAllCaseInsensitive(
|
||||
String source,
|
||||
const String& from,
|
||||
base::FunctionRef<String(const String&)> transform) {
|
||||
size_t offset = 0;
|
||||
size_t pos;
|
||||
StringBuilder builder;
|
||||
for (;;) {
|
||||
pos = source.Find(from, offset,
|
||||
TextCaseSensitivity::kTextCaseASCIIInsensitive);
|
||||
if (pos == kNotFound) {
|
||||
break;
|
||||
}
|
||||
builder.Append(source.Substring(offset, pos - offset));
|
||||
builder.Append(transform(source.Substring(pos, from.length())));
|
||||
offset = pos + from.length();
|
||||
}
|
||||
if (builder.empty()) {
|
||||
return source;
|
||||
}
|
||||
builder.Append(source.Substring(offset));
|
||||
return builder.ToString();
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
// Stores the list of serialized resources which constitute the frame. The
|
||||
// first resource should be the frame's content (usually HTML).
|
||||
class MultiResourcePacker {
|
||||
@ -301,8 +328,9 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
return true;
|
||||
}
|
||||
|
||||
EmitChoice WillProcessAttribute(const Element& element,
|
||||
const Attribute& attribute) const override {
|
||||
EmitAttributeChoice WillProcessAttribute(
|
||||
const Element& element,
|
||||
const Attribute& attribute) const override {
|
||||
// TODO(fgorski): Presence of srcset attribute causes MHTML to not display
|
||||
// images, as only the value of src is pulled into the archive. Discarding
|
||||
// srcset prevents the problem. Long term we should make sure to MHTML plays
|
||||
@ -310,7 +338,7 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
if (IsA<HTMLImageElement>(element) &&
|
||||
(attribute.LocalName() == html_names::kSrcsetAttr ||
|
||||
attribute.LocalName() == html_names::kSizesAttr)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitAttributeChoice::kIgnore;
|
||||
}
|
||||
|
||||
// Do not save ping attribute since anyway the ping will be blocked from
|
||||
@ -318,7 +346,7 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
// TODO(crbug.com/369219144): Should this be IsA<HTMLAnchorElementBase>?
|
||||
if (IsA<HTMLAnchorElement>(element) &&
|
||||
attribute.LocalName() == html_names::kPingAttr) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitAttributeChoice::kIgnore;
|
||||
}
|
||||
|
||||
// The special attribute in a template element to denote the shadow DOM
|
||||
@ -328,7 +356,7 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
(attribute.LocalName() == kShadowModeAttributeName ||
|
||||
attribute.LocalName() == kShadowDelegatesFocusAttributeName) &&
|
||||
!shadow_template_elements_.Contains(&element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitAttributeChoice::kIgnore;
|
||||
}
|
||||
|
||||
// If srcdoc attribute for frame elements will be rewritten as src attribute
|
||||
@ -339,22 +367,22 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
String new_link_for_the_element;
|
||||
if (is_src_doc_attribute &&
|
||||
RewriteLink(element, new_link_for_the_element)) {
|
||||
return EmitChoice::kEmit;
|
||||
return EmitAttributeChoice::kEmit;
|
||||
}
|
||||
|
||||
// Drop integrity attribute for those links with subresource loaded.
|
||||
auto* html_link_element = DynamicTo<HTMLLinkElement>(element);
|
||||
if (attribute.LocalName() == html_names::kIntegrityAttr &&
|
||||
html_link_element && html_link_element->sheet()) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitAttributeChoice::kIgnore;
|
||||
}
|
||||
|
||||
// Do not include attributes that contain javascript. This is because the
|
||||
// script will not be executed when a MHTML page is being loaded.
|
||||
if (element.IsScriptingAttribute(attribute)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitAttributeChoice::kIgnore;
|
||||
}
|
||||
return EmitChoice::kEmit;
|
||||
return EmitAttributeChoice::kEmit;
|
||||
}
|
||||
|
||||
bool RewriteLink(const Element& element, String& rewritten_link) const {
|
||||
@ -457,16 +485,16 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
}
|
||||
}
|
||||
|
||||
EmitChoice WillProcessElement(const Element& element) override {
|
||||
EmitElementChoice WillProcessElement(const Element& element) override {
|
||||
if (IsA<HTMLScriptElement>(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
if (IsA<HTMLNoScriptElement>(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
auto* meta = DynamicTo<HTMLMetaElement>(element);
|
||||
if (meta && meta->ComputeEncoding().IsValid()) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
|
||||
if (MHTMLImprovementsEnabled()) {
|
||||
@ -476,33 +504,43 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
DynamicTo<HTMLStyleElement>(element)) {
|
||||
CSSStyleSheet* sheet = style_element->sheet();
|
||||
if (sheet) {
|
||||
AppendStylesheet(*sheet);
|
||||
return EmitChoice::kIgnore;
|
||||
// JS may update styles programmatically for a <style> node. We detect
|
||||
// whether this has happened, and serialize the stylesheet if it has.
|
||||
// Otherwise, we leave the <style> node unmodified. Because CSS
|
||||
// serialization isn't perfect, it's better to leave the original
|
||||
// <style> element if possible.
|
||||
SerializeCSSResources(*sheet);
|
||||
if (!sheet->Contents()->IsMutable()) {
|
||||
return EmitElementChoice::kEmit;
|
||||
} else {
|
||||
style_elements_to_replace_contents_.insert(style_element);
|
||||
return EmitElementChoice::kEmitButIgnoreChildren;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A <link> element is inserted in `AppendExtraForHeadElement()` as a
|
||||
// substitute for this element.
|
||||
if (IsA<HTMLStyleElement>(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldIgnoreHiddenElement(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
if (ShouldIgnoreMetaElement(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
if (web_delegate_->RemovePopupOverlay() &&
|
||||
ShouldIgnorePopupOverlayElement(element)) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
// Remove <link> for stylesheets that do not load.
|
||||
auto* html_link_element = DynamicTo<HTMLLinkElement>(element);
|
||||
if (html_link_element && html_link_element->RelAttribute().IsStyleSheet() &&
|
||||
!html_link_element->sheet()) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
return MarkupAccumulator::WillProcessElement(element);
|
||||
}
|
||||
@ -535,6 +573,14 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
if (IsA<HTMLHtmlElement>(element)) {
|
||||
AppendAdoptedStyleSheets(document_);
|
||||
}
|
||||
|
||||
if (const HTMLStyleElement* style_element =
|
||||
DynamicTo<HTMLStyleElement>(element)) {
|
||||
if (style_elements_to_replace_contents_.Contains(style_element)) {
|
||||
CSSStyleSheet* sheet = style_element->sheet();
|
||||
markup_.Append(SerializeInlineCSSStyleSheet(*sheet));
|
||||
}
|
||||
}
|
||||
}
|
||||
MarkupAccumulator::AppendEndTag(element, prefix);
|
||||
}
|
||||
@ -736,6 +782,44 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
}
|
||||
}
|
||||
|
||||
// Serializes `style_sheet` as text that can be added to an inline <style>
|
||||
// tag. Ensures the style sheet does not include the </style> end tag.
|
||||
String SerializeInlineCSSStyleSheet(CSSStyleSheet& style_sheet) {
|
||||
StringBuilder css_text;
|
||||
for (unsigned i = 0; i < style_sheet.length(); ++i) {
|
||||
CSSRule* rule = style_sheet.ItemInternal(i);
|
||||
String item_text = rule->cssText();
|
||||
if (!item_text.empty()) {
|
||||
css_text.Append(item_text);
|
||||
if (i < style_sheet.length() - 1) {
|
||||
css_text.Append("\n\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `css_text` is the text that has already been parsed from the <style> tag,
|
||||
// so it does not retain escape sequences. The only time we would need to
|
||||
// emit an escape sequence is if the </style> tag appears within `css_text`.
|
||||
// Parsing <style> contents is described in
|
||||
// https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state.
|
||||
// Note that when replacing the "style" text. HTML tags are case
|
||||
// insensitive, but this is escaped, so it's not not actually an HTML end
|
||||
// tag.
|
||||
return blink::internal::ReplaceAllCaseInsensitive(
|
||||
css_text.ToString(), "</style", [](const String& text) {
|
||||
StringBuilder builder;
|
||||
builder.Append("\\3C/"); // \3C = '<'.
|
||||
builder.Append(text.Substring(2));
|
||||
return builder.ReleaseString();
|
||||
});
|
||||
}
|
||||
|
||||
// Attempts to serialize a stylesheet, if necessary. Does a couple things:
|
||||
// 1. If `url` is valid and not a data URL, and we haven't already serialized
|
||||
// this url, then serialize the stylesheet into a new resource. Note that this
|
||||
// process is lossy, and may not perfectly reflect the intended style.
|
||||
// 2. Even if `url` is invalid or a data URL, serialize the resources within
|
||||
// `style_sheet`.
|
||||
void SerializeCSSStyleSheet(CSSStyleSheet& style_sheet, const KURL& url) {
|
||||
// If the URL is invalid or if it is a data URL this means that this CSS is
|
||||
// defined inline, respectively in a <style> tag or in the data URL itself.
|
||||
@ -794,12 +878,17 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
|
||||
// Sub resources need to be serialized even if the CSS definition doesn't
|
||||
// need to be.
|
||||
SerializeCSSResources(style_sheet);
|
||||
}
|
||||
|
||||
// Serializes resources referred to by `style_sheet`.
|
||||
void SerializeCSSResources(CSSStyleSheet& style_sheet) {
|
||||
for (unsigned i = 0; i < style_sheet.length(); ++i) {
|
||||
SerializeCSSRule(style_sheet.ItemInternal(i));
|
||||
SerializeCSSRuleResources(style_sheet.ItemInternal(i));
|
||||
}
|
||||
}
|
||||
|
||||
void SerializeCSSRule(CSSRule* rule) {
|
||||
void SerializeCSSRuleResources(CSSRule* rule) {
|
||||
DCHECK(rule->parentStyleSheet()->OwnerDocument());
|
||||
Document& document = *rule->parentStyleSheet()->OwnerDocument();
|
||||
|
||||
@ -815,6 +904,9 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
DCHECK(sheet_base_url.IsValid());
|
||||
KURL import_url = KURL(sheet_base_url, import_rule->href());
|
||||
if (import_rule->styleSheet()) {
|
||||
// TODO(crbug.com/363289333): When MHTMLImprovementsEnabled(), we
|
||||
// should avoid serializing the imported stylesheet, and instead fetch
|
||||
// the raw CSS resource.
|
||||
SerializeCSSStyleSheet(*import_rule->styleSheet(), import_url);
|
||||
}
|
||||
break;
|
||||
@ -830,7 +922,7 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
case CSSRule::kStartingStyleRule: {
|
||||
CSSRuleList* rule_list = rule->cssRules();
|
||||
for (unsigned i = 0; i < rule_list->length(); ++i) {
|
||||
SerializeCSSRule(rule_list->item(i));
|
||||
SerializeCSSRuleResources(rule_list->item(i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -925,6 +1017,8 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
// * Serialize styleSheets on shadow roots
|
||||
// * Retain stylesheet order, previously order of stylesheets
|
||||
// was sometimes wrong.
|
||||
// * Serialize <style> nodes as <style> nodes instead of <link> nodes.
|
||||
// * Leave <style> nodes alone if their stylesheet is unmodified.
|
||||
bool MHTMLImprovementsEnabled() const {
|
||||
return base::FeatureList::IsEnabled(blink::features::kMHTML_Improvements);
|
||||
}
|
||||
@ -941,6 +1035,11 @@ class SerializerMarkupAccumulator : public MarkupAccumulator {
|
||||
// Adopted stylesheets can be reused. This stores the set of stylesheets
|
||||
// already serialized as resources, along with their URL.
|
||||
HeapHashMap<Member<blink::CSSStyleSheet>, KURL> stylesheet_pseudo_urls_;
|
||||
|
||||
// Style elements whose contents will be serialized just before inserting
|
||||
// </style>.
|
||||
HeapHashSet<Member<const HTMLStyleElement>>
|
||||
style_elements_to_replace_contents_;
|
||||
};
|
||||
|
||||
// TODO(tiger): Right now there is no support for rewriting URLs inside CSS
|
||||
|
@ -32,6 +32,7 @@
|
||||
#define THIRD_PARTY_BLINK_RENDERER_CORE_FRAME_FRAME_SERIALIZER_H_
|
||||
|
||||
#include "base/functional/callback.h"
|
||||
#include "base/functional/function_ref.h"
|
||||
#include "third_party/blink/public/web/web_frame_serializer.h"
|
||||
#include "third_party/blink/renderer/core/core_export.h"
|
||||
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
|
||||
@ -46,6 +47,16 @@ class Frame;
|
||||
|
||||
struct SerializedResource;
|
||||
|
||||
// Internal functionality exposed for unit testing.
|
||||
namespace internal {
|
||||
// Returns the result of replacing all case-insensitive occurrences of `from` in
|
||||
// `source` with the result of `transform.Run(match)`.
|
||||
CORE_EXPORT String
|
||||
ReplaceAllCaseInsensitive(String source,
|
||||
const String& from,
|
||||
base::FunctionRef<String(const String&)> transform);
|
||||
} // namespace internal
|
||||
|
||||
// This class is used to serialize frame's contents to MHTML. It serializes
|
||||
// frame's document and resources such as images and CSS stylesheets.
|
||||
class CORE_EXPORT FrameSerializer {
|
||||
|
@ -548,4 +548,26 @@ TEST_F(FrameSerializerTest, markOfTheWebDeclaration) {
|
||||
KURL("http://foo.com#bar--baz")));
|
||||
}
|
||||
|
||||
TEST_F(FrameSerializerTest, ReplaceAllCaseInsensitive) {
|
||||
auto transform = [](const String& from) { return String("</HI>"); };
|
||||
EXPECT_EQ(
|
||||
blink::internal::ReplaceAllCaseInsensitive("", "</style>", transform),
|
||||
"");
|
||||
EXPECT_EQ(
|
||||
blink::internal::ReplaceAllCaseInsensitive("test", "</style>", transform),
|
||||
"test");
|
||||
EXPECT_EQ(blink::internal::ReplaceAllCaseInsensitive("</Style>", "</style>",
|
||||
transform),
|
||||
"</HI>");
|
||||
EXPECT_EQ(blink::internal::ReplaceAllCaseInsensitive("x</Style>", "</style>",
|
||||
transform),
|
||||
"x</HI>");
|
||||
EXPECT_EQ(blink::internal::ReplaceAllCaseInsensitive("</Style>x", "</style>",
|
||||
transform),
|
||||
"</HI>x");
|
||||
EXPECT_EQ(blink::internal::ReplaceAllCaseInsensitive(
|
||||
"test</Style>test</Style>testagain", "</style>", transform),
|
||||
"test</HI>test</HI>testagain");
|
||||
}
|
||||
|
||||
} // namespace blink
|
||||
|
@ -31,10 +31,10 @@ String InnerHtmlBuilder::Build(HTMLElement& body) {
|
||||
return SerializeNodes<EditingStrategy>(body, kIncludeNode);
|
||||
}
|
||||
|
||||
MarkupAccumulator::EmitChoice InnerHtmlBuilder::WillProcessElement(
|
||||
MarkupAccumulator::EmitElementChoice InnerHtmlBuilder::WillProcessElement(
|
||||
const Element& e) {
|
||||
if (e.IsScriptElement()) {
|
||||
return EmitChoice::kIgnore;
|
||||
return EmitElementChoice::kIgnore;
|
||||
}
|
||||
return MarkupAccumulator::WillProcessElement(e);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class MODULES_EXPORT InnerHtmlBuilder final : public MarkupAccumulator {
|
||||
String Build(HTMLElement& body);
|
||||
|
||||
// MarkupAccumulator:
|
||||
EmitChoice WillProcessElement(const Element& e) override;
|
||||
EmitElementChoice WillProcessElement(const Element& e) override;
|
||||
};
|
||||
|
||||
} // namespace blink
|
||||
|
Reference in New Issue
Block a user