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