Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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;
}
}