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
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
HTMLMetaElement(Document & document)54 inline HTMLMetaElement::HTMLMetaElement(Document& document)
55 : HTMLElement(metaTag, document)
56 {
57 }
58
DEFINE_NODE_FACTORY(HTMLMetaElement)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.
isSeparator(UChar c)67 static bool isSeparator(UChar c)
68 {
69 return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '=' || c == ',' || c == '\0';
70 }
71
parseContentAttribute(const String & content,KeyValuePairCallback callback,void * data)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
clampLengthValue(float value)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
clampScaleValue(float value)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
parsePositiveNumber(const String & keyString,const String & valueString,bool * ok)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
parseViewportValueAsLength(const String & keyString,const String & valueString)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
parseViewportValueAsZoom(const String & keyString,const String & valueString,bool & computedValueMatchesParsedValue)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
parseViewportValueAsUserZoom(const String & keyString,const String & valueString,bool & computedValueMatchesParsedValue)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
parseViewportValueAsDPI(const String & keyString,const String & valueString)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
processViewportKeyValuePair(const String & keyString,const String & valueString,void * data)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
viewportErrorMessageTemplate(ViewportErrorCode errorCode)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
viewportErrorMessageLevel(ViewportErrorCode errorCode)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
reportViewportWarning(ViewportErrorCode errorCode,const String & replacement1,const String & replacement2)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
processViewportContentAttribute(const String & content,ViewportDescription::Type origin)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
parseAttribute(const QualifiedName & name,const AtomicString & value)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
insertedInto(ContainerNode * insertionPoint)436 Node::InsertionNotificationRequest HTMLMetaElement::insertedInto(ContainerNode* insertionPoint)
437 {
438 HTMLElement::insertedInto(insertionPoint);
439 return InsertionShouldCallDidNotifySubtreeInsertions;
440 }
441
didNotifySubtreeInsertionsToDocument()442 void HTMLMetaElement::didNotifySubtreeInsertionsToDocument()
443 {
444 process();
445 }
446
inDocumentHead(HTMLMetaElement * element)447 static bool inDocumentHead(HTMLMetaElement* element)
448 {
449 if (!element->inDocument())
450 return false;
451
452 return Traversal<HTMLHeadElement>::firstAncestor(*element);
453 }
454
process()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
content() const488 const AtomicString& HTMLMetaElement::content() const
489 {
490 return getAttribute(contentAttr);
491 }
492
httpEquiv() const493 const AtomicString& HTMLMetaElement::httpEquiv() const
494 {
495 return getAttribute(http_equivAttr);
496 }
497
name() const498 const AtomicString& HTMLMetaElement::name() const
499 {
500 return getNameAttribute();
501 }
502
503 }
504