1 /* 2 * Copyright (C) 1999 Lars Knoll (knoll (at) kde.org) 3 * (C) 1999 Antti Koivisto (koivisto (at) kde.org) 4 * (C) 2001 Dirk Mueller (mueller (at) kde.org) 5 * Copyright (C) 2003, 2010 Apple Inc. All rights reserved. 6 * 7 * This library is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU Library General Public 9 * License as published by the Free Software Foundation; either 10 * version 2 of the License, or (at your option) any later version. 11 * 12 * This library is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 * Library General Public License for more details. 16 * 17 * You should have received a copy of the GNU Library General Public License 18 * along with this library; see the file COPYING.LIB. If not, write to 19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 20 * Boston, MA 02110-1301, USA. 21 */ 22 23 #include "config.h" 24 #include "core/html/HTMLMetaElement.h" 25 26 #include "core/HTMLNames.h" 27 #include "core/dom/Document.h" 28 #include "core/frame/LocalFrame.h" 29 #include "core/frame/Settings.h" 30 #include "core/loader/FrameLoaderClient.h" 31 #include "platform/RuntimeEnabledFeatures.h" 32 33 namespace WebCore { 34 35 #define DEFINE_ARRAY_FOR_MATCHING(name, source, maxMatchLength) \ 36 const UChar* name; \ 37 const unsigned uMaxMatchLength = maxMatchLength; \ 38 UChar characterBuffer[uMaxMatchLength]; \ 39 if (!source.is8Bit()) { \ 40 name = source.characters16(); \ 41 } else { \ 42 unsigned bufferLength = std::min(uMaxMatchLength, source.length()); \ 43 const LChar* characters8 = source.characters8(); \ 44 for (unsigned i = 0; i < bufferLength; ++i) \ 45 characterBuffer[i] = characters8[i]; \ 46 name = characterBuffer; \ 47 } 48 49 using namespace HTMLNames; 50 51 inline HTMLMetaElement::HTMLMetaElement(Document& document) 52 : HTMLElement(metaTag, document) 53 { 54 ScriptWrappable::init(this); 55 } 56 57 DEFINE_NODE_FACTORY(HTMLMetaElement) 58 59 static bool isInvalidSeparator(UChar c) 60 { 61 return c == ';'; 62 } 63 64 // Though isspace() considers \t and \v to be whitespace, Win IE doesn't. 65 static bool isSeparator(UChar c) 66 { 67 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' || c == ',' || c == '\0'; 68 } 69 70 void HTMLMetaElement::parseContentAttribute(const String& content, KeyValuePairCallback callback, void* data) 71 { 72 bool error = false; 73 74 // Tread lightly in this code -- it was specifically designed to mimic Win IE's parsing behavior. 75 unsigned keyBegin, keyEnd; 76 unsigned valueBegin, valueEnd; 77 78 String buffer = content.lower(); 79 unsigned length = buffer.length(); 80 for (unsigned i = 0; i < length; /* no increment here */) { 81 // skip to first non-separator, but don't skip past the end of the string 82 while (isSeparator(buffer[i])) { 83 if (i >= length) 84 break; 85 i++; 86 } 87 keyBegin = i; 88 89 // skip to first separator 90 while (!isSeparator(buffer[i])) { 91 error |= isInvalidSeparator(buffer[i]); 92 if (i >= length) 93 break; 94 i++; 95 } 96 keyEnd = i; 97 98 // skip to first '=', but don't skip past a ',' or the end of the string 99 while (buffer[i] != '=') { 100 error |= isInvalidSeparator(buffer[i]); 101 if (buffer[i] == ',' || i >= length) 102 break; 103 i++; 104 } 105 106 // skip to first non-separator, but don't skip past a ',' or the end of the string 107 while (isSeparator(buffer[i])) { 108 if (buffer[i] == ',' || i >= length) 109 break; 110 i++; 111 } 112 valueBegin = i; 113 114 // skip to first separator 115 while (!isSeparator(buffer[i])) { 116 error |= isInvalidSeparator(buffer[i]); 117 if (i >= length) 118 break; 119 i++; 120 } 121 valueEnd = i; 122 123 ASSERT_WITH_SECURITY_IMPLICATION(i <= length); 124 125 String keyString = buffer.substring(keyBegin, keyEnd - keyBegin); 126 String valueString = buffer.substring(valueBegin, valueEnd - valueBegin); 127 (this->*callback)(keyString, valueString, data); 128 } 129 if (error) { 130 String message = "Error parsing a meta element's content: ';' is not a valid key-value pair separator. Please use ',' instead."; 131 document().addConsoleMessage(RenderingMessageSource, WarningMessageLevel, message); 132 } 133 } 134 135 static inline float clampLengthValue(float value) 136 { 137 // Limits as defined in the css-device-adapt spec. 138 if (value != ViewportDescription::ValueAuto) 139 return std::min(float(10000), std::max(value, float(1))); 140 return value; 141 } 142 143 static inline float clampScaleValue(float value) 144 { 145 // Limits as defined in the css-device-adapt spec. 146 if (value != ViewportDescription::ValueAuto) 147 return std::min(float(10), std::max(value, float(0.1))); 148 return value; 149 } 150 151 float HTMLMetaElement::parsePositiveNumber(const String& keyString, const String& valueString, bool* ok) 152 { 153 size_t parsedLength; 154 float value; 155 if (valueString.is8Bit()) 156 value = charactersToFloat(valueString.characters8(), valueString.length(), parsedLength); 157 else 158 value = charactersToFloat(valueString.characters16(), valueString.length(), parsedLength); 159 if (!parsedLength) { 160 reportViewportWarning(UnrecognizedViewportArgumentValueError, valueString, keyString); 161 if (ok) 162 *ok = false; 163 return 0; 164 } 165 if (parsedLength < valueString.length()) 166 reportViewportWarning(TruncatedViewportArgumentValueError, valueString, keyString); 167 if (ok) 168 *ok = true; 169 return value; 170 } 171 172 Length HTMLMetaElement::parseViewportValueAsLength(const String& keyString, const String& valueString) 173 { 174 // 1) Non-negative number values are translated to px lengths. 175 // 2) Negative number values are translated to auto. 176 // 3) device-width and device-height are used as keywords. 177 // 4) Other keywords and unknown values translate to 0.0. 178 179 unsigned length = valueString.length(); 180 DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); 181 SWITCH(characters, length) { 182 CASE("device-width") { 183 return Length(DeviceWidth); 184 } 185 CASE("device-height") { 186 return Length(DeviceHeight); 187 } 188 } 189 190 float value = parsePositiveNumber(keyString, valueString); 191 192 if (value < 0) 193 return Length(); // auto 194 195 return Length(clampLengthValue(value), Fixed); 196 } 197 198 float HTMLMetaElement::parseViewportValueAsZoom(const String& keyString, const String& valueString, bool& computedValueMatchesParsedValue) 199 { 200 // 1) Non-negative number values are translated to <number> values. 201 // 2) Negative number values are translated to auto. 202 // 3) yes is translated to 1.0. 203 // 4) device-width and device-height are translated to 10.0. 204 // 5) no and unknown values are translated to 0.0 205 206 computedValueMatchesParsedValue = false; 207 unsigned length = valueString.length(); 208 DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); 209 SWITCH(characters, length) { 210 CASE("yes") { 211 return 1; 212 } 213 CASE("no") { 214 return 0; 215 } 216 CASE("device-width") { 217 return 10; 218 } 219 CASE("device-height") { 220 return 10; 221 } 222 } 223 224 float value = parsePositiveNumber(keyString, valueString); 225 226 if (value < 0) 227 return ViewportDescription::ValueAuto; 228 229 if (value > 10.0) 230 reportViewportWarning(MaximumScaleTooLargeError, String(), String()); 231 232 if (!value && document().settings() && document().settings()->viewportMetaZeroValuesQuirk()) 233 return ViewportDescription::ValueAuto; 234 235 float clampedValue = clampScaleValue(value); 236 if (clampedValue == value) 237 computedValueMatchesParsedValue = true; 238 239 return clampedValue; 240 } 241 242 bool HTMLMetaElement::parseViewportValueAsUserZoom(const String& keyString, const String& valueString, bool& computedValueMatchesParsedValue) 243 { 244 // yes and no are used as keywords. 245 // Numbers >= 1, numbers <= -1, device-width and device-height are mapped to yes. 246 // Numbers in the range <-1, 1>, and unknown values, are mapped to no. 247 248 computedValueMatchesParsedValue = false; 249 unsigned length = valueString.length(); 250 DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); 251 SWITCH(characters, length) { 252 CASE("yes") { 253 computedValueMatchesParsedValue = true; 254 return true; 255 } 256 CASE("no") { 257 computedValueMatchesParsedValue = true; 258 return false; 259 } 260 CASE("device-width") { 261 return true; 262 } 263 CASE("device-height") { 264 return true; 265 } 266 } 267 268 float value = parsePositiveNumber(keyString, valueString); 269 if (fabs(value) < 1) 270 return false; 271 272 return true; 273 } 274 275 float HTMLMetaElement::parseViewportValueAsDPI(const String& keyString, const String& valueString) 276 { 277 unsigned length = valueString.length(); 278 DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 10); 279 SWITCH(characters, length) { 280 CASE("device-dpi") { 281 return ViewportDescription::ValueDeviceDPI; 282 } 283 CASE("low-dpi") { 284 return ViewportDescription::ValueLowDPI; 285 } 286 CASE("medium-dpi") { 287 return ViewportDescription::ValueMediumDPI; 288 } 289 CASE("high-dpi") { 290 return ViewportDescription::ValueHighDPI; 291 } 292 } 293 294 bool ok; 295 float value = parsePositiveNumber(keyString, valueString, &ok); 296 if (!ok || value < 70 || value > 400) 297 return ViewportDescription::ValueAuto; 298 299 return value; 300 } 301 302 void HTMLMetaElement::processViewportKeyValuePair(const String& keyString, const String& valueString, void* data) 303 { 304 ViewportDescription* description = static_cast<ViewportDescription*>(data); 305 306 unsigned length = keyString.length(); 307 308 DEFINE_ARRAY_FOR_MATCHING(characters, keyString, 17); 309 SWITCH(characters, length) { 310 CASE("width") { 311 const Length& width = parseViewportValueAsLength(keyString, valueString); 312 if (width.isAuto()) 313 return; 314 description->minWidth = Length(ExtendToZoom); 315 description->maxWidth = width; 316 return; 317 } 318 CASE("height") { 319 const Length& height = parseViewportValueAsLength(keyString, valueString); 320 if (height.isAuto()) 321 return; 322 description->minHeight = Length(ExtendToZoom); 323 description->maxHeight = height; 324 return; 325 } 326 CASE("initial-scale") { 327 description->zoom = parseViewportValueAsZoom(keyString, valueString, description->zoomIsExplicit); 328 return; 329 } 330 CASE("minimum-scale") { 331 description->minZoom = parseViewportValueAsZoom(keyString, valueString, description->minZoomIsExplicit); 332 return; 333 } 334 CASE("maximum-scale") { 335 description->maxZoom = parseViewportValueAsZoom(keyString, valueString, description->maxZoomIsExplicit); 336 return; 337 } 338 CASE("user-scalable") { 339 description->userZoom = parseViewportValueAsUserZoom(keyString, valueString, description->userZoomIsExplicit); 340 return; 341 } 342 CASE("target-densitydpi") { 343 description->deprecatedTargetDensityDPI = parseViewportValueAsDPI(keyString, valueString); 344 reportViewportWarning(TargetDensityDpiUnsupported, String(), String()); 345 return; 346 } 347 CASE("minimal-ui") { 348 // Ignore vendor-specific argument. 349 return; 350 } 351 } 352 reportViewportWarning(UnrecognizedViewportArgumentKeyError, keyString, String()); 353 } 354 355 static const char* viewportErrorMessageTemplate(ViewportErrorCode errorCode) 356 { 357 static const char* const errors[] = { 358 "The key \"%replacement1\" is not recognized and ignored.", 359 "The value \"%replacement1\" for key \"%replacement2\" is invalid, and has been ignored.", 360 "The value \"%replacement1\" for key \"%replacement2\" was truncated to its numeric prefix.", 361 "The value for key \"maximum-scale\" is out of bounds and the value has been clamped.", 362 "The key \"target-densitydpi\" is not supported.", 363 }; 364 365 return errors[errorCode]; 366 } 367 368 static MessageLevel viewportErrorMessageLevel(ViewportErrorCode errorCode) 369 { 370 switch (errorCode) { 371 case TruncatedViewportArgumentValueError: 372 case TargetDensityDpiUnsupported: 373 case UnrecognizedViewportArgumentKeyError: 374 case UnrecognizedViewportArgumentValueError: 375 case MaximumScaleTooLargeError: 376 return WarningMessageLevel; 377 } 378 379 ASSERT_NOT_REACHED(); 380 return ErrorMessageLevel; 381 } 382 383 void HTMLMetaElement::reportViewportWarning(ViewportErrorCode errorCode, const String& replacement1, const String& replacement2) 384 { 385 if (!document().frame()) 386 return; 387 388 String message = viewportErrorMessageTemplate(errorCode); 389 if (!replacement1.isNull()) 390 message.replace("%replacement1", replacement1); 391 if (!replacement2.isNull()) 392 message.replace("%replacement2", replacement2); 393 394 // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists. 395 document().addConsoleMessage(RenderingMessageSource, viewportErrorMessageLevel(errorCode), message); 396 } 397 398 void HTMLMetaElement::processViewportContentAttribute(const String& content, ViewportDescription::Type origin) 399 { 400 ASSERT(!content.isNull()); 401 402 if (!document().shouldOverrideLegacyDescription(origin)) 403 return; 404 405 ViewportDescription descriptionFromLegacyTag(origin); 406 if (document().shouldMergeWithLegacyDescription(origin)) 407 descriptionFromLegacyTag = document().viewportDescription(); 408 409 parseContentAttribute(content, &HTMLMetaElement::processViewportKeyValuePair, (void*)&descriptionFromLegacyTag); 410 411 if (descriptionFromLegacyTag.minZoom == ViewportDescription::ValueAuto) 412 descriptionFromLegacyTag.minZoom = 0.25; 413 414 if (descriptionFromLegacyTag.maxZoom == ViewportDescription::ValueAuto) { 415 descriptionFromLegacyTag.maxZoom = 5; 416 descriptionFromLegacyTag.minZoom = std::min(descriptionFromLegacyTag.minZoom, float(5)); 417 } 418 419 document().setViewportDescription(descriptionFromLegacyTag); 420 } 421 422 423 void HTMLMetaElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 424 { 425 if (name == http_equivAttr || name == contentAttr) { 426 process(); 427 return; 428 } 429 430 if (name != nameAttr) 431 HTMLElement::parseAttribute(name, value); 432 } 433 434 Node::InsertionNotificationRequest HTMLMetaElement::insertedInto(ContainerNode* insertionPoint) 435 { 436 HTMLElement::insertedInto(insertionPoint); 437 return InsertionShouldCallDidNotifySubtreeInsertions; 438 } 439 440 void HTMLMetaElement::didNotifySubtreeInsertionsToDocument() 441 { 442 process(); 443 } 444 445 static bool inDocumentHead(HTMLMetaElement* element) 446 { 447 if (!element->inDocument()) 448 return false; 449 450 for (Element* current = element; current; current = current->parentElement()) { 451 if (isHTMLHeadElement(*current)) 452 return true; 453 } 454 return false; 455 } 456 457 void HTMLMetaElement::process() 458 { 459 if (!inDocument()) 460 return; 461 462 // All below situations require a content attribute (which can be the empty string). 463 const AtomicString& contentValue = fastGetAttribute(contentAttr); 464 if (contentValue.isNull()) 465 return; 466 467 const AtomicString& nameValue = fastGetAttribute(nameAttr); 468 if (!nameValue.isEmpty()) { 469 if (equalIgnoringCase(nameValue, "viewport")) 470 processViewportContentAttribute(contentValue, ViewportDescription::ViewportMeta); 471 else if (equalIgnoringCase(nameValue, "referrer")) 472 document().processReferrerPolicy(contentValue); 473 else if (equalIgnoringCase(nameValue, "handheldfriendly") && equalIgnoringCase(contentValue, "true")) 474 processViewportContentAttribute("width=device-width", ViewportDescription::HandheldFriendlyMeta); 475 else if (equalIgnoringCase(nameValue, "mobileoptimized")) 476 processViewportContentAttribute("width=device-width, initial-scale=1", ViewportDescription::MobileOptimizedMeta); 477 else if (RuntimeEnabledFeatures::themeColorEnabled() && equalIgnoringCase(nameValue, "theme-color") && document().frame()) 478 document().frame()->loader().client()->dispatchDidChangeThemeColor(); 479 } 480 481 // Get the document to process the tag, but only if we're actually part of DOM 482 // tree (changing a meta tag while it's not in the tree shouldn't have any effect 483 // on the document). 484 485 const AtomicString& httpEquivValue = fastGetAttribute(http_equivAttr); 486 if (!httpEquivValue.isEmpty()) 487 document().processHttpEquiv(httpEquivValue, contentValue, inDocumentHead(this)); 488 } 489 490 const AtomicString& HTMLMetaElement::content() const 491 { 492 return getAttribute(contentAttr); 493 } 494 495 const AtomicString& HTMLMetaElement::httpEquiv() const 496 { 497 return getAttribute(http_equivAttr); 498 } 499 500 const AtomicString& HTMLMetaElement::name() const 501 { 502 return getNameAttribute(); 503 } 504 505 } 506