blob: 2623f88308b782eb1b7e7aae1038781c57845aaa [file] [log] [blame]
/*
* Copyright (C) 2017-2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#if ENABLE(APPLICATION_MANIFEST)
#include <JavaScriptCore/InitializeThreading.h>
#include <WebCore/ApplicationManifestParser.h>
#include <wtf/RunLoop.h>
using namespace WebCore;
namespace WebCore {
static inline std::ostream& operator<<(std::ostream& os, const ApplicationManifest::Display& display)
{
switch (display) {
case ApplicationManifest::Display::Browser:
return os << "ApplicationManifest::Display::Browser";
case ApplicationManifest::Display::MinimalUI:
return os << "ApplicationManifest::Display::MinimalUI";
case ApplicationManifest::Display::Standalone:
return os << "ApplicationManifest::Display::Standalone";
case ApplicationManifest::Display::Fullscreen:
return os << "ApplicationManifest::Display::Fullscreen";
}
}
} // namespace WebCore
class ApplicationManifestParserTest : public testing::Test {
public:
URL m_manifestURL;
URL m_documentURL;
virtual void SetUp()
{
JSC::initialize();
WTF::initializeMainThread();
m_manifestURL = URL { "https://example.com/manifest.json"_s };
m_documentURL = URL { "https://example.com/"_s };
}
ApplicationManifest parseString(const String& data)
{
return ApplicationManifestParser::parse(data, m_manifestURL, m_documentURL);
}
ApplicationManifest parseTopLevelProperty(const String& key, const String& value)
{
String manifestContent = "{ \"" + key + "\" : " + value + " }";
return parseString(manifestContent);
}
ApplicationManifest parseIconFirstTopLevelProperty(const String& key, const String& value)
{
String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + ", \"src\": \"icon/example.png\" }]}";
return parseString(manifestContent);
}
ApplicationManifest parseIconFirstTopLevelPropertyForSrc(const String& key, const String& value)
{
String manifestContent = "{ \"icons\": [{\"" + key + "\": " + value + " }]}";
return parseString(manifestContent);
}
void testStartURL(const String& rawJSON, const String& expectedValue)
{
testStartURL(rawJSON, { { }, expectedValue });
}
void testStartURL(const String& rawJSON, const URL& expectedValue)
{
auto manifest = parseTopLevelProperty("start_url"_s, rawJSON);
auto value = manifest.startURL;
EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
}
void testDisplay(const String& rawJSON, ApplicationManifest::Display expectedValue)
{
auto manifest = parseTopLevelProperty("display"_s, rawJSON);
auto value = manifest.display;
EXPECT_EQ(expectedValue, value);
}
void testName(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("name"_s, rawJSON);
auto value = manifest.name;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testDescription(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("description"_s, rawJSON);
auto value = manifest.description;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testShortName(const String& rawJSON, const String& expectedValue)
{
auto manifest = parseTopLevelProperty("short_name"_s, rawJSON);
auto value = manifest.shortName;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testScope(const String& rawJSON, const String& startURL, const String& expectedValue)
{
String manifestContent = "{ \"scope\" : " + rawJSON + ", \"start_url\" : \"" + startURL + "\" }";
auto manifest = parseString(manifestContent);
auto value = manifest.scope;
EXPECT_STREQ(expectedValue.utf8().data(), value.string().utf8().data());
}
void testScope(const String& rawJSON, const String& expectedValue)
{
testScope(rawJSON, String(), expectedValue);
}
void testThemeColor(const String& rawJSON, const Color& expectedValue)
{
auto manifest = parseTopLevelProperty("theme_color"_s, rawJSON);
auto value = manifest.themeColor;
EXPECT_EQ(expectedValue, value);
}
void testIconsSrc(const String& rawJSON, const URL& expectedValue)
{
auto manifest = parseIconFirstTopLevelPropertyForSrc("src"_s, rawJSON);
auto value = manifest.icons[0].src;
EXPECT_STREQ(expectedValue.string().utf8().data(), value.string().utf8().data());
}
void testIconsType(const String &rawJSON, const String& expectedValue)
{
auto manifest = parseIconFirstTopLevelProperty("type"_s, rawJSON);
auto value = manifest.icons[0].type;
EXPECT_STREQ(expectedValue.utf8().data(), value.utf8().data());
}
void testIconsSizes(const String &rawJSON, size_t expectedCount, size_t testIndex, const String& expectedValue)
{
auto manifest = parseIconFirstTopLevelProperty("sizes"_s, rawJSON);
auto value = manifest.icons[0].sizes;
EXPECT_EQ(expectedCount, value.size());
EXPECT_TRUE(testIndex < value.size());
EXPECT_STREQ(expectedValue.utf8().data(), value[testIndex].utf8().data());
}
void testIconsPurposes(const String &rawJSON, OptionSet<ApplicationManifest::Icon::Purpose> expectedValues)
{
auto manifest = parseIconFirstTopLevelProperty("purpose"_s, rawJSON);
auto value = manifest.icons[0].purposes;
EXPECT_EQ(expectedValues, value);
}
};
static void assertManifestHasDefaultValues(const URL& manifestURL, const URL& documentURL, const ApplicationManifest& manifest)
{
EXPECT_TRUE(manifest.name.isNull());
EXPECT_TRUE(manifest.shortName.isNull());
EXPECT_TRUE(manifest.description.isNull());
EXPECT_STREQ("https://example.com/", manifest.scope.string().utf8().data());
EXPECT_STREQ(documentURL.string().utf8().data(), manifest.startURL.string().utf8().data());
}
TEST_F(ApplicationManifestParserTest, DefaultManifest)
{
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(String()));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString(""_s));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("{ }"_s));
assertManifestHasDefaultValues(m_manifestURL, m_documentURL, parseString("This is 100% not JSON."_s));
}
TEST_F(ApplicationManifestParserTest, StartURL)
{
m_documentURL = URL { "https://example.com/home"_s };
m_manifestURL = URL { "https://example.com/manifest.json"_s };
testStartURL("123"_s, m_documentURL);
testStartURL("null"_s, m_documentURL);
testStartURL("true"_s, m_documentURL);
testStartURL("{ }"_s, m_documentURL);
testStartURL("[ ]"_s, m_documentURL);
testStartURL("[ \"http://example.com/somepage\" ]"_s, m_documentURL);
testStartURL("\"\""_s, m_documentURL);
testStartURL("\"http:?\""_s, m_documentURL);
testStartURL("\"appstartpage\""_s, "https://example.com/appstartpage"_s);
testStartURL("\"a/b/cdefg\""_s, "https://example.com/a/b/cdefg"_s);
m_documentURL = URL { "https://example.com/subfolder/home"_s };
m_manifestURL = URL { "https://example.com/resources/manifest.json"_s };
testStartURL("\"resource-relative-to-manifest-url\""_s, "https://example.com/resources/resource-relative-to-manifest-url"_s);
testStartURL("\"http://different-page.com/12/34\""_s, m_documentURL);
m_documentURL = URL { "https://example.com/home"_s };
m_manifestURL = URL { "https://other-domain.com/manifiest.json"_s };
testStartURL("\"resource_on_other_domain\""_s, m_documentURL);
testStartURL("\"http://example.com/scheme-does-not-match-document\""_s, m_documentURL);
testStartURL("\"https://example.com:123/port-does-not-match-document"_s, m_documentURL);
testStartURL("\"https://example.com/page2\""_s, "https://example.com/page2"_s);
testStartURL("\"//example.com/page2\""_s, "https://example.com/page2"_s);
m_documentURL = URL { "https://example.com/a"_s };
m_manifestURL = URL { "https://example.com/z/manifest.json"_s };
testStartURL("\"b/c\""_s, "https://example.com/z/b/c"_s);
testStartURL("\"/b/c\""_s, "https://example.com/b/c"_s);
testStartURL("\"?query\""_s, "https://example.com/z/manifest.json?query"_s);
m_documentURL = URL { "https://example.com/dir1/dir2/page1"_s };
m_manifestURL = URL { "https://example.com/dir3/manifest.json"_s };
testStartURL("\"../page2\""_s, "https://example.com/page2"_s);
}
TEST_F(ApplicationManifestParserTest, Display)
{
testDisplay("123"_s, ApplicationManifest::Display::Browser);
testDisplay("null"_s, ApplicationManifest::Display::Browser);
testDisplay("true"_s, ApplicationManifest::Display::Browser);
testDisplay("{ }"_s, ApplicationManifest::Display::Browser);
testDisplay("[ ]"_s, ApplicationManifest::Display::Browser);
testDisplay("\"\""_s, ApplicationManifest::Display::Browser);
testDisplay("\"garbage string\""_s, ApplicationManifest::Display::Browser);
testDisplay("\"browser\""_s, ApplicationManifest::Display::Browser);
testDisplay("\"standalone\""_s, ApplicationManifest::Display::Standalone);
testDisplay("\"minimal-ui\""_s, ApplicationManifest::Display::MinimalUI);
testDisplay("\"fullscreen\""_s, ApplicationManifest::Display::Fullscreen);
testDisplay("\"\t\nMINIMAL-UI \""_s, ApplicationManifest::Display::MinimalUI);
}
TEST_F(ApplicationManifestParserTest, Name)
{
testName("123"_s, String());
testName("null"_s, String());
testName("true"_s, String());
testName("{ }"_s, String());
testName("[ ]"_s, String());
testName("\"\""_s, emptyString());
testName("\"example\""_s, "example"_s);
testName("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}
TEST_F(ApplicationManifestParserTest, Description)
{
testDescription("123"_s, String());
testDescription("null"_s, String());
testDescription("true"_s, String());
testDescription("{ }"_s, String());
testDescription("[ ]"_s, String());
testDescription("\"\""_s, emptyString());
testDescription("\"example\""_s, "example"_s);
testDescription("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}
TEST_F(ApplicationManifestParserTest, ShortName)
{
testShortName("123"_s, String());
testShortName("null"_s, String());
testShortName("true"_s, String());
testShortName("{ }"_s, String());
testShortName("[ ]"_s, String());
testShortName("\"\""_s, ""_s);
testShortName("\"example\""_s, "example"_s);
testShortName("\"\\t Hello\\nWorld\\t \""_s, "Hello\nWorld"_s);
}
TEST_F(ApplicationManifestParserTest, Scope)
{
// If the scope is not a string or not a valid URL, return the default scope (the parent path of the start URL).
m_documentURL = URL { "https://example.com/a/page?queryParam=value#fragment"_s };
m_manifestURL = URL { "https://example.com/manifest.json"_s };
testScope("123"_s, "https://example.com/a/"_s);
testScope("null"_s, "https://example.com/a/"_s);
testScope("true"_s, "https://example.com/a/"_s);
testScope("{ }"_s, "https://example.com/a/"_s);
testScope("[ ]"_s, "https://example.com/a/"_s);
testScope("\"\""_s, "https://example.com/a/"_s);
testScope("\"http:?\""_s, "https://example.com/a/"_s);
m_documentURL = URL { "https://example.com/a/pageEndingWithSlash/"_s };
testScope("null"_s, "https://example.com/a/pageEndingWithSlash/"_s);
// If scope URL is not same origin as document URL, return the default scope.
m_documentURL = URL { "https://example.com/home"_s };
m_manifestURL = URL { "https://other-site.com/manifest.json"_s };
testScope("\"https://other-site.com/some-scope\""_s, "https://example.com/"_s);
m_documentURL = URL { "https://example.com/app/home"_s };
m_manifestURL = URL { "https://example.com/app/manifest.json"_s };
// If start URL is not within scope of scope URL, return the default scope.
testScope("\"https://example.com/subdirectory\""_s, "https://example.com/app/"_s);
testScope("\"https://example.com/app\""_s, "https://example.com/app"_s);
testScope("\"https://example.com/APP\""_s, "https://example.com/app/"_s);
testScope("\"https://example.com/a\""_s, "https://example.com/a"_s);
m_documentURL = URL { "https://example.com/a/b/c/index"_s };
m_manifestURL = URL { "https://example.com/a/manifest.json"_s };
testScope("\"./b/c/index\""_s, "https://example.com/a/b/c/index"_s);
testScope("\"b/somewhere-else/../c\""_s, "https://example.com/a/b/c"_s);
testScope("\"b\""_s, "https://example.com/a/b"_s);
testScope("\"b/\""_s, "https://example.com/a/b/"_s);
m_documentURL = URL { "https://example.com/documents/home"_s };
m_manifestURL = URL { "https://example.com/resources/manifest.json"_s };
// It's fine if the document URL or manifest URL aren't within the application scope - only the start URL needs to be.
testScope("\"https://example.com/other\""_s, "https://example.com/other/start-url"_s, "https://example.com/other"_s);
}
TEST_F(ApplicationManifestParserTest, ThemeColor)
{
testThemeColor("123"_s, Color());
testThemeColor("null"_s, Color());
testThemeColor("true"_s, Color());
testThemeColor("{ }"_s, Color());
testThemeColor("[ ]"_s, Color());
testThemeColor("\"\""_s, Color());
testThemeColor("\"garbage string\""_s, Color());
testThemeColor("\"red\""_s, Color::red);
testThemeColor("\"#f00\""_s, Color::red);
testThemeColor("\"#ff0000\""_s, Color::red);
testThemeColor("\"#ff0000ff\""_s, Color::red);
testThemeColor("\"rgb(255, 0, 0)\""_s, Color::red);
testThemeColor("\"rgba(255, 0, 0, 1)\""_s, Color::red);
testThemeColor("\"hsl(0, 100%, 50%)\""_s, Color::red);
testThemeColor("\"hsla(0, 100%, 50%, 1)\""_s, Color::red);
}
TEST_F(ApplicationManifestParserTest, Whitespace)
{
auto manifest = parseString(" { \"name\": \"PASS\" }\n"_s);
EXPECT_STREQ("PASS", manifest.name.utf8().data());
}
TEST_F(ApplicationManifestParserTest, Icons)
{
URL srcURL = URL { "https://example.com/icon.jpg"_s };
testIconsSrc("\"icon.jpg\""_s, srcURL);
testIconsType("\"image/webp\""_s, "image/webp"_s);
testIconsSizes("\"256x256\""_s, 1, 0, "256x256"_s);
testIconsSizes("\"72x72 96x96\""_s, 2, 0, "72x72"_s);
testIconsSizes("\"72x72 96x96\""_s, 2, 1, "96x96"_s);
OptionSet<ApplicationManifest::Icon::Purpose> purposeAny { ApplicationManifest::Icon::Purpose::Any };
OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochrome { ApplicationManifest::Icon::Purpose::Monochrome };
OptionSet<ApplicationManifest::Icon::Purpose> purposeMaskable { ApplicationManifest::Icon::Purpose::Maskable };
testIconsPurposes("\"monochrome\""_s, purposeMonochrome);
testIconsPurposes("\"maskable\""_s, purposeMaskable);
testIconsPurposes("\"any\""_s, purposeAny);
testIconsPurposes("\"\tMONOCHROME\""_s, purposeMonochrome);
testIconsPurposes("123"_s, purposeAny);
testIconsPurposes("null"_s, purposeAny);
testIconsPurposes("true"_s, purposeAny);
testIconsPurposes("{ }"_s, purposeAny);
testIconsPurposes("[ ]"_s, purposeAny);
OptionSet<ApplicationManifest::Icon::Purpose> purposeMonochromeAny { ApplicationManifest::Icon::Purpose::Monochrome, ApplicationManifest::Icon::Purpose::Any };
testIconsPurposes("\"monochrome any\""_s, purposeMonochromeAny);
}
#endif