c2d2c58d09fd18de514f0ae229e499ed02181802
[WebKit-https.git] / Source / WebCore / contentextensions / ContentExtensionParser.cpp
1 /*
2  * Copyright (C) 2014-2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "ContentExtensionParser.h"
28
29 #if ENABLE(CONTENT_EXTENSIONS)
30
31 #include "CSSParser.h"
32 #include "CSSParserMode.h"
33 #include "CSSSelectorList.h"
34 #include "ContentExtensionError.h"
35 #include "ContentExtensionRule.h"
36 #include "ContentExtensionsBackend.h"
37 #include "ContentExtensionsDebugging.h"
38 #include <JavaScriptCore/JSCInlines.h>
39 #include <JavaScriptCore/JSGlobalObject.h>
40 #include <JavaScriptCore/JSONObject.h>
41 #include <JavaScriptCore/VM.h>
42 #include <wtf/CurrentTime.h>
43 #include <wtf/Expected.h>
44 #include <wtf/text/WTFString.h>
45
46 using namespace JSC;
47
48 namespace WebCore {
49
50 namespace ContentExtensions {
51     
52 static bool containsOnlyASCIIWithNoUppercase(const String& domain)
53 {
54     for (unsigned i = 0; i < domain.length(); ++i) {
55         UChar c = domain.at(i);
56         if (!isASCII(c) || isASCIIUpper(c))
57             return false;
58     }
59     return true;
60 }
61     
62 static Expected<Vector<String>, std::error_code> getStringList(ExecState& exec, const JSObject* arrayObject)
63 {
64     static const ContentExtensionError error = ContentExtensionError::JSONInvalidConditionList;
65     VM& vm = exec.vm();
66     auto scope = DECLARE_THROW_SCOPE(vm);
67
68     if (!arrayObject || !isJSArray(arrayObject))
69         return makeUnexpected(error);
70     const JSArray* array = jsCast<const JSArray*>(arrayObject);
71     
72     Vector<String> strings;
73     unsigned length = array->length();
74     for (unsigned i = 0; i < length; ++i) {
75         const JSValue value = array->getIndex(&exec, i);
76         if (scope.exception() || !value.isString())
77             return makeUnexpected(error);
78         
79         const String& string = asString(value)->value(&exec);
80         if (string.isEmpty())
81             return makeUnexpected(error);
82         strings.append(string);
83     }
84     return WTFMove(strings);
85 }
86
87 static Expected<Vector<String>, std::error_code> getDomainList(ExecState& exec, const JSObject* arrayObject)
88 {
89     auto strings = getStringList(exec, arrayObject);
90     if (!strings.hasValue())
91         return strings;
92     for (auto& domain : strings.value()) {
93         // Domains should be punycode encoded lower case.
94         if (!containsOnlyASCIIWithNoUppercase(domain))
95             return makeUnexpected(ContentExtensionError::JSONDomainNotLowerCaseASCII);
96     }
97     return strings;
98 }
99
100 static std::error_code getTypeFlags(ExecState& exec, const JSValue& typeValue, ResourceFlags& flags, uint16_t (*stringToType)(const String&))
101 {
102     VM& vm = exec.vm();
103     auto scope = DECLARE_THROW_SCOPE(vm);
104
105     if (!typeValue.isObject())
106         return { };
107
108     const JSObject* object = typeValue.toObject(&exec);
109     scope.assertNoException();
110     if (!isJSArray(object))
111         return ContentExtensionError::JSONInvalidTriggerFlagsArray;
112
113     const JSArray* array = jsCast<const JSArray*>(object);
114     
115     unsigned length = array->length();
116     for (unsigned i = 0; i < length; ++i) {
117         const JSValue value = array->getIndex(&exec, i);
118         if (scope.exception() || !value)
119             return ContentExtensionError::JSONInvalidObjectInTriggerFlagsArray;
120         
121         String name = value.toWTFString(&exec);
122         uint16_t type = stringToType(name);
123         if (!type)
124             return ContentExtensionError::JSONInvalidStringInTriggerFlagsArray;
125
126         flags |= type;
127     }
128
129     return { };
130 }
131     
132 static Expected<Trigger, std::error_code> loadTrigger(ExecState& exec, const JSObject& ruleObject)
133 {
134     VM& vm = exec.vm();
135     auto scope = DECLARE_THROW_SCOPE(vm);
136
137     const JSValue triggerObject = ruleObject.get(&exec, Identifier::fromString(&exec, "trigger"));
138     if (!triggerObject || scope.exception() || !triggerObject.isObject())
139         return makeUnexpected(ContentExtensionError::JSONInvalidTrigger);
140     
141     const JSValue urlFilterObject = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter"));
142     if (!urlFilterObject || scope.exception() || !urlFilterObject.isString())
143         return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger);
144
145     String urlFilter = asString(urlFilterObject)->value(&exec);
146     if (urlFilter.isEmpty())
147         return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger);
148
149     Trigger trigger;
150     trigger.urlFilter = urlFilter;
151
152     const JSValue urlFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter-is-case-sensitive"));
153     if (urlFilterCaseValue && !scope.exception() && urlFilterCaseValue.isBoolean())
154         trigger.urlFilterIsCaseSensitive = urlFilterCaseValue.toBoolean(&exec);
155
156     const JSValue topURLFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "top-url-filter-is-case-sensitive"));
157     if (topURLFilterCaseValue && !scope.exception() && topURLFilterCaseValue.isBoolean())
158         trigger.topURLConditionIsCaseSensitive = topURLFilterCaseValue.toBoolean(&exec);
159
160     const JSValue resourceTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "resource-type"));
161     if (!scope.exception() && resourceTypeValue.isObject()) {
162         auto typeFlagsError = getTypeFlags(exec, resourceTypeValue, trigger.flags, readResourceType);
163         if (typeFlagsError)
164             return makeUnexpected(typeFlagsError);
165     } else if (!resourceTypeValue.isUndefined())
166         return makeUnexpected(ContentExtensionError::JSONInvalidTriggerFlagsArray);
167
168     const JSValue loadTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "load-type"));
169     if (!scope.exception() && loadTypeValue.isObject()) {
170         auto typeFlagsError = getTypeFlags(exec, loadTypeValue, trigger.flags, readLoadType);
171         if (typeFlagsError)
172             return makeUnexpected(typeFlagsError);
173     } else if (!loadTypeValue.isUndefined())
174         return makeUnexpected(ContentExtensionError::JSONInvalidTriggerFlagsArray);
175
176     const JSValue ifDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-domain"));
177     if (!scope.exception() && ifDomainValue.isObject()) {
178         auto ifDomain = getDomainList(exec, asObject(ifDomainValue));
179         if (!ifDomain.hasValue())
180             return makeUnexpected(ifDomain.error());
181         trigger.conditions = WTFMove(ifDomain.value());
182         if (trigger.conditions.isEmpty())
183             return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
184         ASSERT(trigger.conditionType == Trigger::ConditionType::None);
185         trigger.conditionType = Trigger::ConditionType::IfDomain;
186     } else if (!ifDomainValue.isUndefined())
187         return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
188
189     const JSValue unlessDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-domain"));
190     if (!scope.exception() && unlessDomainValue.isObject()) {
191         if (trigger.conditionType != Trigger::ConditionType::None)
192             return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
193         auto unlessDomain = getDomainList(exec, asObject(unlessDomainValue));
194         if (!unlessDomain.hasValue())
195             return makeUnexpected(unlessDomain.error());
196         trigger.conditions = WTFMove(unlessDomain.value());
197         if (trigger.conditions.isEmpty())
198             return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
199         trigger.conditionType = Trigger::ConditionType::UnlessDomain;
200     } else if (!unlessDomainValue.isUndefined())
201         return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
202
203     const JSValue ifTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-top-url"));
204     if (!scope.exception() && ifTopURLValue.isObject()) {
205         if (trigger.conditionType != Trigger::ConditionType::None)
206             return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
207         auto ifTopURL = getStringList(exec, asObject(ifTopURLValue));
208         if (!ifTopURL.hasValue())
209             return makeUnexpected(ifTopURL.error());
210         trigger.conditions = WTFMove(ifTopURL.value());
211         if (trigger.conditions.isEmpty())
212             return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
213         trigger.conditionType = Trigger::ConditionType::IfTopURL;
214     } else if (!ifTopURLValue.isUndefined())
215         return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
216
217     const JSValue unlessTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-top-url"));
218     if (!scope.exception() && unlessTopURLValue.isObject()) {
219         if (trigger.conditionType != Trigger::ConditionType::None)
220             return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
221         auto unlessTopURL = getStringList(exec, asObject(unlessTopURLValue));
222         if (!unlessTopURL.hasValue())
223             return makeUnexpected(unlessTopURL.error());
224         trigger.conditions = WTFMove(unlessTopURL.value());
225         if (trigger.conditions.isEmpty())
226             return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
227         trigger.conditionType = Trigger::ConditionType::UnlessTopURL;
228     } else if (!unlessTopURLValue.isUndefined())
229         return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
230
231     return WTFMove(trigger);
232 }
233
234 bool isValidCSSSelector(const String& selector)
235 {
236     AtomicString::init();
237     CSSParserContext context(HTMLQuirksMode);
238     CSSParser parser(context);
239     CSSSelectorList selectorList;
240     parser.parseSelector(selector, selectorList);
241     return selectorList.isValid();
242 }
243
244 static Expected<std::optional<Action>, std::error_code> loadAction(ExecState& exec, const JSObject& ruleObject)
245 {
246     VM& vm = exec.vm();
247     auto scope = DECLARE_THROW_SCOPE(vm);
248
249     const JSValue actionObject = ruleObject.get(&exec, Identifier::fromString(&exec, "action"));
250     if (!actionObject || scope.exception() || !actionObject.isObject())
251         return makeUnexpected(ContentExtensionError::JSONInvalidAction);
252
253     const JSValue typeObject = actionObject.get(&exec, Identifier::fromString(&exec, "type"));
254     if (!typeObject || scope.exception() || !typeObject.isString())
255         return makeUnexpected(ContentExtensionError::JSONInvalidActionType);
256
257     String actionType = asString(typeObject)->value(&exec);
258
259     if (actionType == "block")
260         return {{ActionType::BlockLoad}};
261     if (actionType == "ignore-previous-rules")
262         return {{ActionType::IgnorePreviousRules}};
263     if (actionType == "block-cookies")
264         return {{ActionType::BlockCookies}};
265     if (actionType == "css-display-none") {
266         JSValue selector = actionObject.get(&exec, Identifier::fromString(&exec, "selector"));
267         if (!selector || scope.exception() || !selector.isString())
268             return makeUnexpected(ContentExtensionError::JSONInvalidCSSDisplayNoneActionType);
269
270         String selectorString = asString(selector)->value(&exec);
271         if (!isValidCSSSelector(selectorString)) {
272             // Skip rules with invalid selectors to be backwards-compatible.
273             return {std::nullopt};
274         }
275         return {Action(ActionType::CSSDisplayNoneSelector, selectorString)};
276     }
277     if (actionType == "make-https")
278         return {{ActionType::MakeHTTPS}};
279     return makeUnexpected(ContentExtensionError::JSONInvalidActionType);
280 }
281
282 static Expected<std::optional<ContentExtensionRule>, std::error_code> loadRule(ExecState& exec, const JSObject& ruleObject)
283 {
284     auto trigger = loadTrigger(exec, ruleObject);
285     if (!trigger.hasValue())
286         return makeUnexpected(trigger.error());
287
288     auto action = loadAction(exec, ruleObject);
289     if (!action.hasValue())
290         return makeUnexpected(action.error());
291
292     if (action.value())
293         return {{{WTFMove(trigger.value()), WTFMove(action.value().value())}}};
294
295     return {std::nullopt};
296 }
297
298 static Expected<Vector<ContentExtensionRule>, std::error_code> loadEncodedRules(ExecState& exec, String&& ruleJSON)
299 {
300     VM& vm = exec.vm();
301     auto scope = DECLARE_THROW_SCOPE(vm);
302
303     // FIXME: JSONParse should require callbacks instead of an ExecState.
304     const JSValue decodedRules = JSONParse(&exec, ruleJSON);
305
306     if (scope.exception() || !decodedRules)
307         return makeUnexpected(ContentExtensionError::JSONInvalid);
308
309     if (!decodedRules.isObject())
310         return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject);
311
312     const JSObject* topLevelObject = decodedRules.toObject(&exec);
313     if (!topLevelObject || scope.exception())
314         return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject);
315     
316     if (!isJSArray(topLevelObject))
317         return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnArray);
318
319     const JSArray* topLevelArray = jsCast<const JSArray*>(topLevelObject);
320
321     Vector<ContentExtensionRule> ruleList;
322
323     unsigned length = topLevelArray->length();
324     const unsigned maxRuleCount = 50000;
325     if (length > maxRuleCount)
326         return makeUnexpected(ContentExtensionError::JSONTooManyRules);
327     for (unsigned i = 0; i < length; ++i) {
328         const JSValue value = topLevelArray->getIndex(&exec, i);
329         if (scope.exception() || !value)
330             return makeUnexpected(ContentExtensionError::JSONInvalidObjectInTopLevelArray);
331
332         const JSObject* ruleObject = value.toObject(&exec);
333         if (!ruleObject || scope.exception())
334             return makeUnexpected(ContentExtensionError::JSONInvalidRule);
335
336         auto rule = loadRule(exec, *ruleObject);
337         if (!rule.hasValue())
338             return makeUnexpected(rule.error());
339         if (rule.value())
340             ruleList.append(*rule.value());
341     }
342
343     return WTFMove(ruleList);
344 }
345
346 Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(String&& ruleJSON)
347 {
348 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
349     double loadExtensionStartTime = monotonicallyIncreasingTime();
350 #endif
351     RefPtr<VM> vm = VM::create();
352
353     JSLockHolder locker(vm.get());
354     JSGlobalObject* globalObject = JSGlobalObject::create(*vm, JSGlobalObject::createStructure(*vm, jsNull()));
355
356     ExecState* exec = globalObject->globalExec();
357     auto ruleList = loadEncodedRules(*exec, WTFMove(ruleJSON));
358
359     vm = nullptr;
360
361     if (!ruleList.hasValue())
362         return makeUnexpected(ruleList.error());
363
364     if (ruleList->isEmpty())
365         return makeUnexpected(ContentExtensionError::JSONContainsNoRules);
366
367 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
368     double loadExtensionEndTime = monotonicallyIncreasingTime();
369     dataLogF("Time spent loading extension %f\n", (loadExtensionEndTime - loadExtensionStartTime));
370 #endif
371
372     return WTFMove(*ruleList);
373 }
374
375 } // namespace ContentExtensions
376 } // namespace WebCore
377
378 #endif // ENABLE(CONTENT_EXTENSIONS)