blob: ce395fbf06e1759231bc9baab08424ce2c23cc27 [file] [log] [blame]
/*
* Copyright (C) 2014 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"
#include "ContentExtensionParser.h"
#if ENABLE(CONTENT_EXTENSIONS)
#include "CSSParser.h"
#include "CSSParserMode.h"
#include "CSSSelectorList.h"
#include "ContentExtensionError.h"
#include "ContentExtensionRule.h"
#include "ContentExtensionsBackend.h"
#include "ContentExtensionsDebugging.h"
#include <JavaScriptCore/JSCInlines.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/JSONObject.h>
#include <JavaScriptCore/VM.h>
#include <wtf/CurrentTime.h>
#include <wtf/text/WTFString.h>
using namespace JSC;
namespace WebCore {
namespace ContentExtensions {
static bool containsOnlyASCIIWithNoUppercase(const String& domain)
{
for (unsigned i = 0; i < domain.length(); ++i) {
UChar c = domain.at(i);
if (!isASCII(c) || isASCIIUpper(c))
return false;
}
return true;
}
static std::error_code getDomainList(ExecState& exec, const JSObject* arrayObject, Vector<String>& vector)
{
VM& vm = exec.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(vector.isEmpty());
if (!arrayObject || !isJSArray(arrayObject))
return ContentExtensionError::JSONInvalidDomainList;
const JSArray* array = jsCast<const JSArray*>(arrayObject);
unsigned length = array->length();
for (unsigned i = 0; i < length; ++i) {
const JSValue value = array->getIndex(&exec, i);
if (scope.exception() || !value.isString())
return ContentExtensionError::JSONInvalidDomainList;
// Domains should be punycode encoded lower case.
const String& domain = jsCast<JSString*>(value)->value(&exec);
if (domain.isEmpty())
return ContentExtensionError::JSONInvalidDomainList;
if (!containsOnlyASCIIWithNoUppercase(domain))
return ContentExtensionError::JSONDomainNotLowerCaseASCII;
vector.append(domain);
}
return { };
}
static std::error_code getTypeFlags(ExecState& exec, const JSValue& typeValue, ResourceFlags& flags, uint16_t (*stringToType)(const String&))
{
VM& vm = exec.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
if (!typeValue.isObject())
return { };
const JSObject* object = typeValue.toObject(&exec);
ASSERT(!scope.exception());
if (!isJSArray(object))
return ContentExtensionError::JSONInvalidTriggerFlagsArray;
const JSArray* array = jsCast<const JSArray*>(object);
unsigned length = array->length();
for (unsigned i = 0; i < length; ++i) {
const JSValue value = array->getIndex(&exec, i);
if (scope.exception() || !value)
return ContentExtensionError::JSONInvalidObjectInTriggerFlagsArray;
String name = value.toWTFString(&exec);
uint16_t type = stringToType(name);
if (!type)
return ContentExtensionError::JSONInvalidStringInTriggerFlagsArray;
flags |= type;
}
return { };
}
static std::error_code loadTrigger(ExecState& exec, const JSObject& ruleObject, Trigger& trigger)
{
VM& vm = exec.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
const JSValue triggerObject = ruleObject.get(&exec, Identifier::fromString(&exec, "trigger"));
if (!triggerObject || scope.exception() || !triggerObject.isObject())
return ContentExtensionError::JSONInvalidTrigger;
const JSValue urlFilterObject = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter"));
if (!urlFilterObject || scope.exception() || !urlFilterObject.isString())
return ContentExtensionError::JSONInvalidURLFilterInTrigger;
String urlFilter = urlFilterObject.toWTFString(&exec);
if (urlFilter.isEmpty())
return ContentExtensionError::JSONInvalidURLFilterInTrigger;
trigger.urlFilter = urlFilter;
const JSValue urlFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter-is-case-sensitive"));
if (urlFilterCaseValue && !scope.exception() && urlFilterCaseValue.isBoolean())
trigger.urlFilterIsCaseSensitive = urlFilterCaseValue.toBoolean(&exec);
const JSValue resourceTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "resource-type"));
if (!scope.exception() && resourceTypeValue.isObject()) {
auto typeFlagsError = getTypeFlags(exec, resourceTypeValue, trigger.flags, readResourceType);
if (typeFlagsError)
return typeFlagsError;
} else if (!resourceTypeValue.isUndefined())
return ContentExtensionError::JSONInvalidTriggerFlagsArray;
const JSValue loadTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "load-type"));
if (!scope.exception() && loadTypeValue.isObject()) {
auto typeFlagsError = getTypeFlags(exec, loadTypeValue, trigger.flags, readLoadType);
if (typeFlagsError)
return typeFlagsError;
} else if (!loadTypeValue.isUndefined())
return ContentExtensionError::JSONInvalidTriggerFlagsArray;
const JSValue ifDomain = triggerObject.get(&exec, Identifier::fromString(&exec, "if-domain"));
if (!scope.exception() && ifDomain.isObject()) {
auto ifDomainError = getDomainList(exec, asObject(ifDomain), trigger.domains);
if (ifDomainError)
return ifDomainError;
if (trigger.domains.isEmpty())
return ContentExtensionError::JSONInvalidDomainList;
ASSERT(trigger.domainCondition == Trigger::DomainCondition::None);
trigger.domainCondition = Trigger::DomainCondition::IfDomain;
} else if (!ifDomain.isUndefined())
return ContentExtensionError::JSONInvalidDomainList;
const JSValue unlessDomain = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-domain"));
if (!scope.exception() && unlessDomain.isObject()) {
if (trigger.domainCondition != Trigger::DomainCondition::None)
return ContentExtensionError::JSONUnlessAndIfDomain;
auto unlessDomainError = getDomainList(exec, asObject(unlessDomain), trigger.domains);
if (unlessDomainError)
return unlessDomainError;
if (trigger.domains.isEmpty())
return ContentExtensionError::JSONInvalidDomainList;
trigger.domainCondition = Trigger::DomainCondition::UnlessDomain;
} else if (!unlessDomain.isUndefined())
return ContentExtensionError::JSONInvalidDomainList;
return { };
}
static bool isValidSelector(const String& selector)
{
CSSParserContext context(HTMLQuirksMode);
CSSParser parser(context);
CSSSelectorList selectorList;
parser.parseSelector(selector, selectorList);
return selectorList.isValid();
}
static std::error_code loadAction(ExecState& exec, const JSObject& ruleObject, Action& action, bool& validSelector)
{
VM& vm = exec.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
validSelector = true;
const JSValue actionObject = ruleObject.get(&exec, Identifier::fromString(&exec, "action"));
if (!actionObject || scope.exception() || !actionObject.isObject())
return ContentExtensionError::JSONInvalidAction;
const JSValue typeObject = actionObject.get(&exec, Identifier::fromString(&exec, "type"));
if (!typeObject || scope.exception() || !typeObject.isString())
return ContentExtensionError::JSONInvalidActionType;
String actionType = typeObject.toWTFString(&exec);
if (actionType == "block")
action = ActionType::BlockLoad;
else if (actionType == "ignore-previous-rules")
action = ActionType::IgnorePreviousRules;
else if (actionType == "block-cookies")
action = ActionType::BlockCookies;
else if (actionType == "css-display-none") {
JSValue selector = actionObject.get(&exec, Identifier::fromString(&exec, "selector"));
if (!selector || scope.exception() || !selector.isString())
return ContentExtensionError::JSONInvalidCSSDisplayNoneActionType;
String s = selector.toWTFString(&exec);
if (!isValidSelector(s)) {
// Skip rules with invalid selectors to be backwards-compatible.
validSelector = false;
return { };
}
action = Action(ActionType::CSSDisplayNoneSelector, s);
} else if (actionType == "make-https") {
action = ActionType::MakeHTTPS;
} else
return ContentExtensionError::JSONInvalidActionType;
return { };
}
static std::error_code loadRule(ExecState& exec, const JSObject& ruleObject, Vector<ContentExtensionRule>& ruleList)
{
Trigger trigger;
auto triggerError = loadTrigger(exec, ruleObject, trigger);
if (triggerError)
return triggerError;
Action action;
bool validSelector;
auto actionError = loadAction(exec, ruleObject, action, validSelector);
if (actionError)
return actionError;
if (validSelector)
ruleList.append(ContentExtensionRule(trigger, action));
return { };
}
static std::error_code loadEncodedRules(ExecState& exec, const String& rules, Vector<ContentExtensionRule>& ruleList)
{
VM& vm = exec.vm();
auto scope = DECLARE_THROW_SCOPE(vm);
// FIXME: JSONParse should require callbacks instead of an ExecState.
const JSValue decodedRules = JSONParse(&exec, rules);
if (scope.exception() || !decodedRules)
return ContentExtensionError::JSONInvalid;
if (!decodedRules.isObject())
return ContentExtensionError::JSONTopLevelStructureNotAnObject;
const JSObject* topLevelObject = decodedRules.toObject(&exec);
if (!topLevelObject || scope.exception())
return ContentExtensionError::JSONTopLevelStructureNotAnObject;
if (!isJSArray(topLevelObject))
return ContentExtensionError::JSONTopLevelStructureNotAnArray;
const JSArray* topLevelArray = jsCast<const JSArray*>(topLevelObject);
Vector<ContentExtensionRule> localRuleList;
unsigned length = topLevelArray->length();
const unsigned maxRuleCount = 50000;
if (length > maxRuleCount)
return ContentExtensionError::JSONTooManyRules;
for (unsigned i = 0; i < length; ++i) {
const JSValue value = topLevelArray->getIndex(&exec, i);
if (scope.exception() || !value)
return ContentExtensionError::JSONInvalidObjectInTopLevelArray;
const JSObject* ruleObject = value.toObject(&exec);
if (!ruleObject || scope.exception())
return ContentExtensionError::JSONInvalidRule;
auto error = loadRule(exec, *ruleObject, localRuleList);
if (error)
return error;
}
ruleList = WTFMove(localRuleList);
return { };
}
std::error_code parseRuleList(const String& rules, Vector<ContentExtensionRule>& ruleList)
{
#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
double loadExtensionStartTime = monotonicallyIncreasingTime();
#endif
RefPtr<VM> vm = VM::create();
JSLockHolder locker(vm.get());
JSGlobalObject* globalObject = JSGlobalObject::create(*vm, JSGlobalObject::createStructure(*vm, jsNull()));
ExecState* exec = globalObject->globalExec();
auto error = loadEncodedRules(*exec, rules, ruleList);
vm = nullptr;
if (error)
return error;
if (ruleList.isEmpty())
return ContentExtensionError::JSONContainsNoRules;
#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
double loadExtensionEndTime = monotonicallyIncreasingTime();
dataLogF("Time spent loading extension %f\n", (loadExtensionEndTime - loadExtensionStartTime));
#endif
return { };
}
} // namespace ContentExtensions
} // namespace WebCore
#endif // ENABLE(CONTENT_EXTENSIONS)