1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "L10nOverlays.h"
8 #include "mozilla/dom/Document.h"
9 #include "mozilla/dom/DocumentFragment.h"
10 #include "mozilla/dom/HTMLInputElement.h"
11 #include "HTMLSplitOnSpacesTokenizer.h"
12 #include "nsHtml5StringParser.h"
13 #include "nsTextNode.h"
14 #include "nsIParserUtils.h"
15 #include "nsINodeList.h"
17 using namespace mozilla::dom
;
18 using namespace mozilla
;
21 * Check if attribute is allowed for the given element.
23 * This method is used by the sanitizer when the translation markup contains DOM
24 * attributes, or when the translation has traits which map to DOM attributes.
26 * `aExplicitlyAllowed` can be passed as a list of attributes explicitly allowed
29 static bool IsAttrNameLocalizable(
30 const nsAtom
* aAttrName
, Element
* aElement
,
31 const nsTArray
<nsString
>& aExplicitlyAllowed
) {
32 if (!aExplicitlyAllowed
.IsEmpty()) {
34 aAttrName
->ToString(name
);
35 if (aExplicitlyAllowed
.Contains(name
)) {
40 nsAtom
* elemName
= aElement
->NodeInfo()->NameAtom();
41 uint32_t nameSpace
= aElement
->NodeInfo()->NamespaceID();
43 if (nameSpace
== kNameSpaceID_XHTML
) {
44 // Is it a globally safe attribute?
45 if (aAttrName
== nsGkAtoms::title
|| aAttrName
== nsGkAtoms::aria_label
||
46 aAttrName
== nsGkAtoms::aria_description
) {
50 // Is it allowed on this element?
51 if (elemName
== nsGkAtoms::a
) {
52 return aAttrName
== nsGkAtoms::download
;
54 if (elemName
== nsGkAtoms::area
) {
55 return aAttrName
== nsGkAtoms::download
|| aAttrName
== nsGkAtoms::alt
;
57 if (elemName
== nsGkAtoms::input
) {
58 // Special case for value on HTML inputs with type button, reset, submit
59 if (aAttrName
== nsGkAtoms::value
) {
60 HTMLInputElement
* input
= HTMLInputElement::FromNode(aElement
);
62 auto type
= input
->ControlType();
63 if (type
== FormControlType::InputSubmit
||
64 type
== FormControlType::InputButton
||
65 type
== FormControlType::InputReset
) {
70 return aAttrName
== nsGkAtoms::alt
|| aAttrName
== nsGkAtoms::placeholder
;
72 if (elemName
== nsGkAtoms::menuitem
) {
73 return aAttrName
== nsGkAtoms::label
;
75 if (elemName
== nsGkAtoms::menu
) {
76 return aAttrName
== nsGkAtoms::label
;
78 if (elemName
== nsGkAtoms::optgroup
) {
79 return aAttrName
== nsGkAtoms::label
;
81 if (elemName
== nsGkAtoms::option
) {
82 return aAttrName
== nsGkAtoms::label
;
84 if (elemName
== nsGkAtoms::track
) {
85 return aAttrName
== nsGkAtoms::label
;
87 if (elemName
== nsGkAtoms::img
) {
88 return aAttrName
== nsGkAtoms::alt
;
90 if (elemName
== nsGkAtoms::textarea
) {
91 return aAttrName
== nsGkAtoms::placeholder
;
93 if (elemName
== nsGkAtoms::th
) {
94 return aAttrName
== nsGkAtoms::abbr
;
97 } else if (nameSpace
== kNameSpaceID_XUL
) {
98 // Is it a globally safe attribute?
99 if (aAttrName
== nsGkAtoms::accesskey
||
100 aAttrName
== nsGkAtoms::aria_label
|| aAttrName
== nsGkAtoms::label
||
101 aAttrName
== nsGkAtoms::title
|| aAttrName
== nsGkAtoms::tooltiptext
) {
105 // Is it allowed on this element?
106 if (elemName
== nsGkAtoms::description
) {
107 return aAttrName
== nsGkAtoms::value
;
109 if (elemName
== nsGkAtoms::key
) {
110 return aAttrName
== nsGkAtoms::key
|| aAttrName
== nsGkAtoms::keycode
;
112 if (elemName
== nsGkAtoms::label
) {
113 return aAttrName
== nsGkAtoms::value
;
120 already_AddRefed
<nsINode
> L10nOverlays::CreateTextNodeFromTextContent(
121 Element
* aElement
, ErrorResult
& aRv
) {
122 nsAutoString content
;
123 aElement
->GetTextContent(content
, aRv
);
125 if (NS_WARN_IF(aRv
.Failed())) {
129 return aElement
->OwnerDoc()->CreateTextNode(content
);
132 class AttributeNameValueComparator
{
134 bool Equals(const AttributeNameValue
& aAttribute
,
135 const nsAttrName
* aAttrName
) const {
136 return aAttrName
->Equals(NS_ConvertUTF8toUTF16(aAttribute
.mName
));
140 void L10nOverlays::OverlayAttributes(
141 const Nullable
<Sequence
<AttributeNameValue
>>& aTranslation
,
142 Element
* aToElement
, ErrorResult
& aRv
) {
143 nsTArray
<nsString
> explicitlyAllowed
;
146 nsAutoString l10nAttrs
;
147 if (aToElement
->GetAttr(nsGkAtoms::datal10nattrs
, l10nAttrs
)) {
148 HTMLSplitOnSpacesTokenizer
tokenizer(l10nAttrs
, ',');
149 while (tokenizer
.hasMoreTokens()) {
150 const nsAString
& token
= tokenizer
.nextToken();
151 if (!token
.IsEmpty() && !explicitlyAllowed
.Contains(token
)) {
152 explicitlyAllowed
.AppendElement(token
);
158 uint32_t i
= aToElement
->GetAttrCount();
160 const nsAttrName
* attrName
= aToElement
->GetAttrNameAt(i
- 1);
162 if (IsAttrNameLocalizable(attrName
->LocalName(), aToElement
,
163 explicitlyAllowed
) &&
164 (aTranslation
.IsNull() ||
165 !aTranslation
.Value().Contains(attrName
,
166 AttributeNameValueComparator()))) {
167 RefPtr
<nsAtom
> localName
= attrName
->LocalName();
168 aToElement
->UnsetAttr(localName
, aRv
);
169 if (NS_WARN_IF(aRv
.Failed())) {
176 if (aTranslation
.IsNull()) {
180 for (auto& attribute
: aTranslation
.Value()) {
181 RefPtr
<nsAtom
> nameAtom
= NS_Atomize(attribute
.mName
);
182 if (IsAttrNameLocalizable(nameAtom
, aToElement
, explicitlyAllowed
)) {
183 NS_ConvertUTF8toUTF16
value(attribute
.mValue
);
184 if (!aToElement
->AttrValueIs(kNameSpaceID_None
, nameAtom
, value
,
186 aToElement
->SetAttr(nameAtom
, value
, aRv
);
187 if (NS_WARN_IF(aRv
.Failed())) {
195 void L10nOverlays::OverlayAttributes(Element
* aFromElement
, Element
* aToElement
,
197 Nullable
<Sequence
<AttributeNameValue
>> attributes
;
198 uint32_t attrCount
= aFromElement
->GetAttrCount();
200 if (attrCount
== 0) {
201 attributes
.SetNull();
203 Sequence
<AttributeNameValue
> sequence
;
206 while (BorrowedAttrInfo info
= aFromElement
->GetAttrInfoAt(i
++)) {
207 AttributeNameValue
* attr
= sequence
.AppendElement(fallible
);
208 MOZ_ASSERT(info
.mName
->NamespaceEquals(kNameSpaceID_None
),
209 "No namespaced attributes allowed.");
210 info
.mName
->LocalName()->ToUTF8String(attr
->mName
);
213 info
.mValue
->ToString(value
);
214 attr
->mValue
.Assign(NS_ConvertUTF16toUTF8(value
));
217 attributes
.SetValue(sequence
);
220 return OverlayAttributes(attributes
, aToElement
, aRv
);
223 void L10nOverlays::ShallowPopulateUsing(Element
* aFromElement
,
224 Element
* aToElement
, ErrorResult
& aRv
) {
225 nsAutoString content
;
226 aFromElement
->GetTextContent(content
, aRv
);
227 if (NS_WARN_IF(aRv
.Failed())) {
231 aToElement
->SetTextContent(content
, aRv
);
232 if (NS_WARN_IF(aRv
.Failed())) {
236 OverlayAttributes(aFromElement
, aToElement
, aRv
);
237 if (NS_WARN_IF(aRv
.Failed())) {
242 already_AddRefed
<nsINode
> L10nOverlays::GetNodeForNamedElement(
243 Element
* aSourceElement
, Element
* aTranslatedChild
,
244 nsTArray
<L10nOverlaysError
>& aErrors
, ErrorResult
& aRv
) {
245 nsAutoString childName
;
246 aTranslatedChild
->GetAttr(nsGkAtoms::datal10nname
, childName
);
247 RefPtr
<Element
> sourceChild
= nullptr;
249 nsINodeList
* childNodes
= aSourceElement
->ChildNodes();
250 for (uint32_t i
= 0; i
< childNodes
->Length(); i
++) {
251 nsINode
* childNode
= childNodes
->Item(i
);
253 if (!childNode
->IsElement()) {
256 Element
* childElement
= childNode
->AsElement();
258 if (childElement
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::datal10nname
,
259 childName
, eCaseMatters
)) {
260 sourceChild
= childElement
;
266 L10nOverlaysError error
;
267 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING
);
268 error
.mL10nName
.Construct(childName
);
269 aErrors
.AppendElement(error
);
270 return CreateTextNodeFromTextContent(aTranslatedChild
, aRv
);
273 nsAtom
* sourceChildName
= sourceChild
->NodeInfo()->NameAtom();
274 nsAtom
* translatedChildName
= aTranslatedChild
->NodeInfo()->NameAtom();
275 if (sourceChildName
!= translatedChildName
&&
276 // Create a specific exception for img vs. image mismatches,
278 !(translatedChildName
== nsGkAtoms::img
&&
279 sourceChildName
== nsGkAtoms::image
)) {
280 L10nOverlaysError error
;
281 error
.mCode
.Construct(
282 L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH
);
283 error
.mL10nName
.Construct(childName
);
284 error
.mTranslatedElementName
.Construct(
285 aTranslatedChild
->NodeInfo()->LocalName());
286 error
.mSourceElementName
.Construct(sourceChild
->NodeInfo()->LocalName());
287 aErrors
.AppendElement(error
);
288 return CreateTextNodeFromTextContent(aTranslatedChild
, aRv
);
291 aSourceElement
->RemoveChild(*sourceChild
, aRv
);
292 if (NS_WARN_IF(aRv
.Failed())) {
295 RefPtr
<nsINode
> clone
= sourceChild
->CloneNode(false, aRv
);
296 if (NS_WARN_IF(aRv
.Failed())) {
299 ShallowPopulateUsing(aTranslatedChild
, clone
->AsElement(), aRv
);
300 if (NS_WARN_IF(aRv
.Failed())) {
303 return clone
.forget();
306 bool L10nOverlays::IsElementAllowed(Element
* aElement
) {
307 uint32_t nameSpace
= aElement
->NodeInfo()->NamespaceID();
308 if (nameSpace
!= kNameSpaceID_XHTML
) {
312 nsAtom
* nameAtom
= aElement
->NodeInfo()->NameAtom();
314 return nameAtom
== nsGkAtoms::em
|| nameAtom
== nsGkAtoms::strong
||
315 nameAtom
== nsGkAtoms::small
|| nameAtom
== nsGkAtoms::s
||
316 nameAtom
== nsGkAtoms::cite
|| nameAtom
== nsGkAtoms::q
||
317 nameAtom
== nsGkAtoms::dfn
|| nameAtom
== nsGkAtoms::abbr
||
318 nameAtom
== nsGkAtoms::data
|| nameAtom
== nsGkAtoms::time
||
319 nameAtom
== nsGkAtoms::code
|| nameAtom
== nsGkAtoms::var
||
320 nameAtom
== nsGkAtoms::samp
|| nameAtom
== nsGkAtoms::kbd
||
321 nameAtom
== nsGkAtoms::sub
|| nameAtom
== nsGkAtoms::sup
||
322 nameAtom
== nsGkAtoms::i
|| nameAtom
== nsGkAtoms::b
||
323 nameAtom
== nsGkAtoms::u
|| nameAtom
== nsGkAtoms::mark
||
324 nameAtom
== nsGkAtoms::bdi
|| nameAtom
== nsGkAtoms::bdo
||
325 nameAtom
== nsGkAtoms::span
|| nameAtom
== nsGkAtoms::br
||
326 nameAtom
== nsGkAtoms::wbr
;
329 already_AddRefed
<Element
> L10nOverlays::CreateSanitizedElement(
330 Element
* aElement
, ErrorResult
& aRv
) {
331 // Start with an empty element of the same type to remove nested children
332 // and non-localizable attributes defined by the translation.
334 nsAutoString nameSpaceURI
;
335 aElement
->NodeInfo()->GetNamespaceURI(nameSpaceURI
);
336 ElementCreationOptionsOrString options
;
337 RefPtr
<Element
> clone
= aElement
->OwnerDoc()->CreateElementNS(
338 nameSpaceURI
, aElement
->NodeInfo()->LocalName(), options
, aRv
);
339 if (NS_WARN_IF(aRv
.Failed())) {
343 ShallowPopulateUsing(aElement
, clone
, aRv
);
344 if (NS_WARN_IF(aRv
.Failed())) {
347 return clone
.forget();
350 void L10nOverlays::OverlayChildNodes(DocumentFragment
* aFromFragment
,
352 nsTArray
<L10nOverlaysError
>& aErrors
,
354 nsINodeList
* childNodes
= aFromFragment
->ChildNodes();
355 for (uint32_t i
= 0; i
< childNodes
->Length(); i
++) {
356 nsINode
* childNode
= childNodes
->Item(i
);
358 if (!childNode
->IsElement()) {
359 // Keep the translated text node.
363 RefPtr
<Element
> childElement
= childNode
->AsElement();
365 if (childElement
->HasAttr(nsGkAtoms::datal10nname
)) {
366 RefPtr
<nsINode
> sanitized
=
367 GetNodeForNamedElement(aToElement
, childElement
, aErrors
, aRv
);
368 if (NS_WARN_IF(aRv
.Failed())) {
371 aFromFragment
->ReplaceChild(*sanitized
, *childNode
, aRv
);
372 if (NS_WARN_IF(aRv
.Failed())) {
378 if (IsElementAllowed(childElement
)) {
379 RefPtr
<Element
> sanitized
= CreateSanitizedElement(childElement
, aRv
);
380 if (NS_WARN_IF(aRv
.Failed())) {
383 aFromFragment
->ReplaceChild(*sanitized
, *childNode
, aRv
);
384 if (NS_WARN_IF(aRv
.Failed())) {
390 L10nOverlaysError error
;
391 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE
);
392 error
.mTranslatedElementName
.Construct(
393 childElement
->NodeInfo()->LocalName());
394 aErrors
.AppendElement(error
);
396 // If all else fails, replace the element with its text content.
397 RefPtr
<nsINode
> textNode
= CreateTextNodeFromTextContent(childElement
, aRv
);
398 if (NS_WARN_IF(aRv
.Failed())) {
402 aFromFragment
->ReplaceChild(*textNode
, *childNode
, aRv
);
403 if (NS_WARN_IF(aRv
.Failed())) {
408 while (aToElement
->HasChildren()) {
409 nsIContent
* child
= aToElement
->GetLastChild();
411 if (child
->IsElement()) {
412 if (child
->AsElement()->HasAttr(nsGkAtoms::datal10nid
)) {
413 L10nOverlaysError error
;
414 error
.mCode
.Construct(
415 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED
);
417 child
->AsElement()->GetAttr(nsGkAtoms::datal10nid
, id
);
418 error
.mL10nName
.Construct(id
);
419 error
.mTranslatedElementName
.Construct(
420 aToElement
->NodeInfo()->LocalName());
421 aErrors
.AppendElement(error
);
422 } else if (child
->AsElement()->ChildElementCount() > 0) {
423 L10nOverlaysError error
;
424 error
.mCode
.Construct(
425 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM
);
427 aToElement
->GetAttr(nsGkAtoms::datal10nid
, id
);
428 error
.mL10nName
.Construct(id
);
429 error
.mTranslatedElementName
.Construct(
430 aToElement
->NodeInfo()->LocalName());
431 aErrors
.AppendElement(error
);
435 aToElement
->RemoveChildNode(child
, true);
437 aToElement
->AppendChild(*aFromFragment
, aRv
);
438 if (NS_WARN_IF(aRv
.Failed())) {
443 void L10nOverlays::TranslateElement(
444 const GlobalObject
& aGlobal
, Element
& aElement
,
445 const L10nMessage
& aTranslation
,
446 Nullable
<nsTArray
<L10nOverlaysError
>>& aErrors
) {
447 nsTArray
<L10nOverlaysError
> errors
;
451 TranslateElement(aElement
, aTranslation
, errors
, rv
);
452 if (NS_WARN_IF(rv
.Failed())) {
453 L10nOverlaysError error
;
454 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_UNKNOWN
);
455 errors
.AppendElement(error
);
457 if (!errors
.IsEmpty()) {
458 aErrors
.SetValue(std::move(errors
));
462 bool L10nOverlays::ContainsMarkup(const nsACString
& aStr
) {
463 // We use our custom ContainsMarkup rather than the
464 // one from FragmentOrElement.cpp, because we don't
465 // want to trigger HTML parsing on every `Preferences & Options`
467 const char* start
= aStr
.BeginReading();
468 const char* end
= aStr
.EndReading();
470 while (start
!= end
) {
477 if (c
== '&' && start
!= end
) {
479 if (c
== '#' || (c
>= '0' && c
<= '9') || (c
>= 'a' && c
<= 'z') ||
480 (c
>= 'A' && c
<= 'Z')) {
490 void L10nOverlays::TranslateElement(Element
& aElement
,
491 const L10nMessage
& aTranslation
,
492 nsTArray
<L10nOverlaysError
>& aErrors
,
494 if (!aTranslation
.mValue
.IsVoid()) {
495 NodeInfo
* nodeInfo
= aElement
.NodeInfo();
496 if (nodeInfo
->NameAtom() == nsGkAtoms::title
&&
497 nodeInfo
->NamespaceID() == kNameSpaceID_XHTML
) {
498 // A special case for the HTML title element whose content must be text.
499 aElement
.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation
.mValue
), aRv
);
500 if (NS_WARN_IF(aRv
.Failed())) {
503 } else if (!ContainsMarkup(aTranslation
.mValue
)) {
505 if (aElement
.ChildElementCount() > 0) {
506 L10nOverlaysError error
;
507 error
.mCode
.Construct(
508 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM
);
510 aElement
.GetAttr(nsGkAtoms::datal10nid
, id
);
511 error
.mL10nName
.Construct(id
);
512 error
.mTranslatedElementName
.Construct(
513 aElement
.GetLastElementChild()->NodeInfo()->LocalName());
514 aErrors
.AppendElement(error
);
517 // If the translation doesn't contain any markup skip the overlay logic.
518 aElement
.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation
.mValue
), aRv
);
519 if (NS_WARN_IF(aRv
.Failed())) {
523 // Else parse the translation's HTML into a DocumentFragment,
524 // sanitize it and replace the element's content.
525 RefPtr
<DocumentFragment
> fragment
=
526 new (aElement
.OwnerDoc()->NodeInfoManager())
527 DocumentFragment(aElement
.OwnerDoc()->NodeInfoManager());
528 // Note: these flags should be no less restrictive than the ones in
529 // nsContentUtils::ParseFragmentHTML .
530 // We supply the flags here because otherwise the parsing of HTML can
531 // trip DEBUG-only crashes, see bug 1809902 for details.
532 auto sanitizationFlags
= nsIParserUtils::SanitizerDropForms
|
533 nsIParserUtils::SanitizerLogRemovals
;
534 nsContentUtils::ParseFragmentHTML(
535 NS_ConvertUTF8toUTF16(aTranslation
.mValue
), fragment
,
536 nsGkAtoms::_template
, kNameSpaceID_XHTML
, false, true,
538 if (NS_WARN_IF(aRv
.Failed())) {
542 OverlayChildNodes(fragment
, &aElement
, aErrors
, aRv
);
543 if (NS_WARN_IF(aRv
.Failed())) {
549 // Even if the translation doesn't define any localizable attributes, run
550 // overlayAttributes to remove any localizable attributes set by previous
552 OverlayAttributes(aTranslation
.mAttributes
, &aElement
, aRv
);
553 if (NS_WARN_IF(aRv
.Failed())) {