| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * (C) 2001 Dirk Mueller (mueller@kde.org) |
| * Copyright (C) 2003, 2010 Apple Inc. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "config.h" |
| #include "core/html/HTMLMetaElement.h" |
| |
| #include "HTMLNames.h" |
| #include "core/dom/Document.h" |
| #include "core/page/Settings.h" |
| |
| namespace WebCore { |
| |
| #define DEFINE_ARRAY_FOR_MATCHING(name, source, maxMatchLength) \ |
| const UChar* name; \ |
| const unsigned uMaxMatchLength = maxMatchLength; \ |
| UChar characterBuffer[uMaxMatchLength]; \ |
| if (!source.is8Bit()) { \ |
| name = source.characters16(); \ |
| } else { \ |
| unsigned bufferLength = std::min(uMaxMatchLength, source.length()); \ |
| const LChar* characters8 = source.characters8(); \ |
| for (unsigned i = 0; i < bufferLength; ++i) \ |
| characterBuffer[i] = characters8[i]; \ |
| name = characterBuffer; \ |
| } |
| |
| using namespace HTMLNames; |
| |
| inline HTMLMetaElement::HTMLMetaElement(Document& document) |
| : HTMLElement(metaTag, document) |
| { |
| ScriptWrappable::init(this); |
| } |
| |
| PassRefPtr<HTMLMetaElement> HTMLMetaElement::create(Document& document) |
| { |
| return adoptRef(new HTMLMetaElement(document)); |
| } |
| |
| static bool isInvalidSeparator(UChar c) |
| { |
| return c == ';'; |
| } |
| |
| // Though isspace() considers \t and \v to be whitespace, Win IE doesn't. |
| static bool isSeparator(UChar c) |
| { |
| return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' || c == ',' || c == '\0'; |
| } |
| |
| void HTMLMetaElement::parseContentAttribute(const String& content, KeyValuePairCallback callback, void* data) |
| { |
| bool error = false; |
| |
| // Tread lightly in this code -- it was specifically designed to mimic Win IE's parsing behavior. |
| int keyBegin, keyEnd; |
| int valueBegin, valueEnd; |
| |
| int i = 0; |
| int length = content.length(); |
| String buffer = content.lower(); |
| while (i < length) { |
| // skip to first non-separator, but don't skip past the end of the string |
| while (isSeparator(buffer[i])) { |
| if (i >= length) |
| break; |
| i++; |
| } |
| keyBegin = i; |
| |
| // skip to first separator |
| while (!isSeparator(buffer[i])) { |
| error |= isInvalidSeparator(buffer[i]); |
| i++; |
| } |
| keyEnd = i; |
| |
| // skip to first '=', but don't skip past a ',' or the end of the string |
| while (buffer[i] != '=') { |
| error |= isInvalidSeparator(buffer[i]); |
| if (buffer[i] == ',' || i >= length) |
| break; |
| i++; |
| } |
| |
| // skip to first non-separator, but don't skip past a ',' or the end of the string |
| while (isSeparator(buffer[i])) { |
| if (buffer[i] == ',' || i >= length) |
| break; |
| i++; |
| } |
| valueBegin = i; |
| |
| // skip to first separator |
| while (!isSeparator(buffer[i])) { |
| error |= isInvalidSeparator(buffer[i]); |
| i++; |
| } |
| valueEnd = i; |
| |
| ASSERT_WITH_SECURITY_IMPLICATION(i <= length); |
| |
| String keyString = buffer.substring(keyBegin, keyEnd - keyBegin); |
| String valueString = buffer.substring(valueBegin, valueEnd - valueBegin); |
| (this->*callback)(keyString, valueString, data); |
| } |
| if (error) { |
| String message = "Error parsing a meta element's content: ';' is not a valid key-value pair separator. Please use ',' instead."; |
| document().addConsoleMessage(RenderingMessageSource, WarningMessageLevel, message); |
| } |
| } |
| |
| static inline float clampLengthValue(float value) |
| { |
| // Limits as defined in the css-device-adapt spec. |
| if (value != ViewportDescription::ValueAuto) |
| return std::min(float(10000), std::max(value, float(1))); |
| return value; |
| } |
| |
| static inline float clampScaleValue(float value) |
| { |
| // Limits as defined in the css-device-adapt spec. |
| if (value != ViewportDescription::ValueAuto) |
| return std::min(float(10), std::max(value, float(0.1))); |
| return value; |
| } |
| |
| float HTMLMetaElement::parsePositiveNumber(const String& keyString, const String& valueString, bool* ok) |
| { |
| size_t parsedLength; |
| float value; |
| if (valueString.is8Bit()) |
| value = charactersToFloat(valueString.characters8(), valueString.length(), parsedLength); |
| else |
| value = charactersToFloat(valueString.characters16(), valueString.length(), parsedLength); |
| if (!parsedLength) { |
| reportViewportWarning(UnrecognizedViewportArgumentValueError, valueString, keyString); |
| if (ok) |
| *ok = false; |
| return 0; |
| } |
| if (parsedLength < valueString.length()) |
| reportViewportWarning(TruncatedViewportArgumentValueError, valueString, keyString); |
| if (ok) |
| *ok = true; |
| return value; |
| } |
| |
| Length HTMLMetaElement::parseViewportValueAsLength(const String& keyString, const String& valueString) |
| { |
| // 1) Non-negative number values are translated to px lengths. |
| // 2) Negative number values are translated to auto. |
| // 3) device-width and device-height are used as keywords. |
| // 4) Other keywords and unknown values translate to 0.0. |
| |
| unsigned length = valueString.length(); |
| DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); |
| SWITCH(characters, length) { |
| CASE("device-width") { |
| return Length(100, ViewportPercentageWidth); |
| } |
| CASE("device-height") { |
| return Length(100, ViewportPercentageHeight); |
| } |
| } |
| |
| float value = parsePositiveNumber(keyString, valueString); |
| |
| if (value < 0) |
| return Length(); // auto |
| |
| return Length(clampLengthValue(value), Fixed); |
| } |
| |
| float HTMLMetaElement::parseViewportValueAsZoom(const String& keyString, const String& valueString) |
| { |
| // 1) Non-negative number values are translated to <number> values. |
| // 2) Negative number values are translated to auto. |
| // 3) yes is translated to 1.0. |
| // 4) device-width and device-height are translated to 10.0. |
| // 5) no and unknown values are translated to 0.0 |
| |
| unsigned length = valueString.length(); |
| DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); |
| SWITCH(characters, length) { |
| CASE("yes") { |
| return 1; |
| } |
| CASE("no") { |
| return 0; |
| } |
| CASE("device-width") { |
| return 10; |
| } |
| CASE("device-height") { |
| return 10; |
| } |
| } |
| |
| float value = parsePositiveNumber(keyString, valueString); |
| |
| if (value < 0) |
| return ViewportDescription::ValueAuto; |
| |
| if (value > 10.0) |
| reportViewportWarning(MaximumScaleTooLargeError, String(), String()); |
| |
| if (!value && document().settings() && document().settings()->viewportMetaZeroValuesQuirk()) |
| return ViewportDescription::ValueAuto; |
| |
| return clampScaleValue(value); |
| } |
| |
| float HTMLMetaElement::parseViewportValueAsUserZoom(const String& keyString, const String& valueString) |
| { |
| // yes and no are used as keywords. |
| // Numbers >= 1, numbers <= -1, device-width and device-height are mapped to yes. |
| // Numbers in the range <-1, 1>, and unknown values, are mapped to no. |
| |
| unsigned length = valueString.length(); |
| DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13); |
| SWITCH(characters, length) { |
| CASE("yes") { |
| return 1; |
| } |
| CASE("no") { |
| return 0; |
| } |
| CASE("device-width") { |
| return 1; |
| } |
| CASE("device-height") { |
| return 1; |
| } |
| } |
| |
| float value = parsePositiveNumber(keyString, valueString); |
| if (fabs(value) < 1) |
| return 0; |
| |
| return 1; |
| } |
| |
| float HTMLMetaElement::parseViewportValueAsDPI(const String& keyString, const String& valueString) |
| { |
| unsigned length = valueString.length(); |
| DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 10); |
| SWITCH(characters, length) { |
| CASE("device-dpi") { |
| return ViewportDescription::ValueDeviceDPI; |
| } |
| CASE("low-dpi") { |
| return ViewportDescription::ValueLowDPI; |
| } |
| CASE("medium-dpi") { |
| return ViewportDescription::ValueMediumDPI; |
| } |
| CASE("high-dpi") { |
| return ViewportDescription::ValueHighDPI; |
| } |
| } |
| |
| bool ok; |
| float value = parsePositiveNumber(keyString, valueString, &ok); |
| if (!ok || value < 70 || value > 400) |
| return ViewportDescription::ValueAuto; |
| |
| return value; |
| } |
| |
| void HTMLMetaElement::processViewportKeyValuePair(const String& keyString, const String& valueString, void* data) |
| { |
| ViewportDescription* description = static_cast<ViewportDescription*>(data); |
| |
| unsigned length = keyString.length(); |
| |
| DEFINE_ARRAY_FOR_MATCHING(characters, keyString, 17); |
| SWITCH(characters, length) { |
| CASE("width") { |
| const Length& width = parseViewportValueAsLength(keyString, valueString); |
| if (width.isAuto()) |
| return; |
| description->minWidth = Length(ExtendToZoom); |
| description->maxWidth = width; |
| return; |
| } |
| CASE("height") { |
| const Length& height = parseViewportValueAsLength(keyString, valueString); |
| if (height.isAuto()) |
| return; |
| description->minHeight = Length(ExtendToZoom); |
| description->maxHeight = height; |
| return; |
| } |
| CASE("initial-scale") { |
| description->zoom = parseViewportValueAsZoom(keyString, valueString); |
| return; |
| } |
| CASE("minimum-scale") { |
| description->minZoom = parseViewportValueAsZoom(keyString, valueString); |
| return; |
| } |
| CASE("maximum-scale") { |
| description->maxZoom = parseViewportValueAsZoom(keyString, valueString); |
| return; |
| } |
| CASE("user-scalable") { |
| description->userZoom = parseViewportValueAsUserZoom(keyString, valueString); |
| return; |
| } |
| CASE("target-densitydpi") { |
| description->deprecatedTargetDensityDPI = parseViewportValueAsDPI(keyString, valueString); |
| reportViewportWarning(TargetDensityDpiUnsupported, String(), String()); |
| return; |
| } |
| } |
| reportViewportWarning(UnrecognizedViewportArgumentKeyError, keyString, String()); |
| } |
| |
| static const char* viewportErrorMessageTemplate(ViewportErrorCode errorCode) |
| { |
| static const char* const errors[] = { |
| "The key \"%replacement1\" is not recognized and ignored.", |
| "The value \"%replacement1\" for key \"%replacement2\" is invalid, and has been ignored.", |
| "The value \"%replacement1\" for key \"%replacement2\" was truncated to its numeric prefix.", |
| "The value for key \"maximum-scale\" is out of bounds and the value has been clamped.", |
| "The key \"target-densitydpi\" is not supported.", |
| }; |
| |
| return errors[errorCode]; |
| } |
| |
| static MessageLevel viewportErrorMessageLevel(ViewportErrorCode errorCode) |
| { |
| switch (errorCode) { |
| case TruncatedViewportArgumentValueError: |
| case TargetDensityDpiUnsupported: |
| return WarningMessageLevel; |
| case UnrecognizedViewportArgumentKeyError: |
| case UnrecognizedViewportArgumentValueError: |
| case MaximumScaleTooLargeError: |
| return ErrorMessageLevel; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return ErrorMessageLevel; |
| } |
| |
| void HTMLMetaElement::reportViewportWarning(ViewportErrorCode errorCode, const String& replacement1, const String& replacement2) |
| { |
| if (!document().frame()) |
| return; |
| |
| String message = viewportErrorMessageTemplate(errorCode); |
| if (!replacement1.isNull()) |
| message.replace("%replacement1", replacement1); |
| if (!replacement2.isNull()) |
| message.replace("%replacement2", replacement2); |
| |
| // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists. |
| document().addConsoleMessage(RenderingMessageSource, viewportErrorMessageLevel(errorCode), message); |
| } |
| |
| void HTMLMetaElement::processViewportContentAttribute(const String& content, ViewportDescription::Type origin) |
| { |
| ASSERT(!content.isNull()); |
| |
| if (!document().settings()) |
| return; |
| |
| if (!document().shouldOverrideLegacyDescription(origin)) |
| return; |
| |
| ViewportDescription descriptionFromLegacyTag(origin); |
| if (document().shouldMergeWithLegacyDescription(origin)) |
| descriptionFromLegacyTag = document().viewportDescription(); |
| |
| parseContentAttribute(content, &HTMLMetaElement::processViewportKeyValuePair, (void*)&descriptionFromLegacyTag); |
| |
| if (descriptionFromLegacyTag.minZoom == ViewportDescription::ValueAuto) |
| descriptionFromLegacyTag.minZoom = 0.25; |
| |
| if (descriptionFromLegacyTag.maxZoom == ViewportDescription::ValueAuto) { |
| descriptionFromLegacyTag.maxZoom = 5; |
| descriptionFromLegacyTag.minZoom = std::min(descriptionFromLegacyTag.minZoom, float(5)); |
| } |
| |
| const Settings* settings = document().settings(); |
| |
| if (descriptionFromLegacyTag.maxWidth.isAuto()) { |
| if (descriptionFromLegacyTag.zoom == ViewportDescription::ValueAuto) { |
| descriptionFromLegacyTag.minWidth = Length(ExtendToZoom); |
| descriptionFromLegacyTag.maxWidth = Length(settings->layoutFallbackWidth(), Fixed); |
| } else if (descriptionFromLegacyTag.maxHeight.isAuto()) { |
| descriptionFromLegacyTag.minWidth = Length(ExtendToZoom); |
| descriptionFromLegacyTag.maxWidth = Length(ExtendToZoom); |
| } |
| } |
| |
| document().setViewportDescription(descriptionFromLegacyTag); |
| } |
| |
| |
| void HTMLMetaElement::parseAttribute(const QualifiedName& name, const AtomicString& value) |
| { |
| if (name == http_equivAttr || name == contentAttr) { |
| process(); |
| return; |
| } |
| |
| if (name != nameAttr) |
| HTMLElement::parseAttribute(name, value); |
| } |
| |
| Node::InsertionNotificationRequest HTMLMetaElement::insertedInto(ContainerNode* insertionPoint) |
| { |
| HTMLElement::insertedInto(insertionPoint); |
| if (insertionPoint->inDocument()) |
| process(); |
| return InsertionDone; |
| } |
| |
| void HTMLMetaElement::process() |
| { |
| if (!inDocument()) |
| return; |
| |
| // All below situations require a content attribute (which can be the empty string). |
| const AtomicString& contentValue = fastGetAttribute(contentAttr); |
| if (contentValue.isNull()) |
| return; |
| |
| const AtomicString& nameValue = fastGetAttribute(nameAttr); |
| if (nameValue.isNull()) { |
| // Get the document to process the tag, but only if we're actually part of DOM |
| // tree (changing a meta tag while it's not in the tree shouldn't have any effect |
| // on the document). |
| const AtomicString& httpEquivValue = fastGetAttribute(http_equivAttr); |
| if (!httpEquivValue.isNull()) |
| document().processHttpEquiv(httpEquivValue, contentValue); |
| return; |
| } |
| |
| if (equalIgnoringCase(nameValue, "viewport")) |
| processViewportContentAttribute(contentValue, ViewportDescription::ViewportMeta); |
| else if (equalIgnoringCase(nameValue, "referrer")) |
| document().processReferrerPolicy(contentValue); |
| else if (equalIgnoringCase(nameValue, "handheldfriendly") && equalIgnoringCase(contentValue, "true")) |
| processViewportContentAttribute("width=device-width", ViewportDescription::HandheldFriendlyMeta); |
| else if (equalIgnoringCase(nameValue, "mobileoptimized")) |
| processViewportContentAttribute("width=device-width, initial-scale=1", ViewportDescription::MobileOptimizedMeta); |
| } |
| |
| const AtomicString& HTMLMetaElement::content() const |
| { |
| return getAttribute(contentAttr); |
| } |
| |
| const AtomicString& HTMLMetaElement::httpEquiv() const |
| { |
| return getAttribute(http_equivAttr); |
| } |
| |
| const AtomicString& HTMLMetaElement::name() const |
| { |
| return getNameAttribute(); |
| } |
| |
| } |