• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 "HTMLNames.h"
27 #include "core/dom/Document.h"
28 #include "core/frame/Settings.h"
29 
30 namespace WebCore {
31 
32 #define DEFINE_ARRAY_FOR_MATCHING(name, source, maxMatchLength) \
33 const UChar* name; \
34 const unsigned uMaxMatchLength = maxMatchLength; \
35 UChar characterBuffer[uMaxMatchLength]; \
36 if (!source.is8Bit()) { \
37     name = source.characters16(); \
38 } else { \
39     unsigned bufferLength = std::min(uMaxMatchLength, source.length()); \
40     const LChar* characters8 = source.characters8(); \
41     for (unsigned i = 0; i < bufferLength; ++i) \
42         characterBuffer[i] = characters8[i]; \
43     name = characterBuffer; \
44 }
45 
46 using namespace HTMLNames;
47 
HTMLMetaElement(Document & document)48 inline HTMLMetaElement::HTMLMetaElement(Document& document)
49     : HTMLElement(metaTag, document)
50 {
51     ScriptWrappable::init(this);
52 }
53 
create(Document & document)54 PassRefPtr<HTMLMetaElement> HTMLMetaElement::create(Document& document)
55 {
56     return adoptRef(new HTMLMetaElement(document));
57 }
58 
isInvalidSeparator(UChar c)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.
isSeparator(UChar c)65 static bool isSeparator(UChar c)
66 {
67     return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' || c == ',' || c == '\0';
68 }
69 
parseContentAttribute(const String & content,KeyValuePairCallback callback,void * data)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     int keyBegin, keyEnd;
76     int valueBegin, valueEnd;
77 
78     int i = 0;
79     int length = content.length();
80     String buffer = content.lower();
81     while (i < length) {
82         // skip to first non-separator, but don't skip past the end of the string
83         while (isSeparator(buffer[i])) {
84             if (i >= length)
85                 break;
86             i++;
87         }
88         keyBegin = i;
89 
90         // skip to first separator
91         while (!isSeparator(buffer[i])) {
92             error |= isInvalidSeparator(buffer[i]);
93             i++;
94         }
95         keyEnd = i;
96 
97         // skip to first '=', but don't skip past a ',' or the end of the string
98         while (buffer[i] != '=') {
99             error |= isInvalidSeparator(buffer[i]);
100             if (buffer[i] == ',' || i >= length)
101                 break;
102             i++;
103         }
104 
105         // skip to first non-separator, but don't skip past a ',' or the end of the string
106         while (isSeparator(buffer[i])) {
107             if (buffer[i] == ',' || i >= length)
108                 break;
109             i++;
110         }
111         valueBegin = i;
112 
113         // skip to first separator
114         while (!isSeparator(buffer[i])) {
115             error |= isInvalidSeparator(buffer[i]);
116             i++;
117         }
118         valueEnd = i;
119 
120         ASSERT_WITH_SECURITY_IMPLICATION(i <= length);
121 
122         String keyString = buffer.substring(keyBegin, keyEnd - keyBegin);
123         String valueString = buffer.substring(valueBegin, valueEnd - valueBegin);
124         (this->*callback)(keyString, valueString, data);
125     }
126     if (error) {
127         String message = "Error parsing a meta element's content: ';' is not a valid key-value pair separator. Please use ',' instead.";
128         document().addConsoleMessage(RenderingMessageSource, WarningMessageLevel, message);
129     }
130 }
131 
clampLengthValue(float value)132 static inline float clampLengthValue(float value)
133 {
134     // Limits as defined in the css-device-adapt spec.
135     if (value != ViewportDescription::ValueAuto)
136         return std::min(float(10000), std::max(value, float(1)));
137     return value;
138 }
139 
clampScaleValue(float value)140 static inline float clampScaleValue(float value)
141 {
142     // Limits as defined in the css-device-adapt spec.
143     if (value != ViewportDescription::ValueAuto)
144         return std::min(float(10), std::max(value, float(0.1)));
145     return value;
146 }
147 
parsePositiveNumber(const String & keyString,const String & valueString,bool * ok)148 float HTMLMetaElement::parsePositiveNumber(const String& keyString, const String& valueString, bool* ok)
149 {
150     size_t parsedLength;
151     float value;
152     if (valueString.is8Bit())
153         value = charactersToFloat(valueString.characters8(), valueString.length(), parsedLength);
154     else
155         value = charactersToFloat(valueString.characters16(), valueString.length(), parsedLength);
156     if (!parsedLength) {
157         reportViewportWarning(UnrecognizedViewportArgumentValueError, valueString, keyString);
158         if (ok)
159             *ok = false;
160         return 0;
161     }
162     if (parsedLength < valueString.length())
163         reportViewportWarning(TruncatedViewportArgumentValueError, valueString, keyString);
164     if (ok)
165         *ok = true;
166     return value;
167 }
168 
parseViewportValueAsLength(const String & keyString,const String & valueString)169 Length HTMLMetaElement::parseViewportValueAsLength(const String& keyString, const String& valueString)
170 {
171     // 1) Non-negative number values are translated to px lengths.
172     // 2) Negative number values are translated to auto.
173     // 3) device-width and device-height are used as keywords.
174     // 4) Other keywords and unknown values translate to 0.0.
175 
176     unsigned length = valueString.length();
177     DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13);
178     SWITCH(characters, length) {
179         CASE("device-width") {
180             return Length(100, ViewportPercentageWidth);
181         }
182         CASE("device-height") {
183             return Length(100, ViewportPercentageHeight);
184         }
185     }
186 
187     float value = parsePositiveNumber(keyString, valueString);
188 
189     if (value < 0)
190         return Length(); // auto
191 
192     return Length(clampLengthValue(value), Fixed);
193 }
194 
parseViewportValueAsZoom(const String & keyString,const String & valueString)195 float HTMLMetaElement::parseViewportValueAsZoom(const String& keyString, const String& valueString)
196 {
197     // 1) Non-negative number values are translated to <number> values.
198     // 2) Negative number values are translated to auto.
199     // 3) yes is translated to 1.0.
200     // 4) device-width and device-height are translated to 10.0.
201     // 5) no and unknown values are translated to 0.0
202 
203     unsigned length = valueString.length();
204     DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13);
205     SWITCH(characters, length) {
206         CASE("yes") {
207             return 1;
208         }
209         CASE("no") {
210             return 0;
211         }
212         CASE("device-width") {
213             return 10;
214         }
215         CASE("device-height") {
216             return 10;
217         }
218     }
219 
220     float value = parsePositiveNumber(keyString, valueString);
221 
222     if (value < 0)
223         return ViewportDescription::ValueAuto;
224 
225     if (value > 10.0)
226         reportViewportWarning(MaximumScaleTooLargeError, String(), String());
227 
228     if (!value && document().settings() && document().settings()->viewportMetaZeroValuesQuirk())
229         return ViewportDescription::ValueAuto;
230 
231     return clampScaleValue(value);
232 }
233 
parseViewportValueAsUserZoom(const String & keyString,const String & valueString)234 float HTMLMetaElement::parseViewportValueAsUserZoom(const String& keyString, const String& valueString)
235 {
236     // yes and no are used as keywords.
237     // Numbers >= 1, numbers <= -1, device-width and device-height are mapped to yes.
238     // Numbers in the range <-1, 1>, and unknown values, are mapped to no.
239 
240     unsigned length = valueString.length();
241     DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 13);
242     SWITCH(characters, length) {
243         CASE("yes") {
244             return 1;
245         }
246         CASE("no") {
247             return 0;
248         }
249         CASE("device-width") {
250             return 1;
251         }
252         CASE("device-height") {
253             return 1;
254         }
255     }
256 
257     float value = parsePositiveNumber(keyString, valueString);
258     if (fabs(value) < 1)
259         return 0;
260 
261     return 1;
262 }
263 
parseViewportValueAsDPI(const String & keyString,const String & valueString)264 float HTMLMetaElement::parseViewportValueAsDPI(const String& keyString, const String& valueString)
265 {
266     unsigned length = valueString.length();
267     DEFINE_ARRAY_FOR_MATCHING(characters, valueString, 10);
268     SWITCH(characters, length) {
269         CASE("device-dpi") {
270             return ViewportDescription::ValueDeviceDPI;
271         }
272         CASE("low-dpi") {
273             return ViewportDescription::ValueLowDPI;
274         }
275         CASE("medium-dpi") {
276             return ViewportDescription::ValueMediumDPI;
277         }
278         CASE("high-dpi") {
279             return ViewportDescription::ValueHighDPI;
280         }
281     }
282 
283     bool ok;
284     float value = parsePositiveNumber(keyString, valueString, &ok);
285     if (!ok || value < 70 || value > 400)
286         return ViewportDescription::ValueAuto;
287 
288     return value;
289 }
290 
processViewportKeyValuePair(const String & keyString,const String & valueString,void * data)291 void HTMLMetaElement::processViewportKeyValuePair(const String& keyString, const String& valueString, void* data)
292 {
293     ViewportDescription* description = static_cast<ViewportDescription*>(data);
294 
295     unsigned length = keyString.length();
296 
297     DEFINE_ARRAY_FOR_MATCHING(characters, keyString, 17);
298     SWITCH(characters, length) {
299         CASE("width") {
300             const Length& width = parseViewportValueAsLength(keyString, valueString);
301             if (width.isAuto())
302                 return;
303             description->minWidth = Length(ExtendToZoom);
304             description->maxWidth = width;
305             return;
306         }
307         CASE("height") {
308             const Length& height = parseViewportValueAsLength(keyString, valueString);
309             if (height.isAuto())
310                 return;
311             description->minHeight = Length(ExtendToZoom);
312             description->maxHeight = height;
313             return;
314         }
315         CASE("initial-scale") {
316             description->zoom = parseViewportValueAsZoom(keyString, valueString);
317             return;
318         }
319         CASE("minimum-scale") {
320             description->minZoom = parseViewportValueAsZoom(keyString, valueString);
321             return;
322         }
323         CASE("maximum-scale") {
324             description->maxZoom = parseViewportValueAsZoom(keyString, valueString);
325             return;
326         }
327         CASE("user-scalable") {
328             description->userZoom = parseViewportValueAsUserZoom(keyString, valueString);
329             return;
330         }
331         CASE("target-densitydpi") {
332             description->deprecatedTargetDensityDPI = parseViewportValueAsDPI(keyString, valueString);
333             reportViewportWarning(TargetDensityDpiUnsupported, String(), String());
334             return;
335         }
336     }
337     reportViewportWarning(UnrecognizedViewportArgumentKeyError, keyString, String());
338 }
339 
viewportErrorMessageTemplate(ViewportErrorCode errorCode)340 static const char* viewportErrorMessageTemplate(ViewportErrorCode errorCode)
341 {
342     static const char* const errors[] = {
343         "The key \"%replacement1\" is not recognized and ignored.",
344         "The value \"%replacement1\" for key \"%replacement2\" is invalid, and has been ignored.",
345         "The value \"%replacement1\" for key \"%replacement2\" was truncated to its numeric prefix.",
346         "The value for key \"maximum-scale\" is out of bounds and the value has been clamped.",
347         "The key \"target-densitydpi\" is not supported.",
348     };
349 
350     return errors[errorCode];
351 }
352 
viewportErrorMessageLevel(ViewportErrorCode errorCode)353 static MessageLevel viewportErrorMessageLevel(ViewportErrorCode errorCode)
354 {
355     switch (errorCode) {
356     case TruncatedViewportArgumentValueError:
357     case TargetDensityDpiUnsupported:
358         return WarningMessageLevel;
359     case UnrecognizedViewportArgumentKeyError:
360     case UnrecognizedViewportArgumentValueError:
361     case MaximumScaleTooLargeError:
362         return ErrorMessageLevel;
363     }
364 
365     ASSERT_NOT_REACHED();
366     return ErrorMessageLevel;
367 }
368 
reportViewportWarning(ViewportErrorCode errorCode,const String & replacement1,const String & replacement2)369 void HTMLMetaElement::reportViewportWarning(ViewportErrorCode errorCode, const String& replacement1, const String& replacement2)
370 {
371     if (!document().frame())
372         return;
373 
374     String message = viewportErrorMessageTemplate(errorCode);
375     if (!replacement1.isNull())
376         message.replace("%replacement1", replacement1);
377     if (!replacement2.isNull())
378         message.replace("%replacement2", replacement2);
379 
380     // FIXME: This message should be moved off the console once a solution to https://bugs.webkit.org/show_bug.cgi?id=103274 exists.
381     document().addConsoleMessage(RenderingMessageSource, viewportErrorMessageLevel(errorCode), message);
382 }
383 
processViewportContentAttribute(const String & content,ViewportDescription::Type origin)384 void HTMLMetaElement::processViewportContentAttribute(const String& content, ViewportDescription::Type origin)
385 {
386     ASSERT(!content.isNull());
387 
388     if (!document().settings())
389         return;
390 
391     if (!document().shouldOverrideLegacyDescription(origin))
392         return;
393 
394     ViewportDescription descriptionFromLegacyTag(origin);
395     if (document().shouldMergeWithLegacyDescription(origin))
396         descriptionFromLegacyTag = document().viewportDescription();
397 
398     parseContentAttribute(content, &HTMLMetaElement::processViewportKeyValuePair, (void*)&descriptionFromLegacyTag);
399 
400     if (descriptionFromLegacyTag.minZoom == ViewportDescription::ValueAuto)
401         descriptionFromLegacyTag.minZoom = 0.25;
402 
403     if (descriptionFromLegacyTag.maxZoom == ViewportDescription::ValueAuto) {
404         descriptionFromLegacyTag.maxZoom = 5;
405         descriptionFromLegacyTag.minZoom = std::min(descriptionFromLegacyTag.minZoom, float(5));
406     }
407 
408     const Settings* settings = document().settings();
409 
410     if (descriptionFromLegacyTag.maxWidth.isAuto()) {
411         if (descriptionFromLegacyTag.zoom == ViewportDescription::ValueAuto) {
412             descriptionFromLegacyTag.minWidth = Length(ExtendToZoom);
413             descriptionFromLegacyTag.maxWidth = Length(settings->layoutFallbackWidth(), Fixed);
414         } else if (descriptionFromLegacyTag.maxHeight.isAuto()) {
415             descriptionFromLegacyTag.minWidth = Length(ExtendToZoom);
416             descriptionFromLegacyTag.maxWidth = Length(ExtendToZoom);
417         }
418     }
419 
420     document().setViewportDescription(descriptionFromLegacyTag);
421 }
422 
423 
parseAttribute(const QualifiedName & name,const AtomicString & value)424 void HTMLMetaElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
425 {
426     if (name == http_equivAttr || name == contentAttr) {
427         process();
428         return;
429     }
430 
431     if (name != nameAttr)
432         HTMLElement::parseAttribute(name, value);
433 }
434 
insertedInto(ContainerNode * insertionPoint)435 Node::InsertionNotificationRequest HTMLMetaElement::insertedInto(ContainerNode* insertionPoint)
436 {
437     HTMLElement::insertedInto(insertionPoint);
438     if (insertionPoint->inDocument())
439         process();
440     return InsertionDone;
441 }
442 
process()443 void HTMLMetaElement::process()
444 {
445     if (!inDocument())
446         return;
447 
448     // All below situations require a content attribute (which can be the empty string).
449     const AtomicString& contentValue = fastGetAttribute(contentAttr);
450     if (contentValue.isNull())
451         return;
452 
453     const AtomicString& nameValue = fastGetAttribute(nameAttr);
454     if (nameValue.isNull()) {
455         // Get the document to process the tag, but only if we're actually part of DOM
456         // tree (changing a meta tag while it's not in the tree shouldn't have any effect
457         // on the document).
458         const AtomicString& httpEquivValue = fastGetAttribute(http_equivAttr);
459         if (!httpEquivValue.isNull())
460             document().processHttpEquiv(httpEquivValue, contentValue);
461         return;
462     }
463 
464     if (equalIgnoringCase(nameValue, "viewport"))
465         processViewportContentAttribute(contentValue, ViewportDescription::ViewportMeta);
466     else if (equalIgnoringCase(nameValue, "referrer"))
467         document().processReferrerPolicy(contentValue);
468     else if (equalIgnoringCase(nameValue, "handheldfriendly") && equalIgnoringCase(contentValue, "true"))
469         processViewportContentAttribute("width=device-width", ViewportDescription::HandheldFriendlyMeta);
470     else if (equalIgnoringCase(nameValue, "mobileoptimized"))
471         processViewportContentAttribute("width=device-width, initial-scale=1", ViewportDescription::MobileOptimizedMeta);
472 }
473 
content() const474 const AtomicString& HTMLMetaElement::content() const
475 {
476     return getAttribute(contentAttr);
477 }
478 
httpEquiv() const479 const AtomicString& HTMLMetaElement::httpEquiv() const
480 {
481     return getAttribute(http_equivAttr);
482 }
483 
name() const484 const AtomicString& HTMLMetaElement::name() const
485 {
486     return getNameAttribute();
487 }
488 
489 }
490