Home | History | Annotate | Download | only in html
      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