Bug 1890844 - Tiny cleanup of classes implementing nsDOMCSSDeclaration r=layout-revie...
[gecko.git] / dom / l10n / L10nOverlays.cpp
blob5c3e3b64fad35f3334438269fc5d928a9316581a
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;
20 /**
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
27 * on this element.
29 static bool IsAttrNameLocalizable(
30 const nsAtom* aAttrName, Element* aElement,
31 const nsTArray<nsString>& aExplicitlyAllowed) {
32 if (!aExplicitlyAllowed.IsEmpty()) {
33 nsAutoString name;
34 aAttrName->ToString(name);
35 if (aExplicitlyAllowed.Contains(name)) {
36 return true;
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) {
47 return true;
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);
61 if (input) {
62 auto type = input->ControlType();
63 if (type == FormControlType::InputSubmit ||
64 type == FormControlType::InputButton ||
65 type == FormControlType::InputReset) {
66 return true;
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) {
102 return true;
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;
117 return false;
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())) {
126 return nullptr;
129 return aElement->OwnerDoc()->CreateTextNode(content);
132 class AttributeNameValueComparator {
133 public:
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();
159 while (i > 0) {
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())) {
170 return;
173 i--;
176 if (aTranslation.IsNull()) {
177 return;
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,
185 eCaseMatters)) {
186 aToElement->SetAttr(nameAtom, value, aRv);
187 if (NS_WARN_IF(aRv.Failed())) {
188 return;
195 void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
196 ErrorResult& aRv) {
197 Nullable<Sequence<AttributeNameValue>> attributes;
198 uint32_t attrCount = aFromElement->GetAttrCount();
200 if (attrCount == 0) {
201 attributes.SetNull();
202 } else {
203 Sequence<AttributeNameValue> sequence;
205 uint32_t i = 0;
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);
212 nsAutoString value;
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())) {
228 return;
231 aToElement->SetTextContent(content, aRv);
232 if (NS_WARN_IF(aRv.Failed())) {
233 return;
236 OverlayAttributes(aFromElement, aToElement, aRv);
237 if (NS_WARN_IF(aRv.Failed())) {
238 return;
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()) {
254 continue;
256 Element* childElement = childNode->AsElement();
258 if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname,
259 childName, eCaseMatters)) {
260 sourceChild = childElement;
261 break;
265 if (!sourceChild) {
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,
277 // see bug 1543493
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())) {
293 return nullptr;
295 RefPtr<nsINode> clone = sourceChild->CloneNode(false, aRv);
296 if (NS_WARN_IF(aRv.Failed())) {
297 return nullptr;
299 ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv);
300 if (NS_WARN_IF(aRv.Failed())) {
301 return nullptr;
303 return clone.forget();
306 bool L10nOverlays::IsElementAllowed(Element* aElement) {
307 uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
308 if (nameSpace != kNameSpaceID_XHTML) {
309 return false;
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())) {
340 return nullptr;
343 ShallowPopulateUsing(aElement, clone, aRv);
344 if (NS_WARN_IF(aRv.Failed())) {
345 return nullptr;
347 return clone.forget();
350 void L10nOverlays::OverlayChildNodes(DocumentFragment* aFromFragment,
351 Element* aToElement,
352 nsTArray<L10nOverlaysError>& aErrors,
353 ErrorResult& aRv) {
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.
360 continue;
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())) {
369 return;
371 aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
372 if (NS_WARN_IF(aRv.Failed())) {
373 return;
375 continue;
378 if (IsElementAllowed(childElement)) {
379 RefPtr<Element> sanitized = CreateSanitizedElement(childElement, aRv);
380 if (NS_WARN_IF(aRv.Failed())) {
381 return;
383 aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
384 if (NS_WARN_IF(aRv.Failed())) {
385 return;
387 continue;
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())) {
399 return;
402 aFromFragment->ReplaceChild(*textNode, *childNode, aRv);
403 if (NS_WARN_IF(aRv.Failed())) {
404 return;
408 while (aToElement->HasChildren()) {
409 nsIContent* child = aToElement->GetLastChild();
410 #ifdef DEBUG
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);
416 nsAutoString id;
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);
426 nsAutoString id;
427 aToElement->GetAttr(nsGkAtoms::datal10nid, id);
428 error.mL10nName.Construct(id);
429 error.mTranslatedElementName.Construct(
430 aToElement->NodeInfo()->LocalName());
431 aErrors.AppendElement(error);
434 #endif
435 aToElement->RemoveChildNode(child, true);
437 aToElement->AppendChild(*aFromFragment, aRv);
438 if (NS_WARN_IF(aRv.Failed())) {
439 return;
443 void L10nOverlays::TranslateElement(
444 const GlobalObject& aGlobal, Element& aElement,
445 const L10nMessage& aTranslation,
446 Nullable<nsTArray<L10nOverlaysError>>& aErrors) {
447 nsTArray<L10nOverlaysError> errors;
449 ErrorResult rv;
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`
466 // type of string.
467 const char* start = aStr.BeginReading();
468 const char* end = aStr.EndReading();
470 while (start != end) {
471 char c = *start;
472 if (c == '<') {
473 return true;
475 ++start;
477 if (c == '&' && start != end) {
478 c = *start;
479 if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
480 (c >= 'A' && c <= 'Z')) {
481 return true;
483 ++start;
487 return false;
490 void L10nOverlays::TranslateElement(Element& aElement,
491 const L10nMessage& aTranslation,
492 nsTArray<L10nOverlaysError>& aErrors,
493 ErrorResult& aRv) {
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())) {
501 return;
503 } else if (!ContainsMarkup(aTranslation.mValue)) {
504 #ifdef DEBUG
505 if (aElement.ChildElementCount() > 0) {
506 L10nOverlaysError error;
507 error.mCode.Construct(
508 L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
509 nsAutoString id;
510 aElement.GetAttr(nsGkAtoms::datal10nid, id);
511 error.mL10nName.Construct(id);
512 error.mTranslatedElementName.Construct(
513 aElement.GetLastElementChild()->NodeInfo()->LocalName());
514 aErrors.AppendElement(error);
516 #endif
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())) {
520 return;
522 } else {
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,
537 sanitizationFlags);
538 if (NS_WARN_IF(aRv.Failed())) {
539 return;
542 OverlayChildNodes(fragment, &aElement, aErrors, aRv);
543 if (NS_WARN_IF(aRv.Failed())) {
544 return;
549 // Even if the translation doesn't define any localizable attributes, run
550 // overlayAttributes to remove any localizable attributes set by previous
551 // translations.
552 OverlayAttributes(aTranslation.mAttributes, &aElement, aRv);
553 if (NS_WARN_IF(aRv.Failed())) {
554 return;