summaryrefslogtreecommitdiffstats
path: root/dom/l10n/L10nOverlays.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/l10n/L10nOverlays.cpp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/l10n/L10nOverlays.cpp556
1 files changed, 556 insertions, 0 deletions
diff --git a/dom/l10n/L10nOverlays.cpp b/dom/l10n/L10nOverlays.cpp
new file mode 100644
index 0000000000..5c3e3b64fa
--- /dev/null
+++ b/dom/l10n/L10nOverlays.cpp
@@ -0,0 +1,556 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "L10nOverlays.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/HTMLInputElement.h"
+#include "HTMLSplitOnSpacesTokenizer.h"
+#include "nsHtml5StringParser.h"
+#include "nsTextNode.h"
+#include "nsIParserUtils.h"
+#include "nsINodeList.h"
+
+using namespace mozilla::dom;
+using namespace mozilla;
+
+/**
+ * Check if attribute is allowed for the given element.
+ *
+ * This method is used by the sanitizer when the translation markup contains DOM
+ * attributes, or when the translation has traits which map to DOM attributes.
+ *
+ * `aExplicitlyAllowed` can be passed as a list of attributes explicitly allowed
+ * on this element.
+ */
+static bool IsAttrNameLocalizable(
+ const nsAtom* aAttrName, Element* aElement,
+ const nsTArray<nsString>& aExplicitlyAllowed) {
+ if (!aExplicitlyAllowed.IsEmpty()) {
+ nsAutoString name;
+ aAttrName->ToString(name);
+ if (aExplicitlyAllowed.Contains(name)) {
+ return true;
+ }
+ }
+
+ nsAtom* elemName = aElement->NodeInfo()->NameAtom();
+ uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
+
+ if (nameSpace == kNameSpaceID_XHTML) {
+ // Is it a globally safe attribute?
+ if (aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::aria_label ||
+ aAttrName == nsGkAtoms::aria_description) {
+ return true;
+ }
+
+ // Is it allowed on this element?
+ if (elemName == nsGkAtoms::a) {
+ return aAttrName == nsGkAtoms::download;
+ }
+ if (elemName == nsGkAtoms::area) {
+ return aAttrName == nsGkAtoms::download || aAttrName == nsGkAtoms::alt;
+ }
+ if (elemName == nsGkAtoms::input) {
+ // Special case for value on HTML inputs with type button, reset, submit
+ if (aAttrName == nsGkAtoms::value) {
+ HTMLInputElement* input = HTMLInputElement::FromNode(aElement);
+ if (input) {
+ auto type = input->ControlType();
+ if (type == FormControlType::InputSubmit ||
+ type == FormControlType::InputButton ||
+ type == FormControlType::InputReset) {
+ return true;
+ }
+ }
+ }
+ return aAttrName == nsGkAtoms::alt || aAttrName == nsGkAtoms::placeholder;
+ }
+ if (elemName == nsGkAtoms::menuitem) {
+ return aAttrName == nsGkAtoms::label;
+ }
+ if (elemName == nsGkAtoms::menu) {
+ return aAttrName == nsGkAtoms::label;
+ }
+ if (elemName == nsGkAtoms::optgroup) {
+ return aAttrName == nsGkAtoms::label;
+ }
+ if (elemName == nsGkAtoms::option) {
+ return aAttrName == nsGkAtoms::label;
+ }
+ if (elemName == nsGkAtoms::track) {
+ return aAttrName == nsGkAtoms::label;
+ }
+ if (elemName == nsGkAtoms::img) {
+ return aAttrName == nsGkAtoms::alt;
+ }
+ if (elemName == nsGkAtoms::textarea) {
+ return aAttrName == nsGkAtoms::placeholder;
+ }
+ if (elemName == nsGkAtoms::th) {
+ return aAttrName == nsGkAtoms::abbr;
+ }
+
+ } else if (nameSpace == kNameSpaceID_XUL) {
+ // Is it a globally safe attribute?
+ if (aAttrName == nsGkAtoms::accesskey ||
+ aAttrName == nsGkAtoms::aria_label || aAttrName == nsGkAtoms::label ||
+ aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::tooltiptext) {
+ return true;
+ }
+
+ // Is it allowed on this element?
+ if (elemName == nsGkAtoms::description) {
+ return aAttrName == nsGkAtoms::value;
+ }
+ if (elemName == nsGkAtoms::key) {
+ return aAttrName == nsGkAtoms::key || aAttrName == nsGkAtoms::keycode;
+ }
+ if (elemName == nsGkAtoms::label) {
+ return aAttrName == nsGkAtoms::value;
+ }
+ }
+
+ return false;
+}
+
+already_AddRefed<nsINode> L10nOverlays::CreateTextNodeFromTextContent(
+ Element* aElement, ErrorResult& aRv) {
+ nsAutoString content;
+ aElement->GetTextContent(content, aRv);
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return aElement->OwnerDoc()->CreateTextNode(content);
+}
+
+class AttributeNameValueComparator {
+ public:
+ bool Equals(const AttributeNameValue& aAttribute,
+ const nsAttrName* aAttrName) const {
+ return aAttrName->Equals(NS_ConvertUTF8toUTF16(aAttribute.mName));
+ }
+};
+
+void L10nOverlays::OverlayAttributes(
+ const Nullable<Sequence<AttributeNameValue>>& aTranslation,
+ Element* aToElement, ErrorResult& aRv) {
+ nsTArray<nsString> explicitlyAllowed;
+
+ {
+ nsAutoString l10nAttrs;
+ if (aToElement->GetAttr(nsGkAtoms::datal10nattrs, l10nAttrs)) {
+ HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ',');
+ while (tokenizer.hasMoreTokens()) {
+ const nsAString& token = tokenizer.nextToken();
+ if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) {
+ explicitlyAllowed.AppendElement(token);
+ }
+ }
+ }
+ }
+
+ uint32_t i = aToElement->GetAttrCount();
+ while (i > 0) {
+ const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1);
+
+ if (IsAttrNameLocalizable(attrName->LocalName(), aToElement,
+ explicitlyAllowed) &&
+ (aTranslation.IsNull() ||
+ !aTranslation.Value().Contains(attrName,
+ AttributeNameValueComparator()))) {
+ RefPtr<nsAtom> localName = attrName->LocalName();
+ aToElement->UnsetAttr(localName, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+ i--;
+ }
+
+ if (aTranslation.IsNull()) {
+ return;
+ }
+
+ for (auto& attribute : aTranslation.Value()) {
+ RefPtr<nsAtom> nameAtom = NS_Atomize(attribute.mName);
+ if (IsAttrNameLocalizable(nameAtom, aToElement, explicitlyAllowed)) {
+ NS_ConvertUTF8toUTF16 value(attribute.mValue);
+ if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value,
+ eCaseMatters)) {
+ aToElement->SetAttr(nameAtom, value, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+ }
+ }
+}
+
+void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
+ ErrorResult& aRv) {
+ Nullable<Sequence<AttributeNameValue>> attributes;
+ uint32_t attrCount = aFromElement->GetAttrCount();
+
+ if (attrCount == 0) {
+ attributes.SetNull();
+ } else {
+ Sequence<AttributeNameValue> sequence;
+
+ uint32_t i = 0;
+ while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) {
+ AttributeNameValue* attr = sequence.AppendElement(fallible);
+ MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None),
+ "No namespaced attributes allowed.");
+ info.mName->LocalName()->ToUTF8String(attr->mName);
+
+ nsAutoString value;
+ info.mValue->ToString(value);
+ attr->mValue.Assign(NS_ConvertUTF16toUTF8(value));
+ }
+
+ attributes.SetValue(sequence);
+ }
+
+ return OverlayAttributes(attributes, aToElement, aRv);
+}
+
+void L10nOverlays::ShallowPopulateUsing(Element* aFromElement,
+ Element* aToElement, ErrorResult& aRv) {
+ nsAutoString content;
+ aFromElement->GetTextContent(content, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aToElement->SetTextContent(content, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ OverlayAttributes(aFromElement, aToElement, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+}
+
+already_AddRefed<nsINode> L10nOverlays::GetNodeForNamedElement(
+ Element* aSourceElement, Element* aTranslatedChild,
+ nsTArray<L10nOverlaysError>& aErrors, ErrorResult& aRv) {
+ nsAutoString childName;
+ aTranslatedChild->GetAttr(nsGkAtoms::datal10nname, childName);
+ RefPtr<Element> sourceChild = nullptr;
+
+ nsINodeList* childNodes = aSourceElement->ChildNodes();
+ for (uint32_t i = 0; i < childNodes->Length(); i++) {
+ nsINode* childNode = childNodes->Item(i);
+
+ if (!childNode->IsElement()) {
+ continue;
+ }
+ Element* childElement = childNode->AsElement();
+
+ if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname,
+ childName, eCaseMatters)) {
+ sourceChild = childElement;
+ break;
+ }
+ }
+
+ if (!sourceChild) {
+ L10nOverlaysError error;
+ error.mCode.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING);
+ error.mL10nName.Construct(childName);
+ aErrors.AppendElement(error);
+ return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
+ }
+
+ nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom();
+ nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom();
+ if (sourceChildName != translatedChildName &&
+ // Create a specific exception for img vs. image mismatches,
+ // see bug 1543493
+ !(translatedChildName == nsGkAtoms::img &&
+ sourceChildName == nsGkAtoms::image)) {
+ L10nOverlaysError error;
+ error.mCode.Construct(
+ L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH);
+ error.mL10nName.Construct(childName);
+ error.mTranslatedElementName.Construct(
+ aTranslatedChild->NodeInfo()->LocalName());
+ error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName());
+ aErrors.AppendElement(error);
+ return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
+ }
+
+ aSourceElement->RemoveChild(*sourceChild, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ RefPtr<nsINode> clone = sourceChild->CloneNode(false, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ return clone.forget();
+}
+
+bool L10nOverlays::IsElementAllowed(Element* aElement) {
+ uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
+ if (nameSpace != kNameSpaceID_XHTML) {
+ return false;
+ }
+
+ nsAtom* nameAtom = aElement->NodeInfo()->NameAtom();
+
+ return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong ||
+ nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s ||
+ nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q ||
+ nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr ||
+ nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time ||
+ nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var ||
+ nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd ||
+ nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup ||
+ nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b ||
+ nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark ||
+ nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo ||
+ nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br ||
+ nameAtom == nsGkAtoms::wbr;
+}
+
+already_AddRefed<Element> L10nOverlays::CreateSanitizedElement(
+ Element* aElement, ErrorResult& aRv) {
+ // Start with an empty element of the same type to remove nested children
+ // and non-localizable attributes defined by the translation.
+
+ nsAutoString nameSpaceURI;
+ aElement->NodeInfo()->GetNamespaceURI(nameSpaceURI);
+ ElementCreationOptionsOrString options;
+ RefPtr<Element> clone = aElement->OwnerDoc()->CreateElementNS(
+ nameSpaceURI, aElement->NodeInfo()->LocalName(), options, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ ShallowPopulateUsing(aElement, clone, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ return clone.forget();
+}
+
+void L10nOverlays::OverlayChildNodes(DocumentFragment* aFromFragment,
+ Element* aToElement,
+ nsTArray<L10nOverlaysError>& aErrors,
+ ErrorResult& aRv) {
+ nsINodeList* childNodes = aFromFragment->ChildNodes();
+ for (uint32_t i = 0; i < childNodes->Length(); i++) {
+ nsINode* childNode = childNodes->Item(i);
+
+ if (!childNode->IsElement()) {
+ // Keep the translated text node.
+ continue;
+ }
+
+ RefPtr<Element> childElement = childNode->AsElement();
+
+ if (childElement->HasAttr(nsGkAtoms::datal10nname)) {
+ RefPtr<nsINode> sanitized =
+ GetNodeForNamedElement(aToElement, childElement, aErrors, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ continue;
+ }
+
+ if (IsElementAllowed(childElement)) {
+ RefPtr<Element> sanitized = CreateSanitizedElement(childElement, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ continue;
+ }
+
+ L10nOverlaysError error;
+ error.mCode.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE);
+ error.mTranslatedElementName.Construct(
+ childElement->NodeInfo()->LocalName());
+ aErrors.AppendElement(error);
+
+ // If all else fails, replace the element with its text content.
+ RefPtr<nsINode> textNode = CreateTextNodeFromTextContent(childElement, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ aFromFragment->ReplaceChild(*textNode, *childNode, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ while (aToElement->HasChildren()) {
+ nsIContent* child = aToElement->GetLastChild();
+#ifdef DEBUG
+ if (child->IsElement()) {
+ if (child->AsElement()->HasAttr(nsGkAtoms::datal10nid)) {
+ L10nOverlaysError error;
+ error.mCode.Construct(
+ L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED);
+ nsAutoString id;
+ child->AsElement()->GetAttr(nsGkAtoms::datal10nid, id);
+ error.mL10nName.Construct(id);
+ error.mTranslatedElementName.Construct(
+ aToElement->NodeInfo()->LocalName());
+ aErrors.AppendElement(error);
+ } else if (child->AsElement()->ChildElementCount() > 0) {
+ L10nOverlaysError error;
+ error.mCode.Construct(
+ L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
+ nsAutoString id;
+ aToElement->GetAttr(nsGkAtoms::datal10nid, id);
+ error.mL10nName.Construct(id);
+ error.mTranslatedElementName.Construct(
+ aToElement->NodeInfo()->LocalName());
+ aErrors.AppendElement(error);
+ }
+ }
+#endif
+ aToElement->RemoveChildNode(child, true);
+ }
+ aToElement->AppendChild(*aFromFragment, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+}
+
+void L10nOverlays::TranslateElement(
+ const GlobalObject& aGlobal, Element& aElement,
+ const L10nMessage& aTranslation,
+ Nullable<nsTArray<L10nOverlaysError>>& aErrors) {
+ nsTArray<L10nOverlaysError> errors;
+
+ ErrorResult rv;
+
+ TranslateElement(aElement, aTranslation, errors, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ L10nOverlaysError error;
+ error.mCode.Construct(L10nOverlays_Binding::ERROR_UNKNOWN);
+ errors.AppendElement(error);
+ }
+ if (!errors.IsEmpty()) {
+ aErrors.SetValue(std::move(errors));
+ }
+}
+
+bool L10nOverlays::ContainsMarkup(const nsACString& aStr) {
+ // We use our custom ContainsMarkup rather than the
+ // one from FragmentOrElement.cpp, because we don't
+ // want to trigger HTML parsing on every `Preferences & Options`
+ // type of string.
+ const char* start = aStr.BeginReading();
+ const char* end = aStr.EndReading();
+
+ while (start != end) {
+ char c = *start;
+ if (c == '<') {
+ return true;
+ }
+ ++start;
+
+ if (c == '&' && start != end) {
+ c = *start;
+ if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z')) {
+ return true;
+ }
+ ++start;
+ }
+ }
+
+ return false;
+}
+
+void L10nOverlays::TranslateElement(Element& aElement,
+ const L10nMessage& aTranslation,
+ nsTArray<L10nOverlaysError>& aErrors,
+ ErrorResult& aRv) {
+ if (!aTranslation.mValue.IsVoid()) {
+ NodeInfo* nodeInfo = aElement.NodeInfo();
+ if (nodeInfo->NameAtom() == nsGkAtoms::title &&
+ nodeInfo->NamespaceID() == kNameSpaceID_XHTML) {
+ // A special case for the HTML title element whose content must be text.
+ aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ } else if (!ContainsMarkup(aTranslation.mValue)) {
+#ifdef DEBUG
+ if (aElement.ChildElementCount() > 0) {
+ L10nOverlaysError error;
+ error.mCode.Construct(
+ L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
+ nsAutoString id;
+ aElement.GetAttr(nsGkAtoms::datal10nid, id);
+ error.mL10nName.Construct(id);
+ error.mTranslatedElementName.Construct(
+ aElement.GetLastElementChild()->NodeInfo()->LocalName());
+ aErrors.AppendElement(error);
+ }
+#endif
+ // If the translation doesn't contain any markup skip the overlay logic.
+ aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ } else {
+ // Else parse the translation's HTML into a DocumentFragment,
+ // sanitize it and replace the element's content.
+ RefPtr<DocumentFragment> fragment =
+ new (aElement.OwnerDoc()->NodeInfoManager())
+ DocumentFragment(aElement.OwnerDoc()->NodeInfoManager());
+ // Note: these flags should be no less restrictive than the ones in
+ // nsContentUtils::ParseFragmentHTML .
+ // We supply the flags here because otherwise the parsing of HTML can
+ // trip DEBUG-only crashes, see bug 1809902 for details.
+ auto sanitizationFlags = nsIParserUtils::SanitizerDropForms |
+ nsIParserUtils::SanitizerLogRemovals;
+ nsContentUtils::ParseFragmentHTML(
+ NS_ConvertUTF8toUTF16(aTranslation.mValue), fragment,
+ nsGkAtoms::_template, kNameSpaceID_XHTML, false, true,
+ sanitizationFlags);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+
+ OverlayChildNodes(fragment, &aElement, aErrors, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+ }
+
+ // Even if the translation doesn't define any localizable attributes, run
+ // overlayAttributes to remove any localizable attributes set by previous
+ // translations.
+ OverlayAttributes(aTranslation.mAttributes, &aElement, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+}