Use asString instead of toWTFString, toString, or getString when we already checked...
[WebKit-https.git] / Source / WebCore / contentextensions / ContentExtensionParser.cpp
1 /*
2  * Copyright (C) 2014 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/text/WTFString.h>
44
45 using namespace JSC;
46
47 namespace WebCore {
48
49 namespace ContentExtensions {
50     
51 static bool containsOnlyASCIIWithNoUppercase(const String& domain)
52 {
53     for (unsigned i = 0; i < domain.length(); ++i) {
54         UChar c = domain.at(i);
55         if (!isASCII(c) || isASCIIUpper(c))
56             return false;
57     }
58     return true;
59 }
60     
61 static std::error_code getDomainList(ExecState& exec, const JSObject* arrayObject, Vector<String>& vector)
62 {
63     VM& vm = exec.vm();
64     auto scope = DECLARE_THROW_SCOPE(vm);
65
66     ASSERT(vector.isEmpty());
67     if (!arrayObject || !isJSArray(arrayObject))
68         return ContentExtensionError::JSONInvalidDomainList;
69     const JSArray* array = jsCast<const JSArray*>(arrayObject);
70     
71     unsigned length = array->length();
72     for (unsigned i = 0; i < length; ++i) {
73         const JSValue value = array->getIndex(&exec, i);
74         if (scope.exception() || !value.isString())
75             return ContentExtensionError::JSONInvalidDomainList;
76         
77         // Domains should be punycode encoded lower case.
78         const String& domain = asString(value)->value(&exec);
79         if (domain.isEmpty())
80             return ContentExtensionError::JSONInvalidDomainList;
81         if (!containsOnlyASCIIWithNoUppercase(domain))
82             return ContentExtensionError::JSONDomainNotLowerCaseASCII;
83         vector.append(domain);
84     }
85     return { };
86 }
87
88 static std::error_code getTypeFlags(ExecState& exec, const JSValue& typeValue, ResourceFlags& flags, uint16_t (*stringToType)(const String&))
89 {
90     VM& vm = exec.vm();
91     auto scope = DECLARE_THROW_SCOPE(vm);
92
93     if (!typeValue.isObject())
94         return { };
95
96     const JSObject* object = typeValue.toObject(&exec);
97     ASSERT(!scope.exception());
98     if (!isJSArray(object))
99         return ContentExtensionError::JSONInvalidTriggerFlagsArray;
100
101     const JSArray* array = jsCast<const JSArray*>(object);
102     
103     unsigned length = array->length();
104     for (unsigned i = 0; i < length; ++i) {
105         const JSValue value = array->getIndex(&exec, i);
106         if (scope.exception() || !value)
107             return ContentExtensionError::JSONInvalidObjectInTriggerFlagsArray;
108         
109         String name = value.toWTFString(&exec);
110         uint16_t type = stringToType(name);
111         if (!type)
112             return ContentExtensionError::JSONInvalidStringInTriggerFlagsArray;
113
114         flags |= type;
115     }
116
117     return { };
118 }
119     
120 static std::error_code loadTrigger(ExecState& exec, const JSObject& ruleObject, Trigger& trigger)
121 {
122     VM& vm = exec.vm();
123     auto scope = DECLARE_THROW_SCOPE(vm);
124
125     const JSValue triggerObject = ruleObject.get(&exec, Identifier::fromString(&exec, "trigger"));
126     if (!triggerObject || scope.exception() || !triggerObject.isObject())
127         return ContentExtensionError::JSONInvalidTrigger;
128     
129     const JSValue urlFilterObject = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter"));
130     if (!urlFilterObject || scope.exception() || !urlFilterObject.isString())
131         return ContentExtensionError::JSONInvalidURLFilterInTrigger;
132
133     String urlFilter = asString(urlFilterObject)->value(&exec);
134     if (urlFilter.isEmpty())
135         return ContentExtensionError::JSONInvalidURLFilterInTrigger;
136
137     trigger.urlFilter = urlFilter;
138
139     const JSValue urlFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter-is-case-sensitive"));
140     if (urlFilterCaseValue && !scope.exception() && urlFilterCaseValue.isBoolean())
141         trigger.urlFilterIsCaseSensitive = urlFilterCaseValue.toBoolean(&exec);
142
143     const JSValue resourceTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "resource-type"));
144     if (!scope.exception() && resourceTypeValue.isObject()) {
145         auto typeFlagsError = getTypeFlags(exec, resourceTypeValue, trigger.flags, readResourceType);
146         if (typeFlagsError)
147             return typeFlagsError;
148     } else if (!resourceTypeValue.isUndefined())
149         return ContentExtensionError::JSONInvalidTriggerFlagsArray;
150
151     const JSValue loadTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "load-type"));
152     if (!scope.exception() && loadTypeValue.isObject()) {
153         auto typeFlagsError = getTypeFlags(exec, loadTypeValue, trigger.flags, readLoadType);
154         if (typeFlagsError)
155             return typeFlagsError;
156     } else if (!loadTypeValue.isUndefined())
157         return ContentExtensionError::JSONInvalidTriggerFlagsArray;
158
159     const JSValue ifDomain = triggerObject.get(&exec, Identifier::fromString(&exec, "if-domain"));
160     if (!scope.exception() && ifDomain.isObject()) {
161         auto ifDomainError = getDomainList(exec, asObject(ifDomain), trigger.domains);
162         if (ifDomainError)
163             return ifDomainError;
164         if (trigger.domains.isEmpty())
165             return ContentExtensionError::JSONInvalidDomainList;
166         ASSERT(trigger.domainCondition == Trigger::DomainCondition::None);
167         trigger.domainCondition = Trigger::DomainCondition::IfDomain;
168     } else if (!ifDomain.isUndefined())
169         return ContentExtensionError::JSONInvalidDomainList;
170     
171     const JSValue unlessDomain = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-domain"));
172     if (!scope.exception() && unlessDomain.isObject()) {
173         if (trigger.domainCondition != Trigger::DomainCondition::None)
174             return ContentExtensionError::JSONUnlessAndIfDomain;
175         auto unlessDomainError = getDomainList(exec, asObject(unlessDomain), trigger.domains);
176         if (unlessDomainError)
177             return unlessDomainError;
178         if (trigger.domains.isEmpty())
179             return ContentExtensionError::JSONInvalidDomainList;
180         trigger.domainCondition = Trigger::DomainCondition::UnlessDomain;
181     } else if (!unlessDomain.isUndefined())
182         return ContentExtensionError::JSONInvalidDomainList;
183
184     return { };
185 }
186
187 static bool isValidSelector(const String& selector)
188 {
189     CSSParserContext context(HTMLQuirksMode);
190     CSSParser parser(context);
191     CSSSelectorList selectorList;
192     parser.parseSelector(selector, selectorList);
193     return selectorList.isValid();
194 }
195
196 static std::error_code loadAction(ExecState& exec, const JSObject& ruleObject, Action& action, bool& validSelector)
197 {
198     VM& vm = exec.vm();
199     auto scope = DECLARE_THROW_SCOPE(vm);
200
201     validSelector = true;
202     const JSValue actionObject = ruleObject.get(&exec, Identifier::fromString(&exec, "action"));
203     if (!actionObject || scope.exception() || !actionObject.isObject())
204         return ContentExtensionError::JSONInvalidAction;
205
206     const JSValue typeObject = actionObject.get(&exec, Identifier::fromString(&exec, "type"));
207     if (!typeObject || scope.exception() || !typeObject.isString())
208         return ContentExtensionError::JSONInvalidActionType;
209
210     String actionType = asString(typeObject)->value(&exec);
211
212     if (actionType == "block")
213         action = ActionType::BlockLoad;
214     else if (actionType == "ignore-previous-rules")
215         action = ActionType::IgnorePreviousRules;
216     else if (actionType == "block-cookies")
217         action = ActionType::BlockCookies;
218     else if (actionType == "css-display-none") {
219         JSValue selector = actionObject.get(&exec, Identifier::fromString(&exec, "selector"));
220         if (!selector || scope.exception() || !selector.isString())
221             return ContentExtensionError::JSONInvalidCSSDisplayNoneActionType;
222
223         String selectorString = asString(selector)->value(&exec);
224         if (!isValidSelector(selectorString)) {
225             // Skip rules with invalid selectors to be backwards-compatible.
226             validSelector = false;
227             return { };
228         }
229         action = Action(ActionType::CSSDisplayNoneSelector, selectorString);
230     } else if (actionType == "make-https") {
231         action = ActionType::MakeHTTPS;
232     } else
233         return ContentExtensionError::JSONInvalidActionType;
234
235     return { };
236 }
237
238 static std::error_code loadRule(ExecState& exec, const JSObject& ruleObject, Vector<ContentExtensionRule>& ruleList)
239 {
240     Trigger trigger;
241     auto triggerError = loadTrigger(exec, ruleObject, trigger);
242     if (triggerError)
243         return triggerError;
244
245     Action action;
246     bool validSelector;
247     auto actionError = loadAction(exec, ruleObject, action, validSelector);
248     if (actionError)
249         return actionError;
250
251     if (validSelector)
252         ruleList.append(ContentExtensionRule(trigger, action));
253     return { };
254 }
255
256 static std::error_code loadEncodedRules(ExecState& exec, const String& rules, Vector<ContentExtensionRule>& ruleList)
257 {
258     VM& vm = exec.vm();
259     auto scope = DECLARE_THROW_SCOPE(vm);
260
261     // FIXME: JSONParse should require callbacks instead of an ExecState.
262     const JSValue decodedRules = JSONParse(&exec, rules);
263
264     if (scope.exception() || !decodedRules)
265         return ContentExtensionError::JSONInvalid;
266
267     if (!decodedRules.isObject())
268         return ContentExtensionError::JSONTopLevelStructureNotAnObject;
269
270     const JSObject* topLevelObject = decodedRules.toObject(&exec);
271     if (!topLevelObject || scope.exception())
272         return ContentExtensionError::JSONTopLevelStructureNotAnObject;
273     
274     if (!isJSArray(topLevelObject))
275         return ContentExtensionError::JSONTopLevelStructureNotAnArray;
276
277     const JSArray* topLevelArray = jsCast<const JSArray*>(topLevelObject);
278
279     Vector<ContentExtensionRule> localRuleList;
280
281     unsigned length = topLevelArray->length();
282     const unsigned maxRuleCount = 50000;
283     if (length > maxRuleCount)
284         return ContentExtensionError::JSONTooManyRules;
285     for (unsigned i = 0; i < length; ++i) {
286         const JSValue value = topLevelArray->getIndex(&exec, i);
287         if (scope.exception() || !value)
288             return ContentExtensionError::JSONInvalidObjectInTopLevelArray;
289
290         const JSObject* ruleObject = value.toObject(&exec);
291         if (!ruleObject || scope.exception())
292             return ContentExtensionError::JSONInvalidRule;
293
294         auto error = loadRule(exec, *ruleObject, localRuleList);
295         if (error)
296             return error;
297     }
298
299     ruleList = WTFMove(localRuleList);
300     return { };
301 }
302
303 std::error_code parseRuleList(const String& rules, Vector<ContentExtensionRule>& ruleList)
304 {
305 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
306     double loadExtensionStartTime = monotonicallyIncreasingTime();
307 #endif
308     RefPtr<VM> vm = VM::create();
309
310     JSLockHolder locker(vm.get());
311     JSGlobalObject* globalObject = JSGlobalObject::create(*vm, JSGlobalObject::createStructure(*vm, jsNull()));
312
313     ExecState* exec = globalObject->globalExec();
314     auto error = loadEncodedRules(*exec, rules, ruleList);
315
316     vm = nullptr;
317
318     if (error)
319         return error;
320
321     if (ruleList.isEmpty())
322         return ContentExtensionError::JSONContainsNoRules;
323
324 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
325     double loadExtensionEndTime = monotonicallyIncreasingTime();
326     dataLogF("Time spent loading extension %f\n", (loadExtensionEndTime - loadExtensionStartTime));
327 #endif
328
329     return { };
330 }
331
332 } // namespace ContentExtensions
333 } // namespace WebCore
334
335 #endif // ENABLE(CONTENT_EXTENSIONS)