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 }