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