blob: 61983ac2b35afff46b6a6de3fd8ca05b1f9bf48b [file] [log] [blame]
/*
* Copyright (C) 2000 Harri Porten (porten@kde.org)
* Copyright (C) 2006 Jon Shier (jshier@iastate.edu)
* Copyright (C) 2003, 2004, 2005, 2006, 2007, 2010 Apple Inc. All rights reseved.
* Copyright (C) 2006 Alexey Proskuryakov (ap@webkit.org)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
#include "config.h"
#include "WindowFeatures.h"
#include "FloatRect.h"
#include <wtf/ASCIICType.h>
#include <wtf/Assertions.h>
#include <wtf/HashMap.h>
#include <wtf/MathExtras.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/StringToIntegerConversion.h>
namespace WebCore {
typedef HashMap<String, String, ASCIICaseInsensitiveHash> DialogFeaturesMap;
static void setWindowFeature(WindowFeatures&, StringView key, StringView value);
static DialogFeaturesMap parseDialogFeaturesMap(StringView);
static std::optional<bool> boolFeature(const DialogFeaturesMap&, ASCIILiteral key);
static std::optional<float> floatFeature(const DialogFeaturesMap&, ASCIILiteral key, float min, float max);
// https://html.spec.whatwg.org/#feature-separator
static bool isSeparator(UChar character, FeatureMode mode)
{
if (mode == FeatureMode::Viewport)
return character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == '=' || character == ',';
return isASCIISpace(character) || character == '=' || character == ',';
}
WindowFeatures parseWindowFeatures(StringView featuresString)
{
// The IE rule is: all features except for channelmode and fullscreen default to YES, but
// if the user specifies a feature string, all features default to NO. (There is no public
// standard that applies to this method.)
//
// <http://msdn.microsoft.com/workshop/author/dhtml/reference/methods/open_0.asp>
// We always allow a window to be resized, which is consistent with Firefox.
WindowFeatures features;
if (featuresString.isEmpty())
return features;
features.menuBarVisible = false;
features.statusBarVisible = false;
features.toolBarVisible = false;
features.locationBarVisible = false;
features.scrollbarsVisible = false;
features.noopener = false;
processFeaturesString(featuresString, FeatureMode::Window, [&features](StringView key, StringView value) {
setWindowFeature(features, key, value);
});
return features;
}
// Window: https://html.spec.whatwg.org/#concept-window-open-features-tokenize
// Viewport: https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW6
// FIXME: We should considering aligning Viewport feature parsing with Window features parsing.
void processFeaturesString(StringView features, FeatureMode mode, const Function<void(StringView type, StringView value)>& callback)
{
unsigned length = features.length();
for (unsigned i = 0; i < length; ) {
// Skip to first non-separator.
while (i < length && isSeparator(features[i], mode))
++i;
unsigned keyBegin = i;
// Skip to first separator.
while (i < length && !isSeparator(features[i], mode))
i++;
unsigned keyEnd = i;
// Skip to first '=', but don't skip past a ',' or a non-separator.
while (i < length && features[i] != '=' && features[i] != ',' && (mode == FeatureMode::Viewport || isSeparator(features[i], mode)))
++i;
// Skip to first non-separator, but don't skip past a ','.
if (mode == FeatureMode::Viewport || (i < length && isSeparator(features[i], mode))) {
while (i < length && isSeparator(features[i], mode) && features[i] != ',')
++i;
unsigned valueBegin = i;
// Skip to first separator.
while (i < length && !isSeparator(features[i], mode))
++i;
unsigned valueEnd = i;
callback(features.substring(keyBegin, keyEnd - keyBegin), features.substring(valueBegin, valueEnd - valueBegin));
} else
callback(features.substring(keyBegin, keyEnd - keyBegin), StringView());
}
}
OptionSet<DisabledAdaptations> parseDisabledAdaptations(StringView disabledAdaptationsString)
{
OptionSet<DisabledAdaptations> disabledAdaptations;
for (auto name : disabledAdaptationsString.split(',')) {
auto trimmedName = name.stripWhiteSpace();
if (equalIgnoringASCIICase(trimmedName, watchAdaptationName()))
disabledAdaptations.add(DisabledAdaptations::Watch);
}
return disabledAdaptations;
}
static void setWindowFeature(WindowFeatures& features, StringView key, StringView value)
{
// Listing a key with no value is shorthand for key=yes
int numericValue;
if (value.isEmpty() || equalLettersIgnoringASCIICase(value, "yes"_s) || equalLettersIgnoringASCIICase(value, "true"_s))
numericValue = 1;
else
numericValue = parseIntegerAllowingTrailingJunk<int>(value).value_or(0);
// We treat key of "resizable" here as an additional feature rather than setting resizeable to true.
// This is consistent with Firefox, but could also be handled at another level.
if (equalLettersIgnoringASCIICase(key, "left"_s) || equalLettersIgnoringASCIICase(key, "screenx"_s))
features.x = numericValue;
else if (equalLettersIgnoringASCIICase(key, "top"_s) || equalLettersIgnoringASCIICase(key, "screeny"_s))
features.y = numericValue;
else if (equalLettersIgnoringASCIICase(key, "width"_s) || equalLettersIgnoringASCIICase(key, "innerwidth"_s))
features.width = numericValue;
else if (equalLettersIgnoringASCIICase(key, "height"_s) || equalLettersIgnoringASCIICase(key, "innerheight"_s))
features.height = numericValue;
else if (equalLettersIgnoringASCIICase(key, "menubar"_s))
features.menuBarVisible = numericValue;
else if (equalLettersIgnoringASCIICase(key, "toolbar"_s))
features.toolBarVisible = numericValue;
else if (equalLettersIgnoringASCIICase(key, "location"_s))
features.locationBarVisible = numericValue;
else if (equalLettersIgnoringASCIICase(key, "status"_s))
features.statusBarVisible = numericValue;
else if (equalLettersIgnoringASCIICase(key, "fullscreen"_s))
features.fullscreen = numericValue;
else if (equalLettersIgnoringASCIICase(key, "scrollbars"_s))
features.scrollbarsVisible = numericValue;
else if (equalLettersIgnoringASCIICase(key, "noopener"_s))
features.noopener = numericValue;
else if (equalLettersIgnoringASCIICase(key, "noreferrer"_s))
features.noreferrer = numericValue;
else if (numericValue == 1)
features.additionalFeatures.append(key.toString());
}
WindowFeatures parseDialogFeatures(StringView dialogFeaturesString, const FloatRect& screenAvailableRect)
{
auto featuresMap = parseDialogFeaturesMap(dialogFeaturesString);
// The following features from Microsoft's documentation are not implemented:
// - default font settings
// - width, height, left, and top specified in units other than "px"
// - edge (sunken or raised, default is raised)
// - dialogHide: trusted && boolFeature(features, "dialoghide"), makes dialog hide when you print
// - help: boolFeature(features, "help", true), makes help icon appear in dialog (what does it do on Windows?)
// - unadorned: trusted && boolFeature(features, "unadorned");
WindowFeatures features;
features.menuBarVisible = false;
features.toolBarVisible = false;
features.locationBarVisible = false;
features.dialog = true;
float width = floatFeature(featuresMap, "dialogwidth"_s, 100, screenAvailableRect.width()).value_or(620); // default here came from frame size of dialog in MacIE
float height = floatFeature(featuresMap, "dialogheight"_s, 100, screenAvailableRect.height()).value_or(450); // default here came from frame size of dialog in MacIE
features.width = width;
features.height = height;
features.x = floatFeature(featuresMap, "dialogleft"_s, screenAvailableRect.x(), screenAvailableRect.maxX() - width);
features.y = floatFeature(featuresMap, "dialogtop"_s, screenAvailableRect.y(), screenAvailableRect.maxY() - height);
if (boolFeature(featuresMap, "center"_s).value_or(true)) {
if (!features.x)
features.x = screenAvailableRect.x() + (screenAvailableRect.width() - width) / 2;
if (!features.y)
features.y = screenAvailableRect.y() + (screenAvailableRect.height() - height) / 2;
}
features.resizable = boolFeature(featuresMap, "resizable"_s).value_or(false);
features.scrollbarsVisible = boolFeature(featuresMap, "scroll"_s).value_or(true);
features.statusBarVisible = boolFeature(featuresMap, "status"_s).value_or(false);
return features;
}
static std::optional<bool> boolFeature(const DialogFeaturesMap& features, ASCIILiteral key)
{
auto it = features.find(key);
if (it == features.end())
return std::nullopt;
auto& value = it->value;
return value.isNull()
|| value == "1"_s
|| equalLettersIgnoringASCIICase(value, "yes"_s)
|| equalLettersIgnoringASCIICase(value, "on"_s);
}
static std::optional<float> floatFeature(const DialogFeaturesMap& features, ASCIILiteral key, float min, float max)
{
auto it = features.find(key);
if (it == features.end())
return std::nullopt;
// FIXME: The toDouble function does not offer a way to tell "0q" from string with no digits in it: Both
// return the number 0 and false for ok. But "0q" should yield the minimum rather than the default.
bool ok;
double parsedNumber = it->value.toDouble(&ok);
if ((!parsedNumber && !ok) || std::isnan(parsedNumber))
return std::nullopt;
if (parsedNumber < min || max <= min)
return min;
if (parsedNumber > max)
return max;
// FIXME: Seems strange to cast a double to int and then convert back to a float. Why is this a good idea?
return static_cast<int>(parsedNumber);
}
static DialogFeaturesMap parseDialogFeaturesMap(StringView string)
{
// FIXME: Not clear why we take such a different approach to parsing dialog features
// as opposed to window features (using a map, different parsing quirks).
DialogFeaturesMap features;
for (auto featureString : string.split(';')) {
size_t separatorPosition = featureString.find('=');
size_t colonPosition = featureString.find(':');
if (separatorPosition != notFound && colonPosition != notFound)
continue; // ignore strings that have both = and :
if (separatorPosition == notFound)
separatorPosition = colonPosition;
auto key = featureString.left(separatorPosition).stripWhiteSpace().toString();
// Null string for value indicates key without value.
String value;
if (separatorPosition != notFound) {
auto valueView = featureString.substring(separatorPosition + 1).stripWhiteSpace();
value = valueView.left(valueView.find(' ')).toString();
}
features.set(WTFMove(key), WTFMove(value));
}
return features;
}
} // namespace WebCore