tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

L10nOverlays.cpp (18732B)


      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/. */
      6 
      7 #include "L10nOverlays.h"
      8 
      9 #include "HTMLSplitOnSpacesTokenizer.h"
     10 #include "mozilla/dom/Document.h"
     11 #include "mozilla/dom/DocumentFragment.h"
     12 #include "mozilla/dom/HTMLInputElement.h"
     13 #include "nsHtml5StringParser.h"
     14 #include "nsINodeList.h"
     15 #include "nsIParserUtils.h"
     16 #include "nsTextNode.h"
     17 
     18 using namespace mozilla::dom;
     19 using namespace mozilla;
     20 
     21 /**
     22 * Check if attribute is allowed for the given element.
     23 *
     24 * This method is used by the sanitizer when the translation markup contains DOM
     25 * attributes, or when the translation has traits which map to DOM attributes.
     26 *
     27 * `aExplicitlyAllowed` can be passed as a list of attributes explicitly allowed
     28 * on this element.
     29 */
     30 static bool IsAttrNameLocalizable(
     31    const nsAtom* aAttrName, Element* aElement,
     32    const nsTArray<nsString>& aExplicitlyAllowed) {
     33  if (!aExplicitlyAllowed.IsEmpty()) {
     34    nsAutoString name;
     35    aAttrName->ToString(name);
     36    if (aExplicitlyAllowed.Contains(name)) {
     37      return true;
     38    }
     39  }
     40 
     41  nsAtom* elemName = aElement->NodeInfo()->NameAtom();
     42  uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
     43 
     44  if (nameSpace == kNameSpaceID_XHTML) {
     45    // Is it a globally safe attribute?
     46    if (aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::aria_label ||
     47        aAttrName == nsGkAtoms::aria_description) {
     48      return true;
     49    }
     50 
     51    // Is it allowed on this element?
     52    if (elemName == nsGkAtoms::a) {
     53      return aAttrName == nsGkAtoms::download;
     54    }
     55    if (elemName == nsGkAtoms::area) {
     56      return aAttrName == nsGkAtoms::download || aAttrName == nsGkAtoms::alt;
     57    }
     58    if (elemName == nsGkAtoms::input) {
     59      // Special case for value on HTML inputs with type button, reset, submit
     60      if (aAttrName == nsGkAtoms::value) {
     61        HTMLInputElement* input = HTMLInputElement::FromNode(aElement);
     62        if (input) {
     63          auto type = input->ControlType();
     64          if (type == FormControlType::InputSubmit ||
     65              type == FormControlType::InputButton ||
     66              type == FormControlType::InputReset) {
     67            return true;
     68          }
     69        }
     70      }
     71      return aAttrName == nsGkAtoms::alt || aAttrName == nsGkAtoms::placeholder;
     72    }
     73    if (elemName == nsGkAtoms::menuitem) {
     74      return aAttrName == nsGkAtoms::label;
     75    }
     76    if (elemName == nsGkAtoms::menu) {
     77      return aAttrName == nsGkAtoms::label;
     78    }
     79    if (elemName == nsGkAtoms::optgroup) {
     80      return aAttrName == nsGkAtoms::label;
     81    }
     82    if (elemName == nsGkAtoms::option) {
     83      return aAttrName == nsGkAtoms::label;
     84    }
     85    if (elemName == nsGkAtoms::track) {
     86      return aAttrName == nsGkAtoms::label;
     87    }
     88    if (elemName == nsGkAtoms::img) {
     89      return aAttrName == nsGkAtoms::alt;
     90    }
     91    if (elemName == nsGkAtoms::textarea) {
     92      return aAttrName == nsGkAtoms::placeholder;
     93    }
     94    if (elemName == nsGkAtoms::th) {
     95      return aAttrName == nsGkAtoms::abbr;
     96    }
     97 
     98  } else if (nameSpace == kNameSpaceID_XUL) {
     99    // Is it a globally safe attribute?
    100    if (aAttrName == nsGkAtoms::accesskey ||
    101        aAttrName == nsGkAtoms::aria_label || aAttrName == nsGkAtoms::label ||
    102        aAttrName == nsGkAtoms::title || aAttrName == nsGkAtoms::tooltiptext) {
    103      return true;
    104    }
    105 
    106    // Is it allowed on this element?
    107    if (elemName == nsGkAtoms::description) {
    108      return aAttrName == nsGkAtoms::value;
    109    }
    110    if (elemName == nsGkAtoms::key) {
    111      return aAttrName == nsGkAtoms::key || aAttrName == nsGkAtoms::keycode;
    112    }
    113    if (elemName == nsGkAtoms::label) {
    114      return aAttrName == nsGkAtoms::value;
    115    }
    116  }
    117 
    118  return false;
    119 }
    120 
    121 already_AddRefed<nsINode> L10nOverlays::CreateTextNodeFromTextContent(
    122    Element* aElement, ErrorResult& aRv) {
    123  nsAutoString content;
    124  aElement->GetTextContent(content, aRv);
    125 
    126  if (NS_WARN_IF(aRv.Failed())) {
    127    return nullptr;
    128  }
    129 
    130  return aElement->OwnerDoc()->CreateTextNode(content);
    131 }
    132 
    133 class AttributeNameValueComparator {
    134 public:
    135  bool Equals(const AttributeNameValue& aAttribute,
    136              const nsAttrName* aAttrName) const {
    137    return aAttrName->Equals(NS_ConvertUTF8toUTF16(aAttribute.mName));
    138  }
    139 };
    140 
    141 void L10nOverlays::OverlayAttributes(
    142    const Nullable<Sequence<AttributeNameValue>>& aTranslation,
    143    Element* aToElement, ErrorResult& aRv) {
    144  nsTArray<nsString> explicitlyAllowed;
    145 
    146  {
    147    nsAutoString l10nAttrs;
    148    if (aToElement->GetAttr(nsGkAtoms::datal10nattrs, l10nAttrs)) {
    149      HTMLSplitOnSpacesTokenizer tokenizer(l10nAttrs, ',');
    150      while (tokenizer.hasMoreTokens()) {
    151        const nsAString& token = tokenizer.nextToken();
    152        if (!token.IsEmpty() && !explicitlyAllowed.Contains(token)) {
    153          explicitlyAllowed.AppendElement(token);
    154        }
    155      }
    156    }
    157  }
    158 
    159  uint32_t i = aToElement->GetAttrCount();
    160  while (i > 0) {
    161    const nsAttrName* attrName = aToElement->GetAttrNameAt(i - 1);
    162 
    163    if (IsAttrNameLocalizable(attrName->LocalName(), aToElement,
    164                              explicitlyAllowed) &&
    165        (aTranslation.IsNull() ||
    166         !aTranslation.Value().Contains(attrName,
    167                                        AttributeNameValueComparator()))) {
    168      RefPtr<nsAtom> localName = attrName->LocalName();
    169      aToElement->UnsetAttr(localName, aRv);
    170      if (NS_WARN_IF(aRv.Failed())) {
    171        return;
    172      }
    173    }
    174    i--;
    175  }
    176 
    177  if (aTranslation.IsNull()) {
    178    return;
    179  }
    180 
    181  for (auto& attribute : aTranslation.Value()) {
    182    RefPtr<nsAtom> nameAtom = NS_Atomize(attribute.mName);
    183    if (IsAttrNameLocalizable(nameAtom, aToElement, explicitlyAllowed)) {
    184      NS_ConvertUTF8toUTF16 value(attribute.mValue);
    185      if (!aToElement->AttrValueIs(kNameSpaceID_None, nameAtom, value,
    186                                   eCaseMatters)) {
    187        aToElement->SetAttr(nameAtom, value, aRv);
    188        if (NS_WARN_IF(aRv.Failed())) {
    189          return;
    190        }
    191      }
    192    }
    193  }
    194 }
    195 
    196 void L10nOverlays::OverlayAttributes(Element* aFromElement, Element* aToElement,
    197                                     ErrorResult& aRv) {
    198  Nullable<Sequence<AttributeNameValue>> attributes;
    199  uint32_t attrCount = aFromElement->GetAttrCount();
    200 
    201  if (attrCount == 0) {
    202    attributes.SetNull();
    203  } else {
    204    Sequence<AttributeNameValue> sequence;
    205 
    206    uint32_t i = 0;
    207    while (BorrowedAttrInfo info = aFromElement->GetAttrInfoAt(i++)) {
    208      AttributeNameValue* attr = sequence.AppendElement(fallible);
    209      MOZ_ASSERT(info.mName->NamespaceEquals(kNameSpaceID_None),
    210                 "No namespaced attributes allowed.");
    211      info.mName->LocalName()->ToUTF8String(attr->mName);
    212 
    213      nsAutoString value;
    214      info.mValue->ToString(value);
    215      attr->mValue.Assign(NS_ConvertUTF16toUTF8(value));
    216    }
    217 
    218    attributes.SetValue(sequence);
    219  }
    220 
    221  return OverlayAttributes(attributes, aToElement, aRv);
    222 }
    223 
    224 void L10nOverlays::ShallowPopulateUsing(Element* aFromElement,
    225                                        Element* aToElement, ErrorResult& aRv) {
    226  nsAutoString content;
    227  aFromElement->GetTextContent(content, aRv);
    228  if (NS_WARN_IF(aRv.Failed())) {
    229    return;
    230  }
    231 
    232  aToElement->SetTextContent(content, aRv);
    233  if (NS_WARN_IF(aRv.Failed())) {
    234    return;
    235  }
    236 
    237  OverlayAttributes(aFromElement, aToElement, aRv);
    238  if (NS_WARN_IF(aRv.Failed())) {
    239    return;
    240  }
    241 }
    242 
    243 already_AddRefed<nsINode> L10nOverlays::GetNodeForNamedElement(
    244    Element* aSourceElement, Element* aTranslatedChild,
    245    nsTArray<L10nOverlaysError>& aErrors, ErrorResult& aRv) {
    246  nsAutoString childName;
    247  aTranslatedChild->GetAttr(nsGkAtoms::datal10nname, childName);
    248  RefPtr<Element> sourceChild = nullptr;
    249 
    250  nsINodeList* childNodes = aSourceElement->ChildNodes();
    251  for (uint32_t i = 0; i < childNodes->Length(); i++) {
    252    nsINode* childNode = childNodes->Item(i);
    253 
    254    if (!childNode->IsElement()) {
    255      continue;
    256    }
    257    Element* childElement = childNode->AsElement();
    258 
    259    if (childElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::datal10nname,
    260                                  childName, eCaseMatters)) {
    261      sourceChild = childElement;
    262      break;
    263    }
    264  }
    265 
    266  if (!sourceChild) {
    267    L10nOverlaysError error;
    268    error.mCode.Construct(L10nOverlays_Binding::ERROR_NAMED_ELEMENT_MISSING);
    269    error.mL10nName.Construct(childName);
    270    aErrors.AppendElement(error);
    271    return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
    272  }
    273 
    274  nsAtom* sourceChildName = sourceChild->NodeInfo()->NameAtom();
    275  nsAtom* translatedChildName = aTranslatedChild->NodeInfo()->NameAtom();
    276  if (sourceChildName != translatedChildName &&
    277      // Create a specific exception for img vs. image mismatches,
    278      // see bug 1543493
    279      !(translatedChildName == nsGkAtoms::img &&
    280        sourceChildName == nsGkAtoms::image)) {
    281    L10nOverlaysError error;
    282    error.mCode.Construct(
    283        L10nOverlays_Binding::ERROR_NAMED_ELEMENT_TYPE_MISMATCH);
    284    error.mL10nName.Construct(childName);
    285    error.mTranslatedElementName.Construct(
    286        aTranslatedChild->NodeInfo()->LocalName());
    287    error.mSourceElementName.Construct(sourceChild->NodeInfo()->LocalName());
    288    aErrors.AppendElement(error);
    289    return CreateTextNodeFromTextContent(aTranslatedChild, aRv);
    290  }
    291 
    292  aSourceElement->RemoveChild(*sourceChild, aRv);
    293  if (NS_WARN_IF(aRv.Failed())) {
    294    return nullptr;
    295  }
    296  RefPtr<nsINode> clone = sourceChild->CloneNode(false, aRv);
    297  if (NS_WARN_IF(aRv.Failed())) {
    298    return nullptr;
    299  }
    300  ShallowPopulateUsing(aTranslatedChild, clone->AsElement(), aRv);
    301  if (NS_WARN_IF(aRv.Failed())) {
    302    return nullptr;
    303  }
    304  return clone.forget();
    305 }
    306 
    307 bool L10nOverlays::IsElementAllowed(Element* aElement) {
    308  uint32_t nameSpace = aElement->NodeInfo()->NamespaceID();
    309  if (nameSpace != kNameSpaceID_XHTML) {
    310    return false;
    311  }
    312 
    313  nsAtom* nameAtom = aElement->NodeInfo()->NameAtom();
    314 
    315  return nameAtom == nsGkAtoms::em || nameAtom == nsGkAtoms::strong ||
    316         nameAtom == nsGkAtoms::small || nameAtom == nsGkAtoms::s ||
    317         nameAtom == nsGkAtoms::cite || nameAtom == nsGkAtoms::q ||
    318         nameAtom == nsGkAtoms::dfn || nameAtom == nsGkAtoms::abbr ||
    319         nameAtom == nsGkAtoms::data || nameAtom == nsGkAtoms::time ||
    320         nameAtom == nsGkAtoms::code || nameAtom == nsGkAtoms::var ||
    321         nameAtom == nsGkAtoms::samp || nameAtom == nsGkAtoms::kbd ||
    322         nameAtom == nsGkAtoms::sub || nameAtom == nsGkAtoms::sup ||
    323         nameAtom == nsGkAtoms::i || nameAtom == nsGkAtoms::b ||
    324         nameAtom == nsGkAtoms::u || nameAtom == nsGkAtoms::mark ||
    325         nameAtom == nsGkAtoms::bdi || nameAtom == nsGkAtoms::bdo ||
    326         nameAtom == nsGkAtoms::span || nameAtom == nsGkAtoms::br ||
    327         nameAtom == nsGkAtoms::wbr;
    328 }
    329 
    330 already_AddRefed<Element> L10nOverlays::CreateSanitizedElement(
    331    Element* aElement, ErrorResult& aRv) {
    332  // Start with an empty element of the same type to remove nested children
    333  // and non-localizable attributes defined by the translation.
    334 
    335  nsAutoString nameSpaceURI;
    336  aElement->NodeInfo()->GetNamespaceURI(nameSpaceURI);
    337  ElementCreationOptionsOrString options;
    338  RefPtr<Element> clone = aElement->OwnerDoc()->CreateElementNS(
    339      nameSpaceURI, aElement->NodeInfo()->LocalName(), options, aRv);
    340  if (NS_WARN_IF(aRv.Failed())) {
    341    return nullptr;
    342  }
    343 
    344  ShallowPopulateUsing(aElement, clone, aRv);
    345  if (NS_WARN_IF(aRv.Failed())) {
    346    return nullptr;
    347  }
    348  return clone.forget();
    349 }
    350 
    351 void L10nOverlays::OverlayChildNodes(DocumentFragment* aFromFragment,
    352                                     Element* aToElement,
    353                                     nsTArray<L10nOverlaysError>& aErrors,
    354                                     ErrorResult& aRv) {
    355  nsINodeList* childNodes = aFromFragment->ChildNodes();
    356  for (uint32_t i = 0; i < childNodes->Length(); i++) {
    357    nsINode* childNode = childNodes->Item(i);
    358 
    359    if (!childNode->IsElement()) {
    360      // Keep the translated text node.
    361      continue;
    362    }
    363 
    364    RefPtr<Element> childElement = childNode->AsElement();
    365 
    366    if (childElement->HasAttr(nsGkAtoms::datal10nname)) {
    367      RefPtr<nsINode> sanitized =
    368          GetNodeForNamedElement(aToElement, childElement, aErrors, aRv);
    369      if (NS_WARN_IF(aRv.Failed())) {
    370        return;
    371      }
    372      aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
    373      if (NS_WARN_IF(aRv.Failed())) {
    374        return;
    375      }
    376      continue;
    377    }
    378 
    379    if (IsElementAllowed(childElement)) {
    380      RefPtr<Element> sanitized = CreateSanitizedElement(childElement, aRv);
    381      if (NS_WARN_IF(aRv.Failed())) {
    382        return;
    383      }
    384      aFromFragment->ReplaceChild(*sanitized, *childNode, aRv);
    385      if (NS_WARN_IF(aRv.Failed())) {
    386        return;
    387      }
    388      continue;
    389    }
    390 
    391    L10nOverlaysError error;
    392    error.mCode.Construct(L10nOverlays_Binding::ERROR_FORBIDDEN_TYPE);
    393    error.mTranslatedElementName.Construct(
    394        childElement->NodeInfo()->LocalName());
    395    aErrors.AppendElement(error);
    396 
    397    // If all else fails, replace the element with its text content.
    398    RefPtr<nsINode> textNode = CreateTextNodeFromTextContent(childElement, aRv);
    399    if (NS_WARN_IF(aRv.Failed())) {
    400      return;
    401    }
    402 
    403    aFromFragment->ReplaceChild(*textNode, *childNode, aRv);
    404    if (NS_WARN_IF(aRv.Failed())) {
    405      return;
    406    }
    407  }
    408 
    409  while (aToElement->HasChildren()) {
    410    nsIContent* child = aToElement->GetLastChild();
    411 #ifdef DEBUG
    412    if (child->IsElement()) {
    413      if (child->AsElement()->HasAttr(nsGkAtoms::datal10nid)) {
    414        L10nOverlaysError error;
    415        error.mCode.Construct(
    416            L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISCONNECTED);
    417        nsAutoString id;
    418        child->AsElement()->GetAttr(nsGkAtoms::datal10nid, id);
    419        error.mL10nName.Construct(id);
    420        error.mTranslatedElementName.Construct(
    421            aToElement->NodeInfo()->LocalName());
    422        aErrors.AppendElement(error);
    423      } else if (child->AsElement()->ChildElementCount() > 0) {
    424        L10nOverlaysError error;
    425        error.mCode.Construct(
    426            L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
    427        nsAutoString id;
    428        aToElement->GetAttr(nsGkAtoms::datal10nid, id);
    429        error.mL10nName.Construct(id);
    430        error.mTranslatedElementName.Construct(
    431            aToElement->NodeInfo()->LocalName());
    432        aErrors.AppendElement(error);
    433      }
    434    }
    435 #endif
    436    aToElement->RemoveChildNode(child, true);
    437  }
    438  aToElement->AppendChild(*aFromFragment, aRv);
    439  if (NS_WARN_IF(aRv.Failed())) {
    440    return;
    441  }
    442 }
    443 
    444 void L10nOverlays::TranslateElement(
    445    const GlobalObject& aGlobal, Element& aElement,
    446    const L10nMessage& aTranslation,
    447    Nullable<nsTArray<L10nOverlaysError>>& aErrors) {
    448  nsTArray<L10nOverlaysError> errors;
    449 
    450  ErrorResult rv;
    451 
    452  TranslateElement(aElement, aTranslation, errors, rv);
    453  if (NS_WARN_IF(rv.Failed())) {
    454    L10nOverlaysError error;
    455    error.mCode.Construct(L10nOverlays_Binding::ERROR_UNKNOWN);
    456    errors.AppendElement(error);
    457  }
    458  if (!errors.IsEmpty()) {
    459    aErrors.SetValue(std::move(errors));
    460  }
    461 }
    462 
    463 bool L10nOverlays::ContainsMarkup(const nsACString& aStr) {
    464  // We use our custom ContainsMarkup rather than the
    465  // one from FragmentOrElement.cpp, because we don't
    466  // want to trigger HTML parsing on every `Preferences & Options`
    467  // type of string.
    468  const char* start = aStr.BeginReading();
    469  const char* end = aStr.EndReading();
    470 
    471  while (start != end) {
    472    char c = *start;
    473    if (c == '<') {
    474      return true;
    475    }
    476    ++start;
    477 
    478    if (c == '&' && start != end) {
    479      c = *start;
    480      if (c == '#' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
    481          (c >= 'A' && c <= 'Z')) {
    482        return true;
    483      }
    484      ++start;
    485    }
    486  }
    487 
    488  return false;
    489 }
    490 
    491 void L10nOverlays::TranslateElement(Element& aElement,
    492                                    const L10nMessage& aTranslation,
    493                                    nsTArray<L10nOverlaysError>& aErrors,
    494                                    ErrorResult& aRv) {
    495  if (!aTranslation.mValue.IsVoid()) {
    496    NodeInfo* nodeInfo = aElement.NodeInfo();
    497    if (nodeInfo->NameAtom() == nsGkAtoms::title &&
    498        nodeInfo->NamespaceID() == kNameSpaceID_XHTML) {
    499      // A special case for the HTML title element whose content must be text.
    500      aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
    501      if (NS_WARN_IF(aRv.Failed())) {
    502        return;
    503      }
    504    } else if (!ContainsMarkup(aTranslation.mValue)) {
    505 #ifdef DEBUG
    506      if (aElement.ChildElementCount() > 0) {
    507        L10nOverlaysError error;
    508        error.mCode.Construct(
    509            L10nOverlays_Binding::ERROR_TRANSLATED_ELEMENT_DISALLOWED_DOM);
    510        nsAutoString id;
    511        aElement.GetAttr(nsGkAtoms::datal10nid, id);
    512        error.mL10nName.Construct(id);
    513        error.mTranslatedElementName.Construct(
    514            aElement.GetLastElementChild()->NodeInfo()->LocalName());
    515        aErrors.AppendElement(error);
    516      }
    517 #endif
    518      // If the translation doesn't contain any markup skip the overlay logic.
    519      aElement.SetTextContent(NS_ConvertUTF8toUTF16(aTranslation.mValue), aRv);
    520      if (NS_WARN_IF(aRv.Failed())) {
    521        return;
    522      }
    523    } else {
    524      // Else parse the translation's HTML into a DocumentFragment,
    525      // sanitize it and replace the element's content.
    526      RefPtr<DocumentFragment> fragment =
    527          new (aElement.OwnerDoc()->NodeInfoManager())
    528              DocumentFragment(aElement.OwnerDoc()->NodeInfoManager());
    529      // Note: these flags should be no less restrictive than the ones in
    530      // nsContentUtils::ParseFragmentHTML .
    531      // We supply the flags here because otherwise the parsing of HTML can
    532      // trip DEBUG-only crashes, see bug 1809902 for details.
    533      auto sanitizationFlags = nsIParserUtils::SanitizerDropForms |
    534                               nsIParserUtils::SanitizerLogRemovals;
    535      nsContentUtils::ParseFragmentHTML(
    536          NS_ConvertUTF8toUTF16(aTranslation.mValue), fragment,
    537          nsGkAtoms::_template, kNameSpaceID_XHTML, false, true,
    538          sanitizationFlags);
    539      if (NS_WARN_IF(aRv.Failed())) {
    540        return;
    541      }
    542 
    543      OverlayChildNodes(fragment, &aElement, aErrors, aRv);
    544      if (NS_WARN_IF(aRv.Failed())) {
    545        return;
    546      }
    547    }
    548  }
    549 
    550  // Even if the translation doesn't define any localizable attributes, run
    551  // overlayAttributes to remove any localizable attributes set by previous
    552  // translations.
    553  OverlayAttributes(aTranslation.mAttributes, &aElement, aRv);
    554  if (NS_WARN_IF(aRv.Failed())) {
    555    return;
    556  }
    557 }