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/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 
     36 namespace blink {
     37 
     38 #define DEFINE_ARRAY_FOR_MATCHING(name, source, maxMatchLength) \
     39 const UChar* name; \
     40 const unsigned uMaxMatchLength = maxMatchLength; \
     41 UChar characterBuffer[uMaxMatchLength]; \
     42 if (!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 
     52 using namespace HTMLNames;
     53 
     54 inline HTMLMetaElement::HTMLMetaElement(Document& document)
     55     : HTMLElement(metaTag, document)
     56 {
     57 }
     58 
     59 DEFINE_NODE_FACTORY(HTMLMetaElement)
     60 
     61 static bool isInvalidSeparator(UChar c)
     62 {
     63     return c == ';';
     64 }
     65 
     66 // Though isspace() considers \t and \v to be whitespace, Win IE doesn't.
     67 static bool isSeparator(UChar c)
     68 {
     69     return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' || c == ',' || c == '\0';
     70 }
     71 
     72 void 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 
    137 static 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 
    145 static 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 
    153 float 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 
    174 Length 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 
    200 float 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 
    244 bool 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 
    277 float 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 
    304 void 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 
    357 static 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 
    370 static 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 
    385 void 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 
    400 void 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 
    425 void 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 
    436 Node::InsertionNotificationRequest HTMLMetaElement::insertedInto(ContainerNode* insertionPoint)
    437 {
    438     HTMLElement::insertedInto(insertionPoint);
    439     return InsertionShouldCallDidNotifySubtreeInsertions;
    440 }
    441 
    442 void HTMLMetaElement::didNotifySubtreeInsertionsToDocument()
    443 {
    444     process();
    445 }
    446 
    447 static bool inDocumentHead(HTMLMetaElement* element)
    448 {
    449     if (!element->inDocument())
    450         return false;
    451 
    452     return Traversal<HTMLHeadElement>::firstAncestor(*element);
    453 }
    454 
    455 void 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 
    488 const AtomicString& HTMLMetaElement::content() const
    489 {
    490     return getAttribute(contentAttr);
    491 }
    492 
    493 const AtomicString& HTMLMetaElement::httpEquiv() const
    494 {
    495     return getAttribute(http_equivAttr);
    496 }
    497 
    498 const AtomicString& HTMLMetaElement::name() const
    499 {
    500     return getNameAttribute();
    501 }
    502 
    503 }
    504