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/HTMLTemplateElement.h"
9 #include "mozilla/dom/HTMLInputElement.h"
10 #include "HTMLSplitOnSpacesTokenizer.h"
11 #include "nsHtml5StringParser.h"
12 #include "nsTextNode.h"
14 using namespace mozilla::dom
;
15 using namespace mozilla
;
17 bool L10nOverlays::IsAttrNameLocalizable(
18 const nsAtom
* nameAtom
, Element
* aElement
,
19 nsTArray
<nsString
>* aExplicitlyAllowed
) {
21 nameAtom
->ToString(name
);
23 if (aExplicitlyAllowed
->Contains(name
)) {
27 nsAtom
* elemName
= aElement
->NodeInfo()->NameAtom();
29 uint32_t nameSpace
= aElement
->NodeInfo()->NamespaceID();
31 if (nameSpace
== kNameSpaceID_XHTML
) {
32 // Is it a globally safe attribute?
33 if (nameAtom
== nsGkAtoms::title
|| nameAtom
== nsGkAtoms::aria_label
||
34 nameAtom
== nsGkAtoms::aria_valuetext
) {
38 // Is it allowed on this element?
39 if (elemName
== nsGkAtoms::a
) {
40 return nameAtom
== nsGkAtoms::download
;
42 if (elemName
== nsGkAtoms::area
) {
43 return nameAtom
== nsGkAtoms::download
|| nameAtom
== nsGkAtoms::alt
;
45 if (elemName
== nsGkAtoms::input
) {
46 // Special case for value on HTML inputs with type button, reset, submit
47 if (nameAtom
== nsGkAtoms::value
) {
48 HTMLInputElement
* input
= HTMLInputElement::FromNode(aElement
);
50 uint32_t type
= input
->ControlType();
51 if (type
== NS_FORM_INPUT_SUBMIT
|| type
== NS_FORM_INPUT_BUTTON
||
52 type
== NS_FORM_INPUT_RESET
) {
57 return nameAtom
== nsGkAtoms::alt
|| nameAtom
== nsGkAtoms::placeholder
;
59 if (elemName
== nsGkAtoms::menuitem
) {
60 return nameAtom
== nsGkAtoms::label
;
62 if (elemName
== nsGkAtoms::menu
) {
63 return nameAtom
== nsGkAtoms::label
;
65 if (elemName
== nsGkAtoms::optgroup
) {
66 return nameAtom
== nsGkAtoms::label
;
68 if (elemName
== nsGkAtoms::option
) {
69 return nameAtom
== nsGkAtoms::label
;
71 if (elemName
== nsGkAtoms::track
) {
72 return nameAtom
== nsGkAtoms::label
;
74 if (elemName
== nsGkAtoms::img
) {
75 return nameAtom
== nsGkAtoms::alt
;
77 if (elemName
== nsGkAtoms::textarea
) {
78 return nameAtom
== nsGkAtoms::placeholder
;
80 if (elemName
== nsGkAtoms::th
) {
81 return nameAtom
== nsGkAtoms::abbr
;
84 } else if (nameSpace
== kNameSpaceID_XUL
) {
85 // Is it a globally safe attribute?
86 if (nameAtom
== nsGkAtoms::accesskey
|| nameAtom
== nsGkAtoms::aria_label
||
87 nameAtom
== nsGkAtoms::aria_valuetext
|| nameAtom
== nsGkAtoms::label
||
88 nameAtom
== nsGkAtoms::title
|| nameAtom
== nsGkAtoms::tooltiptext
) {
92 // Is it allowed on this element?
93 if (elemName
== nsGkAtoms::description
) {
94 return nameAtom
== nsGkAtoms::value
;
96 if (elemName
== nsGkAtoms::key
) {
97 return nameAtom
== nsGkAtoms::key
|| nameAtom
== nsGkAtoms::keycode
;
99 if (elemName
== nsGkAtoms::label
) {
100 return nameAtom
== nsGkAtoms::value
;
107 already_AddRefed
<nsINode
> L10nOverlays::CreateTextNodeFromTextContent(
108 Element
* aElement
, ErrorResult
& aRv
) {
109 nsAutoString content
;
110 aElement
->GetTextContent(content
, aRv
);
112 if (NS_WARN_IF(aRv
.Failed())) {
116 return aElement
->OwnerDoc()->CreateTextNode(content
);
119 class AttributeNameValueComparator
{
121 bool Equals(const AttributeNameValue
& aAttribute
,
122 const nsAttrName
* aAttrName
) const {
123 return aAttrName
->Equals(NS_ConvertUTF8toUTF16(aAttribute
.mName
));
127 void L10nOverlays::OverlayAttributes(
128 const Nullable
<Sequence
<AttributeNameValue
>>& aTranslation
,
129 Element
* aToElement
, ErrorResult
& aRv
) {
130 nsTArray
<nsString
> explicitlyAllowed
;
132 nsAutoString l10nAttrs
;
133 aToElement
->GetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nattrs
, l10nAttrs
);
135 HTMLSplitOnSpacesTokenizer
tokenizer(l10nAttrs
, ',');
136 while (tokenizer
.hasMoreTokens()) {
137 const nsAString
& token
= tokenizer
.nextToken();
138 if (!token
.IsEmpty() && !explicitlyAllowed
.Contains(token
)) {
139 explicitlyAllowed
.AppendElement(token
);
143 uint32_t i
= aToElement
->GetAttrCount();
145 const nsAttrName
* attrName
= aToElement
->GetAttrNameAt(i
- 1);
147 if (IsAttrNameLocalizable(attrName
->LocalName(), aToElement
,
148 &explicitlyAllowed
) &&
149 (aTranslation
.IsNull() ||
150 !aTranslation
.Value().Contains(attrName
,
151 AttributeNameValueComparator()))) {
153 attrName
->LocalName()->ToString(name
);
154 aToElement
->RemoveAttribute(name
, aRv
);
155 if (NS_WARN_IF(aRv
.Failed())) {
162 if (aTranslation
.IsNull()) {
166 for (auto& attribute
: aTranslation
.Value()) {
167 RefPtr
<nsAtom
> nameAtom
= NS_Atomize(attribute
.mName
);
168 if (IsAttrNameLocalizable(nameAtom
, aToElement
, &explicitlyAllowed
)) {
169 NS_ConvertUTF8toUTF16
value(attribute
.mValue
);
170 if (!aToElement
->AttrValueIs(kNameSpaceID_None
, nameAtom
, value
,
172 aToElement
->SetAttr(nameAtom
, value
, aRv
);
173 if (NS_WARN_IF(aRv
.Failed())) {
181 void L10nOverlays::OverlayAttributes(Element
* aFromElement
, Element
* aToElement
,
183 Nullable
<Sequence
<AttributeNameValue
>> attributes
;
184 uint32_t attrCount
= aFromElement
->GetAttrCount();
186 if (attrCount
== 0) {
187 attributes
.SetNull();
189 Sequence
<AttributeNameValue
> sequence
;
192 while (BorrowedAttrInfo info
= aFromElement
->GetAttrInfoAt(i
++)) {
193 AttributeNameValue
* attr
= sequence
.AppendElement(fallible
);
194 MOZ_ASSERT(info
.mName
->NamespaceEquals(kNameSpaceID_None
),
195 "No namespaced attributes allowed.");
196 info
.mName
->LocalName()->ToUTF8String(attr
->mName
);
199 info
.mValue
->ToString(value
);
200 attr
->mValue
.Assign(NS_ConvertUTF16toUTF8(value
));
203 attributes
.SetValue(sequence
);
206 return OverlayAttributes(attributes
, aToElement
, aRv
);
209 void L10nOverlays::ShallowPopulateUsing(Element
* aFromElement
,
210 Element
* aToElement
, ErrorResult
& aRv
) {
211 nsAutoString content
;
212 aFromElement
->GetTextContent(content
, aRv
);
213 if (NS_WARN_IF(aRv
.Failed())) {
217 aToElement
->SetTextContent(content
, aRv
);
218 if (NS_WARN_IF(aRv
.Failed())) {
222 OverlayAttributes(aFromElement
, aToElement
, aRv
);
223 if (NS_WARN_IF(aRv
.Failed())) {
228 already_AddRefed
<nsINode
> L10nOverlays::GetNodeForNamedElement(
229 Element
* aSourceElement
, Element
* aTranslatedChild
,
230 nsTArray
<L10nOverlaysError
>& aErrors
, ErrorResult
& aRv
) {
231 nsAutoString childName
;
232 aTranslatedChild
->GetAttr(kNameSpaceID_None
, nsGkAtoms::datal10nname
,
234 RefPtr
<Element
> sourceChild
= nullptr;
236 nsINodeList
* childNodes
= aSourceElement
->ChildNodes();
237 for (uint32_t i
= 0; i
< childNodes
->Length(); i
++) {
238 nsINode
* childNode
= childNodes
->Item(i
);
240 if (!childNode
->IsElement()) {
243 Element
* childElement
= childNode
->AsElement();
245 if (childElement
->AttrValueIs(kNameSpaceID_None
, nsGkAtoms::datal10nname
,
246 childName
, eCaseMatters
)) {
247 sourceChild
= childElement
;
253 L10nOverlaysError error
;
254 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING
);
255 error
.mL10nName
.Construct(childName
);
256 aErrors
.AppendElement(error
);
257 return CreateTextNodeFromTextContent(aTranslatedChild
, aRv
);
260 nsAtom
* sourceChildName
= sourceChild
->NodeInfo()->NameAtom();
261 nsAtom
* translatedChildName
= aTranslatedChild
->NodeInfo()->NameAtom();
262 if (sourceChildName
!= translatedChildName
&&
263 // Create a specific exception for img vs. image mismatches,
265 !(translatedChildName
== nsGkAtoms::img
&&
266 sourceChildName
== nsGkAtoms::image
)) {
267 L10nOverlaysError error
;
268 error
.mCode
.Construct(
269 L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH
);
270 error
.mL10nName
.Construct(childName
);
271 error
.mTranslatedElementName
.Construct(
272 aTranslatedChild
->NodeInfo()->LocalName());
273 error
.mSourceElementName
.Construct(sourceChild
->NodeInfo()->LocalName());
274 aErrors
.AppendElement(error
);
275 return CreateTextNodeFromTextContent(aTranslatedChild
, aRv
);
278 aSourceElement
->RemoveChild(*sourceChild
, aRv
);
279 if (NS_WARN_IF(aRv
.Failed())) {
282 RefPtr
<nsINode
> clone
= sourceChild
->CloneNode(false, aRv
);
283 if (NS_WARN_IF(aRv
.Failed())) {
286 ShallowPopulateUsing(aTranslatedChild
, clone
->AsElement(), aRv
);
287 if (NS_WARN_IF(aRv
.Failed())) {
290 return clone
.forget();
293 bool L10nOverlays::IsElementAllowed(Element
* aElement
) {
294 uint32_t nameSpace
= aElement
->NodeInfo()->NamespaceID();
295 if (nameSpace
!= kNameSpaceID_XHTML
) {
299 nsAtom
* nameAtom
= aElement
->NodeInfo()->NameAtom();
301 return nameAtom
== nsGkAtoms::em
|| nameAtom
== nsGkAtoms::strong
||
302 nameAtom
== nsGkAtoms::small
|| nameAtom
== nsGkAtoms::s
||
303 nameAtom
== nsGkAtoms::cite
|| nameAtom
== nsGkAtoms::q
||
304 nameAtom
== nsGkAtoms::dfn
|| nameAtom
== nsGkAtoms::abbr
||
305 nameAtom
== nsGkAtoms::data
|| nameAtom
== nsGkAtoms::time
||
306 nameAtom
== nsGkAtoms::code
|| nameAtom
== nsGkAtoms::var
||
307 nameAtom
== nsGkAtoms::samp
|| nameAtom
== nsGkAtoms::kbd
||
308 nameAtom
== nsGkAtoms::sub
|| nameAtom
== nsGkAtoms::sup
||
309 nameAtom
== nsGkAtoms::i
|| nameAtom
== nsGkAtoms::b
||
310 nameAtom
== nsGkAtoms::u
|| nameAtom
== nsGkAtoms::mark
||
311 nameAtom
== nsGkAtoms::bdi
|| nameAtom
== nsGkAtoms::bdo
||
312 nameAtom
== nsGkAtoms::span
|| nameAtom
== nsGkAtoms::br
||
313 nameAtom
== nsGkAtoms::wbr
;
316 already_AddRefed
<Element
> L10nOverlays::CreateSanitizedElement(
317 Element
* aElement
, ErrorResult
& aRv
) {
318 // Start with an empty element of the same type to remove nested children
319 // and non-localizable attributes defined by the translation.
321 nsAutoString nameSpaceURI
;
322 aElement
->NodeInfo()->GetNamespaceURI(nameSpaceURI
);
323 ElementCreationOptionsOrString options
;
324 RefPtr
<Element
> clone
= aElement
->OwnerDoc()->CreateElementNS(
325 nameSpaceURI
, aElement
->NodeInfo()->LocalName(), options
, aRv
);
326 if (NS_WARN_IF(aRv
.Failed())) {
330 ShallowPopulateUsing(aElement
, clone
, aRv
);
331 if (NS_WARN_IF(aRv
.Failed())) {
334 return clone
.forget();
337 void L10nOverlays::OverlayChildNodes(DocumentFragment
* aFromFragment
,
339 nsTArray
<L10nOverlaysError
>& aErrors
,
341 nsINodeList
* childNodes
= aFromFragment
->ChildNodes();
342 for (uint32_t i
= 0; i
< childNodes
->Length(); i
++) {
343 nsINode
* childNode
= childNodes
->Item(i
);
345 if (!childNode
->IsElement()) {
346 // Keep the translated text node.
350 RefPtr
<Element
> childElement
= childNode
->AsElement();
352 if (childElement
->HasAttr(kNameSpaceID_None
, nsGkAtoms::datal10nname
)) {
353 RefPtr
<nsINode
> sanitized
=
354 GetNodeForNamedElement(aToElement
, childElement
, aErrors
, aRv
);
355 if (NS_WARN_IF(aRv
.Failed())) {
358 aFromFragment
->ReplaceChild(*sanitized
, *childNode
, aRv
);
359 if (NS_WARN_IF(aRv
.Failed())) {
365 if (IsElementAllowed(childElement
)) {
366 RefPtr
<Element
> sanitized
= CreateSanitizedElement(childElement
, aRv
);
367 if (NS_WARN_IF(aRv
.Failed())) {
370 aFromFragment
->ReplaceChild(*sanitized
, *childNode
, aRv
);
371 if (NS_WARN_IF(aRv
.Failed())) {
377 L10nOverlaysError error
;
378 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE
);
379 error
.mTranslatedElementName
.Construct(
380 childElement
->NodeInfo()->LocalName());
381 aErrors
.AppendElement(error
);
383 // If all else fails, replace the element with its text content.
384 RefPtr
<nsINode
> textNode
= CreateTextNodeFromTextContent(childElement
, aRv
);
385 if (NS_WARN_IF(aRv
.Failed())) {
389 aFromFragment
->ReplaceChild(*textNode
, *childNode
, aRv
);
390 if (NS_WARN_IF(aRv
.Failed())) {
395 while (aToElement
->HasChildren()) {
396 nsIContent
* child
= aToElement
->GetLastChild();
398 if (child
->IsElement()) {
399 if (child
->AsElement()->HasAttr(kNameSpaceID_None
,
400 nsGkAtoms::datal10nid
)) {
401 L10nOverlaysError error
;
402 error
.mCode
.Construct(
403 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED
);
405 child
->AsElement()->GetAttr(nsGkAtoms::datal10nid
, id
);
406 error
.mL10nName
.Construct(id
);
407 error
.mTranslatedElementName
.Construct(
408 aToElement
->NodeInfo()->LocalName());
409 aErrors
.AppendElement(error
);
410 } else if (child
->AsElement()->ChildElementCount() > 0) {
411 L10nOverlaysError error
;
412 error
.mCode
.Construct(
413 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM
);
415 aToElement
->GetAttr(nsGkAtoms::datal10nid
, id
);
416 error
.mL10nName
.Construct(id
);
417 error
.mTranslatedElementName
.Construct(
418 aToElement
->NodeInfo()->LocalName());
419 aErrors
.AppendElement(error
);
423 aToElement
->RemoveChildNode(child
, true);
425 aToElement
->AppendChild(*aFromFragment
, aRv
);
426 if (NS_WARN_IF(aRv
.Failed())) {
431 void L10nOverlays::TranslateElement(
432 const GlobalObject
& aGlobal
, Element
& aElement
,
433 const L10nMessage
& aTranslation
,
434 Nullable
<nsTArray
<L10nOverlaysError
>>& aErrors
) {
435 nsTArray
<L10nOverlaysError
> errors
;
439 TranslateElement(aElement
, aTranslation
, errors
, rv
);
440 if (NS_WARN_IF(rv
.Failed())) {
441 L10nOverlaysError error
;
442 error
.mCode
.Construct(L10nOverlays_Binding::ERROR_UNKNOWN
);
443 errors
.AppendElement(error
);
445 if (!errors
.IsEmpty()) {
446 aErrors
.SetValue(std::move(errors
));
450 bool L10nOverlays::ContainsMarkup(const nsACString
& aStr
) {
451 // We use our custom ContainsMarkup rather than the
452 // one from FragmentOrElement.cpp, because we don't
453 // want to trigger HTML parsing on every `Preferences & Options`
455 const char* start
= aStr
.BeginReading();
456 const char* end
= aStr
.EndReading();
458 while (start
!= end
) {
465 if (c
== '&' && start
!= end
) {
467 if (c
== '#' || (c
>= '0' && c
<= '9') || (c
>= 'a' && c
<= 'z') ||
468 (c
>= 'A' && c
<= 'Z')) {
478 void L10nOverlays::TranslateElement(Element
& aElement
,
479 const L10nMessage
& aTranslation
,
480 nsTArray
<L10nOverlaysError
>& aErrors
,
482 if (!aTranslation
.mValue
.IsVoid()) {
483 NodeInfo
* nodeInfo
= aElement
.NodeInfo();
484 if (nodeInfo
->NameAtom() == nsGkAtoms::title
&&
485 nodeInfo
->NamespaceID() == kNameSpaceID_XHTML
) {
486 // A special case for the HTML title element whose content must be text.
487 aElement
.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation
.mValue
), aRv
);
488 if (NS_WARN_IF(aRv
.Failed())) {
491 } else if (!ContainsMarkup(aTranslation
.mValue
)) {
493 if (aElement
.ChildElementCount() > 0) {
494 L10nOverlaysError error
;
495 error
.mCode
.Construct(
496 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM
);
498 aElement
.GetAttr(nsGkAtoms::datal10nid
, id
);
499 error
.mL10nName
.Construct(id
);
500 error
.mTranslatedElementName
.Construct(
501 aElement
.GetLastElementChild()->NodeInfo()->LocalName());
502 aErrors
.AppendElement(error
);
505 // If the translation doesn't contain any markup skip the overlay logic.
506 aElement
.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation
.mValue
), aRv
);
507 if (NS_WARN_IF(aRv
.Failed())) {
511 // Else parse the translation's HTML into a DocumentFragment,
512 // sanitize it and replace the element's content.
513 RefPtr
<DocumentFragment
> fragment
=
514 new (aElement
.OwnerDoc()->NodeInfoManager())
515 DocumentFragment(aElement
.OwnerDoc()->NodeInfoManager());
516 nsContentUtils::ParseFragmentHTML(
517 NS_ConvertUTF8toUTF16(aTranslation
.mValue
), fragment
,
518 nsGkAtoms::_template
, kNameSpaceID_XHTML
, false, true);
519 if (NS_WARN_IF(aRv
.Failed())) {
523 OverlayChildNodes(fragment
, &aElement
, aErrors
, aRv
);
524 if (NS_WARN_IF(aRv
.Failed())) {
530 // Even if the translation doesn't define any localizable attributes, run
531 // overlayAttributes to remove any localizable attributes set by previous
533 OverlayAttributes(aTranslation
.mAttributes
, &aElement
, aRv
);
534 if (NS_WARN_IF(aRv
.Failed())) {