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 "CSSSelectorList.h"
33#include "ContentExtensionError.h"
34#include "ContentExtensionRule.h"
35#include "ContentExtensionsBackend.h"
36#include "ContentExtensionsDebugging.h"
37#include <JavaScriptCore/JSCInlines.h>
38#include <JavaScriptCore/JSGlobalObject.h>
39#include <JavaScriptCore/JSONObject.h>
40#include <JavaScriptCore/VM.h>
41#include <wtf/Expected.h>
42#include <wtf/text/WTFString.h>
43
44
45namespace WebCore {
46using namespace JSC;
47
48namespace ContentExtensions {
49
50static bool containsOnlyASCIIWithNoUppercase(const String& domain)
51{
52 for (auto character : StringView { domain }.codeUnits()) {
53 if (!isASCII(character) || isASCIIUpper(character))
54 return false;
55 }
56 return true;
57}
58
59static Expected<Vector<String>, std::error_code> getStringList(ExecState& exec, const JSObject* arrayObject)
60{
61 static const ContentExtensionError error = ContentExtensionError::JSONInvalidConditionList;
62 VM& vm = exec.vm();
63 auto scope = DECLARE_THROW_SCOPE(vm);
64
65 if (!arrayObject || !isJSArray(arrayObject))
66 return makeUnexpected(error);
67 const JSArray* array = jsCast<const JSArray*>(arrayObject);
68
69 Vector<String> strings;
70 unsigned length = array->length();
71 for (unsigned i = 0; i < length; ++i) {
72 const JSValue value = array->getIndex(&exec, i);
73 if (scope.exception() || !value.isString())
74 return makeUnexpected(error);
75
76 const String& string = asString(value)->value(&exec);
77 if (string.isEmpty())
78 return makeUnexpected(error);
79 strings.append(string);
80 }
81 return strings;
82}
83
84static Expected<Vector<String>, std::error_code> getDomainList(ExecState& exec, const JSObject* arrayObject)
85{
86 auto strings = getStringList(exec, arrayObject);
87 if (!strings.has_value())
88 return strings;
89 for (auto& domain : strings.value()) {
90 // Domains should be punycode encoded lower case.
91 if (!containsOnlyASCIIWithNoUppercase(domain))
92 return makeUnexpected(ContentExtensionError::JSONDomainNotLowerCaseASCII);
93 }
94 return strings;
95}
96
97static std::error_code getTypeFlags(ExecState& exec, const JSValue& typeValue, ResourceFlags& flags, uint16_t (*stringToType)(const String&))
98{
99 VM& vm = exec.vm();
100 auto scope = DECLARE_THROW_SCOPE(vm);
101
102 if (!typeValue.isObject())
103 return { };
104
105 const JSObject* object = typeValue.toObject(&exec);
106 scope.assertNoException();
107 if (!isJSArray(object))
108 return ContentExtensionError::JSONInvalidTriggerFlagsArray;
109
110 const JSArray* array = jsCast<const JSArray*>(object);
111
112 unsigned length = array->length();
113 for (unsigned i = 0; i < length; ++i) {
114 const JSValue value = array->getIndex(&exec, i);
115 if (scope.exception() || !value)
116 return ContentExtensionError::JSONInvalidObjectInTriggerFlagsArray;
117
118 String name = value.toWTFString(&exec);
119 uint16_t type = stringToType(name);
120 if (!type)
121 return ContentExtensionError::JSONInvalidStringInTriggerFlagsArray;
122
123 flags |= type;
124 }
125
126 return { };
127}
128
129static Expected<Trigger, std::error_code> loadTrigger(ExecState& exec, const JSObject& ruleObject)
130{
131 VM& vm = exec.vm();
132 auto scope = DECLARE_THROW_SCOPE(vm);
133
134 const JSValue triggerObject = ruleObject.get(&exec, Identifier::fromString(&exec, "trigger"));
135 if (!triggerObject || scope.exception() || !triggerObject.isObject())
136 return makeUnexpected(ContentExtensionError::JSONInvalidTrigger);
137
138 const JSValue urlFilterObject = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter"));
139 if (!urlFilterObject || scope.exception() || !urlFilterObject.isString())
140 return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger);
141
142 String urlFilter = asString(urlFilterObject)->value(&exec);
143 if (urlFilter.isEmpty())
144 return makeUnexpected(ContentExtensionError::JSONInvalidURLFilterInTrigger);
145
146 Trigger trigger;
147 trigger.urlFilter = urlFilter;
148
149 const JSValue urlFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "url-filter-is-case-sensitive"));
150 if (urlFilterCaseValue && !scope.exception() && urlFilterCaseValue.isBoolean())
151 trigger.urlFilterIsCaseSensitive = urlFilterCaseValue.toBoolean(&exec);
152
153 const JSValue topURLFilterCaseValue = triggerObject.get(&exec, Identifier::fromString(&exec, "top-url-filter-is-case-sensitive"));
154 if (topURLFilterCaseValue && !scope.exception() && topURLFilterCaseValue.isBoolean())
155 trigger.topURLConditionIsCaseSensitive = topURLFilterCaseValue.toBoolean(&exec);
156
157 const JSValue resourceTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "resource-type"));
158 if (!scope.exception() && resourceTypeValue.isObject()) {
159 auto typeFlagsError = getTypeFlags(exec, resourceTypeValue, trigger.flags, readResourceType);
160 if (typeFlagsError)
161 return makeUnexpected(typeFlagsError);
162 } else if (!resourceTypeValue.isUndefined())
163 return makeUnexpected(ContentExtensionError::JSONInvalidTriggerFlagsArray);
164
165 const JSValue loadTypeValue = triggerObject.get(&exec, Identifier::fromString(&exec, "load-type"));
166 if (!scope.exception() && loadTypeValue.isObject()) {
167 auto typeFlagsError = getTypeFlags(exec, loadTypeValue, trigger.flags, readLoadType);
168 if (typeFlagsError)
169 return makeUnexpected(typeFlagsError);
170 } else if (!loadTypeValue.isUndefined())
171 return makeUnexpected(ContentExtensionError::JSONInvalidTriggerFlagsArray);
172
173 const JSValue ifDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-domain"));
174 if (!scope.exception() && ifDomainValue.isObject()) {
175 auto ifDomain = getDomainList(exec, asObject(ifDomainValue));
176 if (!ifDomain.has_value())
177 return makeUnexpected(ifDomain.error());
178 trigger.conditions = WTFMove(ifDomain.value());
179 if (trigger.conditions.isEmpty())
180 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
181 ASSERT(trigger.conditionType == Trigger::ConditionType::None);
182 trigger.conditionType = Trigger::ConditionType::IfDomain;
183 } else if (!ifDomainValue.isUndefined())
184 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
185
186 const JSValue unlessDomainValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-domain"));
187 if (!scope.exception() && unlessDomainValue.isObject()) {
188 if (trigger.conditionType != Trigger::ConditionType::None)
189 return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
190 auto unlessDomain = getDomainList(exec, asObject(unlessDomainValue));
191 if (!unlessDomain.has_value())
192 return makeUnexpected(unlessDomain.error());
193 trigger.conditions = WTFMove(unlessDomain.value());
194 if (trigger.conditions.isEmpty())
195 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
196 trigger.conditionType = Trigger::ConditionType::UnlessDomain;
197 } else if (!unlessDomainValue.isUndefined())
198 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
199
200 const JSValue ifTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "if-top-url"));
201 if (!scope.exception() && ifTopURLValue.isObject()) {
202 if (trigger.conditionType != Trigger::ConditionType::None)
203 return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
204 auto ifTopURL = getStringList(exec, asObject(ifTopURLValue));
205 if (!ifTopURL.has_value())
206 return makeUnexpected(ifTopURL.error());
207 trigger.conditions = WTFMove(ifTopURL.value());
208 if (trigger.conditions.isEmpty())
209 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
210 trigger.conditionType = Trigger::ConditionType::IfTopURL;
211 } else if (!ifTopURLValue.isUndefined())
212 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
213
214 const JSValue unlessTopURLValue = triggerObject.get(&exec, Identifier::fromString(&exec, "unless-top-url"));
215 if (!scope.exception() && unlessTopURLValue.isObject()) {
216 if (trigger.conditionType != Trigger::ConditionType::None)
217 return makeUnexpected(ContentExtensionError::JSONMultipleConditions);
218 auto unlessTopURL = getStringList(exec, asObject(unlessTopURLValue));
219 if (!unlessTopURL.has_value())
220 return makeUnexpected(unlessTopURL.error());
221 trigger.conditions = WTFMove(unlessTopURL.value());
222 if (trigger.conditions.isEmpty())
223 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
224 trigger.conditionType = Trigger::ConditionType::UnlessTopURL;
225 } else if (!unlessTopURLValue.isUndefined())
226 return makeUnexpected(ContentExtensionError::JSONInvalidConditionList);
227
228 return trigger;
229}
230
231bool isValidCSSSelector(const String& selector)
232{
233 ASSERT(isMainThread());
234 AtomicString::init();
235 QualifiedName::init();
236 CSSParserContext context(HTMLQuirksMode);
237 CSSParser parser(context);
238 CSSSelectorList selectorList;
239 parser.parseSelector(selector, selectorList);
240 return selectorList.isValid();
241}
242
243static Expected<Optional<Action>, std::error_code> loadAction(ExecState& exec, const JSObject& ruleObject)
244{
245 VM& vm = exec.vm();
246 auto scope = DECLARE_THROW_SCOPE(vm);
247
248 const JSValue actionObject = ruleObject.get(&exec, Identifier::fromString(&exec, "action"));
249 if (scope.exception() || !actionObject.isObject())
250 return makeUnexpected(ContentExtensionError::JSONInvalidAction);
251
252 const JSValue typeObject = actionObject.get(&exec, Identifier::fromString(&exec, "type"));
253 if (scope.exception() || !typeObject.isString())
254 return makeUnexpected(ContentExtensionError::JSONInvalidActionType);
255
256 String actionType = asString(typeObject)->value(&exec);
257
258 if (actionType == "block")
259 return { Action(ActionType::BlockLoad) };
260 if (actionType == "ignore-previous-rules")
261 return { Action(ActionType::IgnorePreviousRules) };
262 if (actionType == "block-cookies")
263 return { Action(ActionType::BlockCookies) };
264 if (actionType == "css-display-none") {
265 JSValue selector = actionObject.get(&exec, Identifier::fromString(&exec, "selector"));
266 if (scope.exception() || !selector.isString())
267 return makeUnexpected(ContentExtensionError::JSONInvalidCSSDisplayNoneActionType);
268
269 String selectorString = asString(selector)->value(&exec);
270 if (!isValidCSSSelector(selectorString)) {
271 // Skip rules with invalid selectors to be backwards-compatible.
272 return { WTF::nullopt };
273 }
274 return { Action(ActionType::CSSDisplayNoneSelector, selectorString) };
275 }
276 if (actionType == "make-https")
277 return { Action(ActionType::MakeHTTPS) };
278 if (actionType == "notify") {
279 JSValue notification = actionObject.get(&exec, Identifier::fromString(&exec, "notification"));
280 if (scope.exception() || !notification.isString())
281 return makeUnexpected(ContentExtensionError::JSONInvalidNotification);
282 return { Action(ActionType::Notify, asString(notification)->value(&exec)) };
283 }
284 return makeUnexpected(ContentExtensionError::JSONInvalidActionType);
285}
286
287static Expected<Optional<ContentExtensionRule>, std::error_code> loadRule(ExecState& exec, const JSObject& ruleObject)
288{
289 auto trigger = loadTrigger(exec, ruleObject);
290 if (!trigger.has_value())
291 return makeUnexpected(trigger.error());
292
293 auto action = loadAction(exec, ruleObject);
294 if (!action.has_value())
295 return makeUnexpected(action.error());
296
297 if (action.value())
298 return {{{ WTFMove(trigger.value()), WTFMove(action.value().value()) }}};
299
300 return { WTF::nullopt };
301}
302
303static Expected<Vector<ContentExtensionRule>, std::error_code> loadEncodedRules(ExecState& exec, const String& ruleJSON)
304{
305 VM& vm = exec.vm();
306 auto scope = DECLARE_THROW_SCOPE(vm);
307
308 // FIXME: JSONParse should require callbacks instead of an ExecState.
309 const JSValue decodedRules = JSONParse(&exec, ruleJSON);
310
311 if (scope.exception() || !decodedRules)
312 return makeUnexpected(ContentExtensionError::JSONInvalid);
313
314 if (!decodedRules.isObject())
315 return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject);
316
317 const JSObject* topLevelObject = decodedRules.toObject(&exec);
318 if (!topLevelObject || scope.exception())
319 return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnObject);
320
321 if (!isJSArray(topLevelObject))
322 return makeUnexpected(ContentExtensionError::JSONTopLevelStructureNotAnArray);
323
324 const JSArray* topLevelArray = jsCast<const JSArray*>(topLevelObject);
325
326 Vector<ContentExtensionRule> ruleList;
327
328 unsigned length = topLevelArray->length();
329 const unsigned maxRuleCount = 50000;
330 if (length > maxRuleCount)
331 return makeUnexpected(ContentExtensionError::JSONTooManyRules);
332 for (unsigned i = 0; i < length; ++i) {
333 const JSValue value = topLevelArray->getIndex(&exec, i);
334 if (scope.exception() || !value)
335 return makeUnexpected(ContentExtensionError::JSONInvalidObjectInTopLevelArray);
336
337 const JSObject* ruleObject = value.toObject(&exec);
338 if (!ruleObject || scope.exception())
339 return makeUnexpected(ContentExtensionError::JSONInvalidRule);
340
341 auto rule = loadRule(exec, *ruleObject);
342 if (!rule.has_value())
343 return makeUnexpected(rule.error());
344 if (rule.value())
345 ruleList.append(WTFMove(*rule.value()));
346 }
347
348 return ruleList;
349}
350
351Expected<Vector<ContentExtensionRule>, std::error_code> parseRuleList(const String& ruleJSON)
352{
353#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
354 MonotonicTime loadExtensionStartTime = MonotonicTime::now();
355#endif
356 RefPtr<VM> vm = VM::create();
357
358 JSLockHolder locker(vm.get());
359 JSGlobalObject* globalObject = JSGlobalObject::create(*vm, JSGlobalObject::createStructure(*vm, jsNull()));
360
361 ExecState* exec = globalObject->globalExec();
362 auto ruleList = loadEncodedRules(*exec, ruleJSON);
363
364 vm = nullptr;
365
366 if (!ruleList.has_value())
367 return makeUnexpected(ruleList.error());
368
369 if (ruleList->isEmpty())
370 return makeUnexpected(ContentExtensionError::JSONContainsNoRules);
371
372#if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
373 MonotonicTime loadExtensionEndTime = MonotonicTime::now();
374 dataLogF("Time spent loading extension %f\n", (loadExtensionEndTime - loadExtensionStartTime).seconds());
375#endif
376
377 return ruleList;
378}
379
380} // namespace ContentExtensions
381} // namespace WebCore
382
383#endif // ENABLE(CONTENT_EXTENSIONS)
384