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