blob: c6347df23bfa2b2aea9e0797444fdc2f22f1389e [file] [log] [blame]
/*
* 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();
}
}