1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
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/. */
7 #include "mozJSSubScriptLoader.h"
8 #include "js/experimental/JSStencil.h"
9 #include "mozJSModuleLoader.h"
10 #include "mozJSLoaderUtils.h"
13 #include "nsIIOService.h"
14 #include "nsIChannel.h"
15 #include "nsIInputStream.h"
17 #include "nsNetUtil.h"
20 #include "jsfriendapi.h"
21 #include "xpcprivate.h" // xpc::OptionsBase
22 #include "js/CompilationAndEvaluation.h" // JS::Compile
23 #include "js/CompileOptions.h" // JS::ReadOnlyCompileOptions, JS::DecodeOptions
24 #include "js/EnvironmentChain.h" // JS::EnvironmentChain
25 #include "js/friend/JSMEnvironment.h" // JS::ExecuteInJSMEnvironment, JS::IsJSMEnvironment
26 #include "js/SourceText.h" // JS::Source{Ownership,Text}
27 #include "js/Wrapper.h"
29 #include "mozilla/ContentPrincipal.h"
30 #include "mozilla/dom/ScriptLoader.h"
31 #include "mozilla/ProfilerLabels.h"
32 #include "mozilla/ProfilerMarkers.h"
33 #include "mozilla/ScriptPreloader.h"
34 #include "mozilla/SystemPrincipal.h"
35 #include "mozilla/scache/StartupCache.h"
36 #include "mozilla/scache/StartupCacheUtils.h"
37 #include "mozilla/Unused.h"
38 #include "mozilla/Utf8.h" // mozilla::Utf8Unit
39 #include "nsContentUtils.h"
42 using namespace mozilla::scache
;
45 using namespace mozilla
;
46 using namespace mozilla::dom
;
48 class MOZ_STACK_CLASS LoadSubScriptOptions
: public OptionsBase
{
50 explicit LoadSubScriptOptions(JSContext
* cx
= xpc_GetSafeJSContext(),
51 JSObject
* options
= nullptr)
52 : OptionsBase(cx
, options
),
55 wantReturnValue(false) {}
57 virtual bool Parse() override
{
58 return ParseObject("target", &target
) &&
59 ParseBoolean("ignoreCache", &ignoreCache
) &&
60 ParseBoolean("wantReturnValue", &wantReturnValue
);
68 /* load() error msgs, XXX localize? */
69 #define LOAD_ERROR_NOSERVICE "Error creating IO Service."
70 #define LOAD_ERROR_NOURI "Error creating URI (invalid URL scheme?)"
71 #define LOAD_ERROR_NOSCHEME "Failed to get URI scheme. This is bad."
72 #define LOAD_ERROR_URI_NOT_LOCAL "Trying to load a non-local URI."
73 #define LOAD_ERROR_NOSTREAM "Error opening input stream (invalid filename?)"
74 #define LOAD_ERROR_NOCONTENT "ContentLength not available (not a local URL?)"
75 #define LOAD_ERROR_BADCHARSET "Error converting to specified charset"
76 #define LOAD_ERROR_NOSPEC "Failed to get URI spec. This is bad."
77 #define LOAD_ERROR_CONTENTTOOBIG "ContentLength is too large"
79 mozJSSubScriptLoader::mozJSSubScriptLoader() = default;
81 mozJSSubScriptLoader::~mozJSSubScriptLoader() = default;
83 NS_IMPL_ISUPPORTS(mozJSSubScriptLoader
, mozIJSSubScriptLoader
)
85 #define JSSUB_CACHE_PREFIX(aScopeType, aCompilationTarget) \
86 "jssubloader/" aScopeType "/" aCompilationTarget
88 static void SubscriptCachePath(JSContext
* cx
, nsIURI
* uri
,
89 JS::HandleObject targetObj
,
90 nsACString
& cachePath
) {
91 // StartupCache must distinguish between non-syntactic vs global when
92 // computing the cache key.
93 if (!JS_IsGlobalObject(targetObj
)) {
94 PathifyURI(JSSUB_CACHE_PREFIX("non-syntactic", "script"), uri
, cachePath
);
96 PathifyURI(JSSUB_CACHE_PREFIX("global", "script"), uri
, cachePath
);
100 static void ReportError(JSContext
* cx
, const nsACString
& msg
) {
101 NS_ConvertUTF8toUTF16
ucMsg(msg
);
104 if (xpc::NonVoidStringToJsval(cx
, ucMsg
, &exn
)) {
105 JS_SetPendingException(cx
, exn
);
109 static void ReportError(JSContext
* cx
, const char* origMsg
, nsIURI
* uri
) {
111 ReportError(cx
, nsDependentCString(origMsg
));
116 nsresult rv
= uri
->GetSpec(spec
);
118 spec
.AssignLiteral("(unknown)");
121 nsAutoCString
msg(origMsg
);
122 msg
.AppendLiteral(": ");
124 ReportError(cx
, msg
);
127 static bool EvalStencil(JSContext
* cx
, HandleObject targetObj
,
128 HandleObject loadScope
, MutableHandleValue retval
,
129 nsIURI
* uri
, bool storeIntoStartupCache
,
130 bool storeIntoPreloadCache
, JS::Stencil
* stencil
) {
131 MOZ_ASSERT(!js::IsWrapper(targetObj
));
133 JS::InstantiateOptions options
;
134 JS::RootedScript
script(cx
,
135 JS::InstantiateGlobalStencil(cx
, options
, stencil
));
140 if (JS_IsGlobalObject(targetObj
)) {
141 if (!JS_ExecuteScript(cx
, script
, retval
)) {
144 } else if (JS::IsJSMEnvironment(targetObj
)) {
145 if (!JS::ExecuteInJSMEnvironment(cx
, script
, targetObj
)) {
148 retval
.setUndefined();
150 JS::EnvironmentChain
envChain(cx
, JS::SupportUnscopables::No
);
151 if (!envChain
.append(targetObj
)) {
155 // A null loadScope means we are cross-realm. In this case, we should
156 // check the target isn't in the JSM loader shared-global or we will
157 // contaminate all JSMs in the realm.
159 // NOTE: If loadScope is already a shared-global JSM, we can't
160 // determine which JSM the target belongs to and have to assume it
162 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
163 JSObject
* targetGlobal
= JS::GetNonCCWObjectGlobal(targetObj
);
164 MOZ_DIAGNOSTIC_ASSERT(
165 !mozJSModuleLoader::Get()->IsLoaderGlobal(targetGlobal
),
166 "Don't load subscript into target in a shared-global JSM");
168 if (!JS_ExecuteScript(cx
, envChain
, script
, retval
)) {
171 } else if (JS_IsGlobalObject(loadScope
)) {
172 if (!JS_ExecuteScript(cx
, envChain
, script
, retval
)) {
176 MOZ_ASSERT(JS::IsJSMEnvironment(loadScope
));
177 if (!JS::ExecuteInJSMEnvironment(cx
, script
, loadScope
, envChain
)) {
180 retval
.setUndefined();
184 JSAutoRealm
rar(cx
, targetObj
);
185 if (!JS_WrapValue(cx
, retval
)) {
189 if (script
&& (storeIntoStartupCache
|| storeIntoPreloadCache
)) {
190 nsAutoCString cachePath
;
191 SubscriptCachePath(cx
, uri
, targetObj
, cachePath
);
194 if (storeIntoPreloadCache
&& NS_SUCCEEDED(uri
->GetSpec(uriStr
))) {
195 ScriptPreloader::GetSingleton().NoteStencil(uriStr
, cachePath
, stencil
);
198 if (storeIntoStartupCache
) {
199 JSAutoRealm
ar(cx
, script
);
200 WriteCachedStencil(StartupCache::GetSingleton(), cachePath
, cx
, stencil
);
207 bool mozJSSubScriptLoader::ReadStencil(
208 JS::Stencil
** stencilOut
, nsIURI
* uri
, JSContext
* cx
,
209 const JS::ReadOnlyCompileOptions
& options
, nsIIOService
* serv
,
210 bool useCompilationScope
) {
211 // We create a channel and call SetContentType, to avoid expensive MIME type
212 // lookups (bug 632490).
213 nsCOMPtr
<nsIChannel
> chan
;
214 nsCOMPtr
<nsIInputStream
> instream
;
216 rv
= NS_NewChannel(getter_AddRefs(chan
), uri
,
217 nsContentUtils::GetSystemPrincipal(),
218 nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL
,
219 nsIContentPolicy::TYPE_OTHER
,
220 nullptr, // nsICookieJarSettings
221 nullptr, // PerformanceStorage
222 nullptr, // aLoadGroup
223 nullptr, // aCallbacks
224 nsIRequest::LOAD_NORMAL
, serv
);
226 if (NS_SUCCEEDED(rv
)) {
227 chan
->SetContentType("application/javascript"_ns
);
228 rv
= chan
->Open(getter_AddRefs(instream
));
232 ReportError(cx
, LOAD_ERROR_NOSTREAM
, uri
);
238 rv
= chan
->GetContentLength(&len
);
240 ReportError(cx
, LOAD_ERROR_NOCONTENT
, uri
);
244 if (len
> INT32_MAX
) {
245 ReportError(cx
, LOAD_ERROR_CONTENTTOOBIG
, uri
);
250 rv
= NS_ReadInputStreamToString(instream
, buf
, len
);
251 NS_ENSURE_SUCCESS(rv
, false);
257 Maybe
<JSAutoRealm
> ar
;
259 // Note that when using the ScriptPreloader cache with loadSubScript, there
260 // will be a side-effect of keeping the global that the script was compiled
261 // for alive. See note above in EvalScript().
263 // This will compile the script in XPConnect compilation scope. When the
264 // script is evaluated, it will be cloned into the target scope to be
265 // executed, avoiding leaks on the first session when we don't have a
267 if (useCompilationScope
) {
268 ar
.emplace(cx
, xpc::CompilationScope());
271 JS::SourceText
<Utf8Unit
> srcBuf
;
272 if (!srcBuf
.init(cx
, buf
.get(), len
, JS::SourceOwnership::Borrowed
)) {
276 RefPtr
<JS::Stencil
> stencil
=
277 JS::CompileGlobalScriptToStencil(cx
, options
, srcBuf
);
278 stencil
.forget(stencilOut
);
283 mozJSSubScriptLoader::LoadSubScript(const nsAString
& url
, HandleValue target
,
284 JSContext
* cx
, MutableHandleValue retval
) {
286 * Loads a local url, referring to UTF-8-encoded data, and evals it into the
287 * current cx. Synchronous. ChromeUtils.compileScript() should be used for
289 * url: The url to load. Must be local so that it can be loaded
291 * targetObj: Optional object to eval the script onto (defaults to context
293 * returns: Whatever jsval the script pointed to by the url returns.
294 * Should ONLY (O N L Y !) be called from JavaScript code.
296 LoadSubScriptOptions
options(cx
);
297 options
.target
= target
.isObject() ? &target
.toObject() : nullptr;
298 return DoLoadSubScriptWithOptions(url
, options
, cx
, retval
);
302 mozJSSubScriptLoader::LoadSubScriptWithOptions(const nsAString
& url
,
303 HandleValue optionsVal
,
305 MutableHandleValue retval
) {
306 if (!optionsVal
.isObject()) {
307 return NS_ERROR_INVALID_ARG
;
310 LoadSubScriptOptions
options(cx
, &optionsVal
.toObject());
311 if (!options
.Parse()) {
312 return NS_ERROR_INVALID_ARG
;
315 return DoLoadSubScriptWithOptions(url
, options
, cx
, retval
);
318 nsresult
mozJSSubScriptLoader::DoLoadSubScriptWithOptions(
319 const nsAString
& url
, LoadSubScriptOptions
& options
, JSContext
* cx
,
320 MutableHandleValue retval
) {
322 RootedObject
targetObj(cx
);
323 RootedObject
loadScope(cx
);
324 mozJSModuleLoader
* loader
= mozJSModuleLoader::Get();
325 loader
->FindTargetObject(cx
, &loadScope
);
327 if (options
.target
) {
328 targetObj
= options
.target
;
330 targetObj
= loadScope
;
333 targetObj
= JS_FindCompilationScope(cx
, targetObj
);
334 if (!targetObj
|| !loadScope
) {
335 return NS_ERROR_FAILURE
;
338 MOZ_ASSERT(!js::IsWrapper(targetObj
), "JS_FindCompilationScope must unwrap");
340 if (js::GetNonCCWObjectRealm(loadScope
) !=
341 js::GetNonCCWObjectRealm(targetObj
)) {
345 /* load up the url. From here on, failures are reflected as ``custom''
347 nsCOMPtr
<nsIURI
> uri
;
348 nsAutoCString uriStr
;
349 nsAutoCString scheme
;
351 // Figure out who's calling us
352 JS::AutoFilename filename
;
353 if (!JS::DescribeScriptedCaller(&filename
, cx
)) {
354 // No scripted frame means we don't know who's calling, bail.
355 return NS_ERROR_FAILURE
;
358 JSAutoRealm
ar(cx
, targetObj
);
360 nsCOMPtr
<nsIIOService
> serv
= do_GetService(NS_IOSERVICE_CONTRACTID
);
362 ReportError(cx
, nsLiteralCString(LOAD_ERROR_NOSERVICE
));
366 NS_LossyConvertUTF16toASCII
asciiUrl(url
);
367 const nsDependentCSubstring profilerUrl
=
368 Substring(asciiUrl
, 0, std::min(size_t(128), asciiUrl
.Length()));
369 AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE(
370 "mozJSSubScriptLoader::DoLoadSubScriptWithOptions", OTHER
, profilerUrl
);
371 AUTO_PROFILER_MARKER_TEXT("SubScript", JS
,
372 MarkerOptions(MarkerStack::Capture(),
373 MarkerInnerWindowIdFromJSContext(cx
)),
376 // Make sure to explicitly create the URI, since we'll need the
377 // canonicalized spec.
378 rv
= NS_NewURI(getter_AddRefs(uri
), asciiUrl
);
380 ReportError(cx
, nsLiteralCString(LOAD_ERROR_NOURI
));
384 rv
= uri
->GetSpec(uriStr
);
386 ReportError(cx
, nsLiteralCString(LOAD_ERROR_NOSPEC
));
390 rv
= uri
->GetScheme(scheme
);
392 ReportError(cx
, LOAD_ERROR_NOSCHEME
, uri
);
396 // Suppress caching if we're compiling as content or if we're loading a
398 bool useCompilationScope
= false;
399 auto* principal
= BasePrincipal::Cast(GetObjectPrincipal(targetObj
));
400 bool isSystem
= principal
->Is
<SystemPrincipal
>();
401 if (!isSystem
&& principal
->Is
<ContentPrincipal
>()) {
402 nsAutoCString scheme
;
403 principal
->GetScheme(scheme
);
405 // We want to enable caching for scripts with Activity Stream's
407 if (scheme
.EqualsLiteral("about")) {
408 nsAutoCString filePath
;
409 principal
->GetFilePath(filePath
);
411 useCompilationScope
= filePath
.EqualsLiteral("home") ||
412 filePath
.EqualsLiteral("newtab") ||
413 filePath
.EqualsLiteral("welcome");
418 options
.ignoreCache
|| !isSystem
|| scheme
.EqualsLiteral("blob");
420 StartupCache
* cache
= ignoreCache
? nullptr : StartupCache::GetSingleton();
422 nsAutoCString cachePath
;
423 SubscriptCachePath(cx
, uri
, targetObj
, cachePath
);
425 JS::DecodeOptions decodeOptions
;
426 ScriptPreloader::FillDecodeOptionsForCachedStencil(decodeOptions
);
428 RefPtr
<JS::Stencil
> stencil
;
429 if (!options
.ignoreCache
) {
430 if (!options
.wantReturnValue
) {
431 // NOTE: If we need the return value, we cannot use ScriptPreloader.
432 stencil
= ScriptPreloader::GetSingleton().GetCachedStencil(
433 cx
, decodeOptions
, cachePath
);
435 if (!stencil
&& cache
) {
436 rv
= ReadCachedStencil(cache
, cachePath
, cx
, decodeOptions
,
437 getter_AddRefs(stencil
));
438 if (NS_FAILED(rv
) || !stencil
) {
439 JS_ClearPendingException(cx
);
444 bool storeIntoStartupCache
= false;
446 // Store into startup cache only when the script isn't come from any cache.
447 storeIntoStartupCache
= cache
;
449 JS::CompileOptions
compileOptions(cx
);
450 ScriptPreloader::FillCompileOptionsForCachedStencil(compileOptions
);
451 compileOptions
.setFileAndLine(uriStr
.get(), 1);
452 compileOptions
.setNonSyntacticScope(!JS_IsGlobalObject(targetObj
));
454 if (options
.wantReturnValue
) {
455 compileOptions
.setNoScriptRval(false);
458 if (!ReadStencil(getter_AddRefs(stencil
), uri
, cx
, compileOptions
, serv
,
459 useCompilationScope
)) {
464 // The above shouldn't touch any options for instantiation.
465 JS::InstantiateOptions
instantiateOptions(compileOptions
);
466 instantiateOptions
.assertDefault();
470 // As a policy choice, we don't store scripts that want return values
471 // into the preload cache.
472 bool storeIntoPreloadCache
= !ignoreCache
&& !options
.wantReturnValue
;
474 Unused
<< EvalStencil(cx
, targetObj
, loadScope
, retval
, uri
,
475 storeIntoStartupCache
, storeIntoPreloadCache
, stencil
);