1/*
2 * Copyright (C) 2000 Harri Porten (porten@kde.org)
3 * Copyright (C) 2006 Jon Shier (jshier@iastate.edu)
4 * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2010 Apple Inc. All rights reseved.
5 * Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser 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 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this library; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
20 * USA
21 */
22
23#include "config.h"
24#include "WindowFeatures.h"
25
26#include "FloatRect.h"
27#include <wtf/ASCIICType.h>
28#include <wtf/Assertions.h>
29#include <wtf/HashMap.h>
30#include <wtf/MathExtras.h>
31#include <wtf/text/StringHash.h>
32
33namespace WebCore {
34
35typedef HashMap<String, String, ASCIICaseInsensitiveHash> DialogFeaturesMap;
36
37static void setWindowFeature(WindowFeatures&, StringView key, StringView value);
38
39static DialogFeaturesMap parseDialogFeaturesMap(const String&);
40static Optional<bool> boolFeature(const DialogFeaturesMap&, const char* key);
41static Optional<float> floatFeature(const DialogFeaturesMap&, const char* key, float min, float max);
42
43// https://html.spec.whatwg.org/#feature-separator
44static bool isSeparator(UChar character, FeatureMode mode)
45{
46 if (mode == FeatureMode::Viewport)
47 return character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == '=' || character == ',';
48
49 return isASCIISpace(character) || character == '=' || character == ',';
50}
51
52WindowFeatures parseWindowFeatures(StringView featuresString)
53{
54 // The IE rule is: all features except for channelmode and fullscreen default to YES, but
55 // if the user specifies a feature string, all features default to NO. (There is no public
56 // standard that applies to this method.)
57 //
58 // <http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/open_0.asp>
59 // We always allow a window to be resized, which is consistent with Firefox.
60
61 WindowFeatures features;
62
63 if (featuresString.isEmpty())
64 return features;
65
66 features.menuBarVisible = false;
67 features.statusBarVisible = false;
68 features.toolBarVisible = false;
69 features.locationBarVisible = false;
70 features.scrollbarsVisible = false;
71 features.noopener = false;
72
73 processFeaturesString(featuresString, FeatureMode::Window, [&features](StringView key, StringView value) {
74 setWindowFeature(features, key, value);
75 });
76
77 return features;
78}
79
80// Window: https://html.spec.whatwg.org/#concept-window-open-features-tokenize
81// Viewport: https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW6
82// FIXME: We should considering aligning Viewport feature parsing with Window features parsing.
83void processFeaturesString(StringView features, FeatureMode mode, const WTF::Function<void(StringView type, StringView value)>& callback)
84{
85 unsigned length = features.length();
86 for (unsigned i = 0; i < length; ) {
87 // Skip to first non-separator.
88 while (i < length && isSeparator(features[i], mode))
89 ++i;
90 unsigned keyBegin = i;
91
92 // Skip to first separator.
93 while (i < length && !isSeparator(features[i], mode))
94 i++;
95 unsigned keyEnd = i;
96
97 // Skip to first '=', but don't skip past a ',' or a non-separator.
98 while (i < length && features[i] != '=' && features[i] != ',' && (mode == FeatureMode::Viewport || isSeparator(features[i], mode)))
99 ++i;
100
101 // Skip to first non-separator, but don't skip past a ','.
102 if (mode == FeatureMode::Viewport || (i < length && isSeparator(features[i], mode))) {
103 while (i < length && isSeparator(features[i], mode) && features[i] != ',')
104 ++i;
105 unsigned valueBegin = i;
106
107 // Skip to first separator.
108 while (i < length && !isSeparator(features[i], mode))
109 ++i;
110 unsigned valueEnd = i;
111 callback(features.substring(keyBegin, keyEnd - keyBegin), features.substring(valueBegin, valueEnd - valueBegin));
112 } else
113 callback(features.substring(keyBegin, keyEnd - keyBegin), StringView());
114 }
115}
116
117OptionSet<DisabledAdaptations> parseDisabledAdaptations(const String& disabledAdaptationsString)
118{
119 OptionSet<DisabledAdaptations> disabledAdaptations;
120 for (auto& name : disabledAdaptationsString.split(',')) {
121 auto normalizedName = name.stripWhiteSpace().convertToASCIILowercase();
122 if (normalizedName == watchAdaptationName())
123 disabledAdaptations.add(DisabledAdaptations::Watch);
124 }
125 return disabledAdaptations;
126}
127
128static void setWindowFeature(WindowFeatures& features, StringView key, StringView value)
129{
130 // Listing a key with no value is shorthand for key=yes
131 int numericValue;
132 if (value.isEmpty() || equalLettersIgnoringASCIICase(value, "yes"))
133 numericValue = 1;
134 else
135 numericValue = value.toInt();
136
137 // We treat key of "resizable" here as an additional feature rather than setting resizeable to true.
138 // This is consistent with Firefox, but could also be handled at another level.
139
140 if (equalLettersIgnoringASCIICase(key, "left") || equalLettersIgnoringASCIICase(key, "screenx"))
141 features.x = numericValue;
142 else if (equalLettersIgnoringASCIICase(key, "top") || equalLettersIgnoringASCIICase(key, "screeny"))
143 features.y = numericValue;
144 else if (equalLettersIgnoringASCIICase(key, "width") || equalLettersIgnoringASCIICase(key, "innerwidth"))
145 features.width = numericValue;
146 else if (equalLettersIgnoringASCIICase(key, "height") || equalLettersIgnoringASCIICase(key, "innerheight"))
147 features.height = numericValue;
148 else if (equalLettersIgnoringASCIICase(key, "menubar"))
149 features.menuBarVisible = numericValue;
150 else if (equalLettersIgnoringASCIICase(key, "toolbar"))
151 features.toolBarVisible = numericValue;
152 else if (equalLettersIgnoringASCIICase(key, "location"))
153 features.locationBarVisible = numericValue;
154 else if (equalLettersIgnoringASCIICase(key, "status"))
155 features.statusBarVisible = numericValue;
156 else if (equalLettersIgnoringASCIICase(key, "fullscreen"))
157 features.fullscreen = numericValue;
158 else if (equalLettersIgnoringASCIICase(key, "scrollbars"))
159 features.scrollbarsVisible = numericValue;
160 else if (equalLettersIgnoringASCIICase(key, "noopener"))
161 features.noopener = numericValue;
162 else if (equalLettersIgnoringASCIICase(key, "noreferrer"))
163 features.noreferrer = numericValue;
164 else if (numericValue == 1)
165 features.additionalFeatures.append(key.toString());
166}
167
168WindowFeatures parseDialogFeatures(const String& dialogFeaturesString, const FloatRect& screenAvailableRect)
169{
170 auto featuresMap = parseDialogFeaturesMap(dialogFeaturesString);
171
172 // The following features from Microsoft's documentation are not implemented:
173 // - default font settings
174 // - width, height, left, and top specified in units other than "px"
175 // - edge (sunken or raised, default is raised)
176 // - dialogHide: trusted && boolFeature(features, "dialoghide"), makes dialog hide when you print
177 // - help: boolFeature(features, "help", true), makes help icon appear in dialog (what does it do on Windows?)
178 // - unadorned: trusted && boolFeature(features, "unadorned");
179
180 WindowFeatures features;
181
182 features.menuBarVisible = false;
183 features.toolBarVisible = false;
184 features.locationBarVisible = false;
185 features.dialog = true;
186
187 float width = floatFeature(featuresMap, "dialogwidth", 100, screenAvailableRect.width()).valueOr(620); // default here came from frame size of dialog in MacIE
188 float height = floatFeature(featuresMap, "dialogheight", 100, screenAvailableRect.height()).valueOr(450); // default here came from frame size of dialog in MacIE
189
190 features.width = width;
191 features.height = height;
192
193 features.x = floatFeature(featuresMap, "dialogleft", screenAvailableRect.x(), screenAvailableRect.maxX() - width);
194 features.y = floatFeature(featuresMap, "dialogtop", screenAvailableRect.y(), screenAvailableRect.maxY() - height);
195
196 if (boolFeature(featuresMap, "center").valueOr(true)) {
197 if (!features.x)
198 features.x = screenAvailableRect.x() + (screenAvailableRect.width() - width) / 2;
199 if (!features.y)
200 features.y = screenAvailableRect.y() + (screenAvailableRect.height() - height) / 2;
201 }
202
203 features.resizable = boolFeature(featuresMap, "resizable").valueOr(false);
204 features.scrollbarsVisible = boolFeature(featuresMap, "scroll").valueOr(true);
205 features.statusBarVisible = boolFeature(featuresMap, "status").valueOr(false);
206
207 return features;
208}
209
210static Optional<bool> boolFeature(const DialogFeaturesMap& features, const char* key)
211{
212 auto it = features.find(key);
213 if (it == features.end())
214 return WTF::nullopt;
215
216 auto& value = it->value;
217 return value.isNull()
218 || value == "1"
219 || equalLettersIgnoringASCIICase(value, "yes")
220 || equalLettersIgnoringASCIICase(value, "on");
221}
222
223static Optional<float> floatFeature(const DialogFeaturesMap& features, const char* key, float min, float max)
224{
225 auto it = features.find(key);
226 if (it == features.end())
227 return WTF::nullopt;
228
229 // FIXME: The toDouble function does not offer a way to tell "0q" from string with no digits in it: Both
230 // return the number 0 and false for ok. But "0q" should yield the minimum rather than the default.
231 bool ok;
232 double parsedNumber = it->value.toDouble(&ok);
233 if ((!parsedNumber && !ok) || std::isnan(parsedNumber))
234 return WTF::nullopt;
235 if (parsedNumber < min || max <= min)
236 return min;
237 if (parsedNumber > max)
238 return max;
239
240 // FIXME: Seems strange to cast a double to int and then convert back to a float. Why is this a good idea?
241 return static_cast<int>(parsedNumber);
242}
243
244static DialogFeaturesMap parseDialogFeaturesMap(const String& string)
245{
246 // FIXME: Not clear why we take such a different approach to parsing dialog features
247 // as opposed to window features (using a map, different parsing quirks).
248
249 DialogFeaturesMap features;
250
251 for (auto& featureString : string.split(';')) {
252 size_t separatorPosition = featureString.find('=');
253 size_t colonPosition = featureString.find(':');
254 if (separatorPosition != notFound && colonPosition != notFound)
255 continue; // ignore strings that have both = and :
256 if (separatorPosition == notFound)
257 separatorPosition = colonPosition;
258
259 String key = featureString.left(separatorPosition).stripWhiteSpace();
260
261 // Null string for value indicates key without value.
262 String value;
263 if (separatorPosition != notFound) {
264 value = featureString.substring(separatorPosition + 1).stripWhiteSpace();
265 value = value.left(value.find(' '));
266 }
267
268 features.set(key, value);
269 }
270
271 return features;
272}
273
274} // namespace WebCore
275