| /* |
| * Copyright (C) 2014-2017 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 "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/Expected.h> |
| #include <wtf/text/WTFString.h> |
| |
| |
| namespace WebCore { |
| using namespace JSC; |
| |
| namespace ContentExtensions { |
| |
| static bool containsOnlyASCIIWithNoUppercase(const String& domain) |
| { |
| for (auto character : StringView { domain }.codeUnits()) { |
| if (!isASCII(character) || isASCIIUpper(character)) |
| return false; |
| } |
| return true; |
| } |
| |
| static Expected<Vector<String>, std::error_code> getStringList(ExecState& exec, const JSObject* arrayObject) |
| { |
| static const ContentExtensionError error = ContentExtensionError::JSONInvalidConditionList; |
| VM& vm = exec.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| if (!arrayObject || !isJSArray(arrayObject)) |
| return makeUnexpected(error); |
| const JSArray* array = jsCast<const JSArray*>(arrayObject); |
| |
| Vector<String> strings; |
| unsigned length = array->length(); |
| for (unsigned i = 0; i < length; ++i) { |
| const JSValue value = array->getIndex(&exec, i); |
| if (scope.exception() || !value.isString()) |
| return makeUnexpected(error); |
| |
| const String& string = asString(value)->value(&exec); |
| if (string.isEmpty()) |
| return makeUnexpected(error); |
| strings.append(string); |
| } |
| return strings; |
| } |
| |
| static Expected<Vector<String>, std::error_code> getDomainList(ExecState& exec, const JSObject* arrayObject) |
| { |
| auto strings = getStringList(exec, arrayObject); |
| if (!strings.has_value()) |
| return strings; |
| for (auto& domain : strings.value()) { |
| // Domains should be punycode encoded lower case. |
| if (!containsOnlyASCIIWithNoUppercase(domain)) |
| return makeUnexpected(ContentExtensionError::JSONDomainNotLowerCaseASCII); |
| } |
| return strings; |
| } |
| |
| 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); |
| scope.assertNoException(); |
| 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 Expected<Trigger, std::error_code> loadTrigger(ExecState& exec, const JSObject& ruleObject) |
| { |
| 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 makeUnexpected(ContentExtensionError::JSONInvalidTrigger); |
| |
| const JSValue urlFilterObject = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter")); |
| if (!urlFilterObject || scope.exception() || !urlFilterObject.isString()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger); |
| |
| String urlFilter = asString(urlFilterObject)->value(&exec); |
| if (urlFilter.isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger); |
| |
| Trigger trigger; |
| 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 topURLFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "top-url-filter-is-case-sensitive")); |
| if (topURLFilterCaseValue && !scope.exception() && topURLFilterCaseValue.isBoolean()) |
| trigger.topURLConditionIsCaseSensitive = topURLFilterCaseValue.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 makeUnexpected(typeFlagsError); |
| } else if (!resourceTypeValue.isUndefined()) |
| return makeUnexpected(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 makeUnexpected(typeFlagsError); |
| } else if (!loadTypeValue.isUndefined()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidTriggerFlagsArray); |
| |
| const JSValue ifDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-domain")); |
| if (!scope.exception() && ifDomainValue.isObject()) { |
| auto ifDomain = getDomainList(exec, asObject(ifDomainValue)); |
| if (!ifDomain.has_value()) |
| return makeUnexpected(ifDomain.error()); |
| trigger.conditions = WTFMove(ifDomain.value()); |
| if (trigger.conditions.isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| ASSERT(trigger.conditionType == Trigger::ConditionType::None); |
| trigger.conditionType = Trigger::ConditionType::IfDomain; |
| } else if (!ifDomainValue.isUndefined()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| |
| const JSValue unlessDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-domain")); |
| if (!scope.exception() && unlessDomainValue.isObject()) { |
| if (trigger.conditionType != Trigger::ConditionType::None) |
| return makeUnexpected(ContentExtensionError::JSONMultipleConditions); |
| auto unlessDomain = getDomainList(exec, asObject(unlessDomainValue)); |
| if (!unlessDomain.has_value()) |
| return makeUnexpected(unlessDomain.error()); |
| trigger.conditions = WTFMove(unlessDomain.value()); |
| if (trigger.conditions.isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| trigger.conditionType = Trigger::ConditionType::UnlessDomain; |
| } else if (!unlessDomainValue.isUndefined()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| |
| const JSValue ifTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-top-url")); |
| if (!scope.exception() && ifTopURLValue.isObject()) { |
| if (trigger.conditionType != Trigger::ConditionType::None) |
| return makeUnexpected(ContentExtensionError::JSONMultipleConditions); |
| auto ifTopURL = getStringList(exec, asObject(ifTopURLValue)); |
| if (!ifTopURL.has_value()) |
| return makeUnexpected(ifTopURL.error()); |
| trigger.conditions = WTFMove(ifTopURL.value()); |
| if (trigger.conditions.isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| trigger.conditionType = Trigger::ConditionType::IfTopURL; |
| } else if (!ifTopURLValue.isUndefined()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| |
| const JSValue unlessTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-top-url")); |
| if (!scope.exception() && unlessTopURLValue.isObject()) { |
| if (trigger.conditionType != Trigger::ConditionType::None) |
| return makeUnexpected(ContentExtensionError::JSONMultipleConditions); |
| auto unlessTopURL = getStringList(exec, asObject(unlessTopURLValue)); |
| if (!unlessTopURL.has_value()) |
| return makeUnexpected(unlessTopURL.error()); |
| trigger.conditions = WTFMove(unlessTopURL.value()); |
| if (trigger.conditions.isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| trigger.conditionType = Trigger::ConditionType::UnlessTopURL; |
| } else if (!unlessTopURLValue.isUndefined()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidConditionList); |
| |
| return trigger; |
| } |
| |
| bool isValidCSSSelector(const String& selector) |
| { |
| ASSERT(isMainThread()); |
| AtomicString::init(); |
| QualifiedName::init(); |
| CSSParserContext context(HTMLQuirksMode); |
| CSSParser parser(context); |
| CSSSelectorList selectorList; |
| parser.parseSelector(selector, selectorList); |
| return selectorList.isValid(); |
| } |
| |
| static Expected<Optional<Action>, std::error_code> loadAction(ExecState& exec, const JSObject& ruleObject) |
| { |
| VM& vm = exec.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| const JSValue actionObject = ruleObject.get(&exec, Identifier::fromString(&exec, "action")); |
| if (scope.exception() || !actionObject.isObject()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidAction); |
| |
| const JSValue typeObject = actionObject.get(&exec, Identifier::fromString(&exec, "type")); |
| if (scope.exception() || !typeObject.isString()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidActionType); |
| |
| String actionType = asString(typeObject)->value(&exec); |
| |
| if (actionType == "block") |
| return { Action(ActionType::BlockLoad) }; |
| if (actionType == "ignore-previous-rules") |
| return { Action(ActionType::IgnorePreviousRules) }; |
| if (actionType == "block-cookies") |
| return { Action(ActionType::BlockCookies) }; |
| if (actionType == "css-display-none") { |
| JSValue selector = actionObject.get(&exec, Identifier::fromString(&exec, "selector")); |
| if (scope.exception() || !selector.isString()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidCSSDisplayNoneActionType); |
| |
| String selectorString = asString(selector)->value(&exec); |
| if (!isValidCSSSelector(selectorString)) { |
| // Skip rules with invalid selectors to be backwards-compatible. |
| return { WTF::nullopt }; |
| } |
| return { Action(ActionType::CSSDisplayNoneSelector, selectorString) }; |
| } |
| if (actionType == "make-https") |
| return { Action(ActionType::MakeHTTPS) }; |
| if (actionType == "notify") { |
| JSValue notification = actionObject.get(&exec, Identifier::fromString(&exec, "notification")); |
| if (scope.exception() || !notification.isString()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidNotification); |
| return { Action(ActionType::Notify, asString(notification)->value(&exec)) }; |
| } |
| return makeUnexpected(ContentExtensionError::JSONInvalidActionType); |
| } |
| |
| static Expected<Optional<ContentExtensionRule>, std::error_code> loadRule(ExecState& exec, const JSObject& ruleObject) |
| { |
| auto trigger = loadTrigger(exec, ruleObject); |
| if (!trigger.has_value()) |
| return makeUnexpected(trigger.error()); |
| |
| auto action = loadAction(exec, ruleObject); |
| if (!action.has_value()) |
| return makeUnexpected(action.error()); |
| |
| if (action.value()) |
| return {{{ WTFMove(trigger.value()), WTFMove(action.value().value()) }}}; |
| |
| return { WTF::nullopt }; |
| } |
| |
| static Expected<Vector<ContentExtensionRule>, std::error_code> loadEncodedRules(ExecState& exec, const String& ruleJSON) |
| { |
| VM& vm = exec.vm(); |
| auto scope = DECLARE_THROW_SCOPE(vm); |
| |
| // FIXME: JSONParse should require callbacks instead of an ExecState. |
| const JSValue decodedRules = JSONParse(&exec, ruleJSON); |
| |
| if (scope.exception() || !decodedRules) |
| return makeUnexpected(ContentExtensionError::JSONInvalid); |
| |
| if (!decodedRules.isObject()) |
| return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject); |
| |
| const JSObject* topLevelObject = decodedRules.toObject(&exec); |
| if (!topLevelObject || scope.exception()) |
| return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject); |
| |
| if (!isJSArray(topLevelObject)) |
| return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnArray); |
| |
| const JSArray* topLevelArray = jsCast<const JSArray*>(topLevelObject); |
| |
| Vector<ContentExtensionRule> ruleList; |
| |
| unsigned length = topLevelArray->length(); |
| const unsigned maxRuleCount = 50000; |
| if (length > maxRuleCount) |
| return makeUnexpected(ContentExtensionError::JSONTooManyRules); |
| for (unsigned i = 0; i < length; ++i) { |
| const JSValue value = topLevelArray->getIndex(&exec, i); |
| if (scope.exception() || !value) |
| return makeUnexpected(ContentExtensionError::JSONInvalidObjectInTopLevelArray); |
| |
| const JSObject* ruleObject = value.toObject(&exec); |
| if (!ruleObject || scope.exception()) |
| return makeUnexpected(ContentExtensionError::JSONInvalidRule); |
| |
| auto rule = loadRule(exec, *ruleObject); |
| if (!rule.has_value()) |
| return makeUnexpected(rule.error()); |
| if (rule.value()) |
| ruleList.append(WTFMove(*rule.value())); |
| } |
| |
| return ruleList; |
| } |
| |
| Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(const String& ruleJSON) |
| { |
| #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING |
| MonotonicTime loadExtensionStartTime = MonotonicTime::now(); |
| #endif |
| RefPtr<VM> vm = VM::create(); |
| |
| JSLockHolder locker(vm.get()); |
| JSGlobalObject* globalObject = JSGlobalObject::create(*vm, JSGlobalObject::createStructure(*vm, jsNull())); |
| |
| ExecState* exec = globalObject->globalExec(); |
| auto ruleList = loadEncodedRules(*exec, ruleJSON); |
| |
| vm = nullptr; |
| |
| if (!ruleList.has_value()) |
| return makeUnexpected(ruleList.error()); |
| |
| if (ruleList->isEmpty()) |
| return makeUnexpected(ContentExtensionError::JSONContainsNoRules); |
| |
| #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING |
| MonotonicTime loadExtensionEndTime = MonotonicTime::now(); |
| dataLogF("Time spent loading extension %f\n", (loadExtensionEndTime - loadExtensionStartTime).seconds()); |
| #endif |
| |
| return ruleList; |
| } |
| |
| } // namespace ContentExtensions |
| } // namespace WebCore |
| |
| #endif // ENABLE(CONTENT_EXTENSIONS) |