|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 sw=2 et tw=78: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 #include "nsCOMPtr.h" |
|
8 #include "nsIDocument.h" |
|
9 #include "nsIScriptTimeoutHandler.h" |
|
10 #include "nsIXPConnect.h" |
|
11 #include "nsJSUtils.h" |
|
12 #include "nsContentUtils.h" |
|
13 #include "nsError.h" |
|
14 #include "nsGlobalWindow.h" |
|
15 #include "nsIContentSecurityPolicy.h" |
|
16 #include "mozilla/Attributes.h" |
|
17 #include "mozilla/Likely.h" |
|
18 #include <algorithm> |
|
19 #include "mozilla/dom/FunctionBinding.h" |
|
20 #include "nsAXPCNativeCallContext.h" |
|
21 |
|
22 static const char kSetIntervalStr[] = "setInterval"; |
|
23 static const char kSetTimeoutStr[] = "setTimeout"; |
|
24 |
|
25 using namespace mozilla; |
|
26 using namespace mozilla::dom; |
|
27 |
|
28 // Our JS nsIScriptTimeoutHandler implementation. |
|
29 class nsJSScriptTimeoutHandler MOZ_FINAL : public nsIScriptTimeoutHandler |
|
30 { |
|
31 public: |
|
32 // nsISupports |
|
33 NS_DECL_CYCLE_COLLECTING_ISUPPORTS |
|
34 NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(nsJSScriptTimeoutHandler) |
|
35 |
|
36 nsJSScriptTimeoutHandler(); |
|
37 // This will call SwapElements on aArguments with an empty array. |
|
38 nsJSScriptTimeoutHandler(nsGlobalWindow *aWindow, Function& aFunction, |
|
39 FallibleTArray<JS::Heap<JS::Value> >& aArguments, |
|
40 ErrorResult& aError); |
|
41 nsJSScriptTimeoutHandler(JSContext* aCx, nsGlobalWindow *aWindow, |
|
42 const nsAString& aExpression, bool* aAllowEval, |
|
43 ErrorResult& aError); |
|
44 ~nsJSScriptTimeoutHandler(); |
|
45 |
|
46 virtual const char16_t *GetHandlerText(); |
|
47 virtual Function* GetCallback() |
|
48 { |
|
49 return mFunction; |
|
50 } |
|
51 virtual void GetLocation(const char **aFileName, uint32_t *aLineNo) |
|
52 { |
|
53 *aFileName = mFileName.get(); |
|
54 *aLineNo = mLineNo; |
|
55 } |
|
56 |
|
57 virtual const nsTArray<JS::Value>& GetArgs() |
|
58 { |
|
59 return mArgs; |
|
60 } |
|
61 |
|
62 nsresult Init(nsGlobalWindow *aWindow, bool *aIsInterval, |
|
63 int32_t *aInterval, bool* aAllowEval); |
|
64 |
|
65 void ReleaseJSObjects(); |
|
66 |
|
67 private: |
|
68 // filename, line number and JS language version string of the |
|
69 // caller of setTimeout() |
|
70 nsCString mFileName; |
|
71 uint32_t mLineNo; |
|
72 nsTArray<JS::Heap<JS::Value> > mArgs; |
|
73 |
|
74 // The expression to evaluate or function to call. If mFunction is non-null |
|
75 // it should be used, else use mExpr. |
|
76 nsString mExpr; |
|
77 nsRefPtr<Function> mFunction; |
|
78 }; |
|
79 |
|
80 |
|
81 // nsJSScriptTimeoutHandler |
|
82 // QueryInterface implementation for nsJSScriptTimeoutHandler |
|
83 NS_IMPL_CYCLE_COLLECTION_CLASS(nsJSScriptTimeoutHandler) |
|
84 |
|
85 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsJSScriptTimeoutHandler) |
|
86 tmp->ReleaseJSObjects(); |
|
87 NS_IMPL_CYCLE_COLLECTION_UNLINK_END |
|
88 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(nsJSScriptTimeoutHandler) |
|
89 if (MOZ_UNLIKELY(cb.WantDebugInfo())) { |
|
90 nsAutoCString name("nsJSScriptTimeoutHandler"); |
|
91 if (tmp->mFunction) { |
|
92 JSFunction* fun = |
|
93 JS_GetObjectFunction(js::UncheckedUnwrap(tmp->mFunction->Callable())); |
|
94 if (fun && JS_GetFunctionId(fun)) { |
|
95 JSFlatString *funId = JS_ASSERT_STRING_IS_FLAT(JS_GetFunctionId(fun)); |
|
96 size_t size = 1 + JS_PutEscapedFlatString(nullptr, 0, funId, 0); |
|
97 char *funIdName = new char[size]; |
|
98 if (funIdName) { |
|
99 JS_PutEscapedFlatString(funIdName, size, funId, 0); |
|
100 name.AppendLiteral(" ["); |
|
101 name.Append(funIdName); |
|
102 delete[] funIdName; |
|
103 name.AppendLiteral("]"); |
|
104 } |
|
105 } |
|
106 } else { |
|
107 name.AppendLiteral(" ["); |
|
108 name.Append(tmp->mFileName); |
|
109 name.AppendLiteral(":"); |
|
110 name.AppendInt(tmp->mLineNo); |
|
111 name.AppendLiteral("]"); |
|
112 } |
|
113 cb.DescribeRefCountedNode(tmp->mRefCnt.get(), name.get()); |
|
114 } |
|
115 else { |
|
116 NS_IMPL_CYCLE_COLLECTION_DESCRIBE(nsJSScriptTimeoutHandler, |
|
117 tmp->mRefCnt.get()) |
|
118 } |
|
119 |
|
120 if (tmp->mFunction) { |
|
121 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFunction) |
|
122 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS |
|
123 } |
|
124 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END |
|
125 |
|
126 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsJSScriptTimeoutHandler) |
|
127 for (uint32_t i = 0; i < tmp->mArgs.Length(); ++i) { |
|
128 NS_IMPL_CYCLE_COLLECTION_TRACE_JSVAL_MEMBER_CALLBACK(mArgs[i]) |
|
129 } |
|
130 NS_IMPL_CYCLE_COLLECTION_TRACE_END |
|
131 |
|
132 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsJSScriptTimeoutHandler) |
|
133 NS_INTERFACE_MAP_ENTRY(nsIScriptTimeoutHandler) |
|
134 NS_INTERFACE_MAP_ENTRY(nsISupports) |
|
135 NS_INTERFACE_MAP_END |
|
136 |
|
137 NS_IMPL_CYCLE_COLLECTING_ADDREF(nsJSScriptTimeoutHandler) |
|
138 NS_IMPL_CYCLE_COLLECTING_RELEASE(nsJSScriptTimeoutHandler) |
|
139 |
|
140 static bool |
|
141 CheckCSPForEval(JSContext* aCx, nsGlobalWindow* aWindow, ErrorResult& aError) |
|
142 { |
|
143 // if CSP is enabled, and setTimeout/setInterval was called with a string, |
|
144 // disable the registration and log an error |
|
145 nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); |
|
146 if (!doc) { |
|
147 // if there's no document, we don't have to do anything. |
|
148 return true; |
|
149 } |
|
150 |
|
151 nsCOMPtr<nsIContentSecurityPolicy> csp; |
|
152 aError = doc->NodePrincipal()->GetCsp(getter_AddRefs(csp)); |
|
153 if (aError.Failed()) { |
|
154 return false; |
|
155 } |
|
156 |
|
157 if (!csp) { |
|
158 return true; |
|
159 } |
|
160 |
|
161 bool allowsEval = true; |
|
162 bool reportViolation = false; |
|
163 aError = csp->GetAllowsEval(&reportViolation, &allowsEval); |
|
164 if (aError.Failed()) { |
|
165 return false; |
|
166 } |
|
167 |
|
168 if (reportViolation) { |
|
169 // TODO : need actual script sample in violation report. |
|
170 NS_NAMED_LITERAL_STRING(scriptSample, |
|
171 "call to eval() or related function blocked by CSP"); |
|
172 |
|
173 // Get the calling location. |
|
174 uint32_t lineNum = 0; |
|
175 const char *fileName; |
|
176 nsAutoString fileNameString; |
|
177 if (nsJSUtils::GetCallingLocation(aCx, &fileName, &lineNum)) { |
|
178 AppendUTF8toUTF16(fileName, fileNameString); |
|
179 } else { |
|
180 fileNameString.AssignLiteral("unknown"); |
|
181 } |
|
182 |
|
183 csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, |
|
184 fileNameString, scriptSample, lineNum, |
|
185 EmptyString(), EmptyString()); |
|
186 } |
|
187 |
|
188 return allowsEval; |
|
189 } |
|
190 |
|
191 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler() : |
|
192 mLineNo(0) |
|
193 { |
|
194 } |
|
195 |
|
196 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(nsGlobalWindow *aWindow, |
|
197 Function& aFunction, |
|
198 FallibleTArray<JS::Heap<JS::Value> >& aArguments, |
|
199 ErrorResult& aError) : |
|
200 mLineNo(0), |
|
201 mFunction(&aFunction) |
|
202 { |
|
203 if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { |
|
204 // This window was already closed, or never properly initialized, |
|
205 // don't let a timer be scheduled on such a window. |
|
206 aError.Throw(NS_ERROR_NOT_INITIALIZED); |
|
207 return; |
|
208 } |
|
209 |
|
210 mozilla::HoldJSObjects(this); |
|
211 mArgs.SwapElements(aArguments); |
|
212 } |
|
213 |
|
214 nsJSScriptTimeoutHandler::nsJSScriptTimeoutHandler(JSContext* aCx, |
|
215 nsGlobalWindow *aWindow, |
|
216 const nsAString& aExpression, |
|
217 bool* aAllowEval, |
|
218 ErrorResult& aError) : |
|
219 mLineNo(0), |
|
220 mExpr(aExpression) |
|
221 { |
|
222 if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { |
|
223 // This window was already closed, or never properly initialized, |
|
224 // don't let a timer be scheduled on such a window. |
|
225 aError.Throw(NS_ERROR_NOT_INITIALIZED); |
|
226 return; |
|
227 } |
|
228 |
|
229 *aAllowEval = CheckCSPForEval(aCx, aWindow, aError); |
|
230 if (aError.Failed() || !*aAllowEval) { |
|
231 return; |
|
232 } |
|
233 |
|
234 // Get the calling location. |
|
235 const char *filename; |
|
236 if (nsJSUtils::GetCallingLocation(aCx, &filename, &mLineNo)) { |
|
237 mFileName.Assign(filename); |
|
238 } |
|
239 } |
|
240 |
|
241 nsJSScriptTimeoutHandler::~nsJSScriptTimeoutHandler() |
|
242 { |
|
243 ReleaseJSObjects(); |
|
244 } |
|
245 |
|
246 void |
|
247 nsJSScriptTimeoutHandler::ReleaseJSObjects() |
|
248 { |
|
249 if (mFunction) { |
|
250 mFunction = nullptr; |
|
251 mArgs.Clear(); |
|
252 mozilla::DropJSObjects(this); |
|
253 } |
|
254 } |
|
255 |
|
256 nsresult |
|
257 nsJSScriptTimeoutHandler::Init(nsGlobalWindow *aWindow, bool *aIsInterval, |
|
258 int32_t *aInterval, bool *aAllowEval) |
|
259 { |
|
260 if (!aWindow->GetContextInternal() || !aWindow->FastGetGlobalJSObject()) { |
|
261 // This window was already closed, or never properly initialized, |
|
262 // don't let a timer be scheduled on such a window. |
|
263 |
|
264 return NS_ERROR_NOT_INITIALIZED; |
|
265 } |
|
266 |
|
267 nsAXPCNativeCallContext *ncc = nullptr; |
|
268 nsresult rv = nsContentUtils::XPConnect()-> |
|
269 GetCurrentNativeCallContext(&ncc); |
|
270 NS_ENSURE_SUCCESS(rv, rv); |
|
271 |
|
272 if (!ncc) |
|
273 return NS_ERROR_NOT_AVAILABLE; |
|
274 |
|
275 JSContext *cx = nullptr; |
|
276 |
|
277 rv = ncc->GetJSContext(&cx); |
|
278 NS_ENSURE_SUCCESS(rv, rv); |
|
279 |
|
280 uint32_t argc; |
|
281 JS::Value *argv = nullptr; |
|
282 |
|
283 ncc->GetArgc(&argc); |
|
284 ncc->GetArgvPtr(&argv); |
|
285 |
|
286 JS::Rooted<JSFlatString*> expr(cx); |
|
287 JS::Rooted<JSObject*> funobj(cx); |
|
288 |
|
289 if (argc < 1) { |
|
290 ::JS_ReportError(cx, "Function %s requires at least 2 parameter", |
|
291 *aIsInterval ? kSetIntervalStr : kSetTimeoutStr); |
|
292 return NS_ERROR_DOM_TYPE_ERR; |
|
293 } |
|
294 |
|
295 int32_t interval = 0; |
|
296 if (argc > 1) { |
|
297 JS::Rooted<JS::Value> arg(cx, argv[1]); |
|
298 |
|
299 if (!JS::ToInt32(cx, arg, &interval)) { |
|
300 ::JS_ReportError(cx, |
|
301 "Second argument to %s must be a millisecond interval", |
|
302 aIsInterval ? kSetIntervalStr : kSetTimeoutStr); |
|
303 return NS_ERROR_DOM_TYPE_ERR; |
|
304 } |
|
305 } |
|
306 |
|
307 if (argc == 1) { |
|
308 // If no interval was specified, treat this like a timeout, to avoid |
|
309 // setting an interval of 0 milliseconds. |
|
310 *aIsInterval = false; |
|
311 } |
|
312 |
|
313 JS::Rooted<JS::Value> arg(cx, argv[0]); |
|
314 switch (::JS_TypeOfValue(cx, arg)) { |
|
315 case JSTYPE_FUNCTION: |
|
316 funobj = &arg.toObject(); |
|
317 break; |
|
318 |
|
319 case JSTYPE_STRING: |
|
320 case JSTYPE_OBJECT: |
|
321 { |
|
322 JSString *str = JS::ToString(cx, arg); |
|
323 if (!str) |
|
324 return NS_ERROR_OUT_OF_MEMORY; |
|
325 |
|
326 expr = ::JS_FlattenString(cx, str); |
|
327 if (!expr) |
|
328 return NS_ERROR_OUT_OF_MEMORY; |
|
329 |
|
330 argv[0] = JS::StringValue(str); |
|
331 } |
|
332 break; |
|
333 |
|
334 default: |
|
335 ::JS_ReportError(cx, "useless %s call (missing quotes around argument?)", |
|
336 *aIsInterval ? kSetIntervalStr : kSetTimeoutStr); |
|
337 |
|
338 // Return an error that nsGlobalWindow can recognize and turn into NS_OK. |
|
339 return NS_ERROR_DOM_TYPE_ERR; |
|
340 } |
|
341 |
|
342 if (expr) { |
|
343 // if CSP is enabled, and setTimeout/setInterval was called with a string, |
|
344 // disable the registration and log an error |
|
345 ErrorResult error; |
|
346 *aAllowEval = CheckCSPForEval(cx, aWindow, error); |
|
347 if (error.Failed() || !*aAllowEval) { |
|
348 return error.ErrorCode(); |
|
349 } |
|
350 |
|
351 mExpr.Append(JS_GetFlatStringChars(expr), |
|
352 JS_GetStringLength(JS_FORGET_STRING_FLATNESS(expr))); |
|
353 |
|
354 // Get the calling location. |
|
355 const char *filename; |
|
356 if (nsJSUtils::GetCallingLocation(cx, &filename, &mLineNo)) { |
|
357 mFileName.Assign(filename); |
|
358 } |
|
359 } else if (funobj) { |
|
360 *aAllowEval = true; |
|
361 |
|
362 mozilla::HoldJSObjects(this); |
|
363 |
|
364 mFunction = new Function(funobj, GetIncumbentGlobal()); |
|
365 |
|
366 // Create our arg array. argc is the number of arguments passed |
|
367 // to setTimeout or setInterval; the first two are our callback |
|
368 // and the delay, so only arguments after that need to go in our |
|
369 // array. |
|
370 // std::max(argc - 2, 0) wouldn't work right because argc is unsigned. |
|
371 uint32_t argCount = std::max(argc, 2u) - 2; |
|
372 |
|
373 FallibleTArray<JS::Heap<JS::Value> > args; |
|
374 if (!args.SetCapacity(argCount)) { |
|
375 // No need to drop here, since we already have a non-null mFunction |
|
376 return NS_ERROR_OUT_OF_MEMORY; |
|
377 } |
|
378 for (uint32_t idx = 0; idx < argCount; ++idx) { |
|
379 *args.AppendElement() = argv[idx + 2]; |
|
380 } |
|
381 args.SwapElements(mArgs); |
|
382 } else { |
|
383 NS_WARNING("No func and no expr - why are we here?"); |
|
384 } |
|
385 *aInterval = interval; |
|
386 return NS_OK; |
|
387 } |
|
388 |
|
389 const char16_t * |
|
390 nsJSScriptTimeoutHandler::GetHandlerText() |
|
391 { |
|
392 NS_ASSERTION(!mFunction, "No expression, so no handler text!"); |
|
393 return mExpr.get(); |
|
394 } |
|
395 |
|
396 nsresult NS_CreateJSTimeoutHandler(nsGlobalWindow *aWindow, |
|
397 bool *aIsInterval, |
|
398 int32_t *aInterval, |
|
399 nsIScriptTimeoutHandler **aRet) |
|
400 { |
|
401 *aRet = nullptr; |
|
402 nsRefPtr<nsJSScriptTimeoutHandler> handler = new nsJSScriptTimeoutHandler(); |
|
403 bool allowEval; |
|
404 nsresult rv = handler->Init(aWindow, aIsInterval, aInterval, &allowEval); |
|
405 if (NS_FAILED(rv) || !allowEval) { |
|
406 return rv; |
|
407 } |
|
408 |
|
409 handler.forget(aRet); |
|
410 |
|
411 return NS_OK; |
|
412 } |
|
413 |
|
414 already_AddRefed<nsIScriptTimeoutHandler> |
|
415 NS_CreateJSTimeoutHandler(nsGlobalWindow *aWindow, Function& aFunction, |
|
416 const Sequence<JS::Value>& aArguments, |
|
417 ErrorResult& aError) |
|
418 { |
|
419 FallibleTArray<JS::Heap<JS::Value> > args; |
|
420 if (!args.AppendElements(aArguments)) { |
|
421 aError.Throw(NS_ERROR_OUT_OF_MEMORY); |
|
422 return 0; |
|
423 } |
|
424 |
|
425 nsRefPtr<nsJSScriptTimeoutHandler> handler = |
|
426 new nsJSScriptTimeoutHandler(aWindow, aFunction, args, aError); |
|
427 return aError.Failed() ? nullptr : handler.forget(); |
|
428 } |
|
429 |
|
430 already_AddRefed<nsIScriptTimeoutHandler> |
|
431 NS_CreateJSTimeoutHandler(JSContext* aCx, nsGlobalWindow *aWindow, |
|
432 const nsAString& aExpression, ErrorResult& aError) |
|
433 { |
|
434 ErrorResult rv; |
|
435 bool allowEval = false; |
|
436 nsRefPtr<nsJSScriptTimeoutHandler> handler = |
|
437 new nsJSScriptTimeoutHandler(aCx, aWindow, aExpression, &allowEval, rv); |
|
438 if (rv.Failed() || !allowEval) { |
|
439 return nullptr; |
|
440 } |
|
441 |
|
442 return handler.forget(); |
|
443 } |