1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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/. */
6
7 #include <stdlib.h>
8 #include <errno.h>
9 #ifdef HAVE_IO_H
10 # include <io.h> /* for isatty() */
11 #endif
12 #ifdef HAVE_UNISTD_H
13 # include <unistd.h> /* for isatty() */
14 #endif
15
16 #include "base/basictypes.h"
17
18 #include "jsapi.h"
19 #include "js/CharacterEncoding.h"
20 #include "js/CompilationAndEvaluation.h" // JS::Compile{,Utf8File}
21 #include "js/PropertySpec.h"
22 #include "js/SourceText.h" // JS::Source{Ownership,Text}
23
24 #include "xpcpublic.h"
25
26 #include "XPCShellEnvironment.h"
27
28 #include "mozilla/Utf8.h" // mozilla::Utf8Unit
29 #include "mozilla/XPCOM.h"
30 #include "mozilla/dom/AutoEntryScript.h"
31 #include "mozilla/dom/ScriptSettings.h"
32
33 #include "nsIPrincipal.h"
34 #include "nsIScriptSecurityManager.h"
35 #include "nsIXPConnect.h"
36
37 #include "nsJSUtils.h"
38 #include "nsJSPrincipals.h"
39 #include "nsThreadUtils.h"
40 #include "nsXULAppAPI.h"
41
42 #include "BackstagePass.h"
43
44 #include "TestShellChild.h"
45 #include "TestShellParent.h"
46
47 using mozilla::AutoSafeJSContext;
48 using mozilla::dom::AutoEntryScript;
49 using mozilla::dom::AutoJSAPI;
50 using mozilla::ipc::XPCShellEnvironment;
51 using namespace JS;
52
53 namespace {
54
55 static const char kDefaultRuntimeScriptFilename[] = "xpcshell.js";
56
Environment(JS::Handle<JSObject * > global)57 inline XPCShellEnvironment* Environment(JS::Handle<JSObject*> global) {
58 AutoJSAPI jsapi;
59 if (!jsapi.Init(global)) {
60 return nullptr;
61 }
62 JSContext* cx = jsapi.cx();
63 Rooted<Value> v(cx);
64 if (!JS_GetProperty(cx, global, "__XPCShellEnvironment", &v) ||
65 !v.get().isDouble()) {
66 return nullptr;
67 }
68 return static_cast<XPCShellEnvironment*>(v.get().toPrivate());
69 }
70
Print(JSContext * cx,unsigned argc,JS::Value * vp)71 static bool Print(JSContext* cx, unsigned argc, JS::Value* vp) {
72 JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
73
74 for (unsigned i = 0; i < args.length(); i++) {
75 JSString* str = JS::ToString(cx, args[i]);
76 if (!str) return false;
77 JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, str);
78 if (!bytes) return false;
79 fprintf(stdout, "%s%s", i ? " " : "", bytes.get());
80 fflush(stdout);
81 }
82 fputc('\n', stdout);
83 args.rval().setUndefined();
84 return true;
85 }
86
GetLine(char * bufp,FILE * file,const char * prompt)87 static bool GetLine(char* bufp, FILE* file, const char* prompt) {
88 char line[256];
89 fputs(prompt, stdout);
90 fflush(stdout);
91 if (!fgets(line, sizeof line, file)) return false;
92 strcpy(bufp, line);
93 return true;
94 }
95
Dump(JSContext * cx,unsigned argc,JS::Value * vp)96 static bool Dump(JSContext* cx, unsigned argc, JS::Value* vp) {
97 JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
98
99 if (!args.length()) return true;
100
101 JSString* str = JS::ToString(cx, args[0]);
102 if (!str) return false;
103 JS::UniqueChars bytes = JS_EncodeStringToLatin1(cx, str);
104 if (!bytes) return false;
105
106 fputs(bytes.get(), stdout);
107 fflush(stdout);
108 return true;
109 }
110
Load(JSContext * cx,unsigned argc,JS::Value * vp)111 static bool Load(JSContext* cx, unsigned argc, JS::Value* vp) {
112 JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
113
114 JS::RootedObject thisObject(cx);
115 if (!args.computeThis(cx, &thisObject)) return false;
116 if (!JS_IsGlobalObject(thisObject)) {
117 JS_ReportErrorASCII(cx, "Trying to load() into a non-global object");
118 return false;
119 }
120
121 for (unsigned i = 0; i < args.length(); i++) {
122 JS::Rooted<JSString*> str(cx, JS::ToString(cx, args[i]));
123 if (!str) return false;
124 JS::UniqueChars filename = JS_EncodeStringToLatin1(cx, str);
125 if (!filename) return false;
126 FILE* file = fopen(filename.get(), "r");
127 if (!file) {
128 filename = JS_EncodeStringToUTF8(cx, str);
129 if (!filename) return false;
130 JS_ReportErrorUTF8(cx, "cannot open file '%s' for reading",
131 filename.get());
132 return false;
133 }
134
135 JS::CompileOptions options(cx);
136 options.setFileAndLine(filename.get(), 1);
137
138 JS::Rooted<JSScript*> script(cx, JS::CompileUtf8File(cx, options, file));
139 fclose(file);
140 if (!script) return false;
141
142 if (!JS_ExecuteScript(cx, script)) {
143 return false;
144 }
145 }
146 args.rval().setUndefined();
147 return true;
148 }
149
Quit(JSContext * cx,unsigned argc,JS::Value * vp)150 static bool Quit(JSContext* cx, unsigned argc, JS::Value* vp) {
151 Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
152 XPCShellEnvironment* env = Environment(global);
153 env->SetIsQuitting();
154
155 return false;
156 }
157
DumpXPC(JSContext * cx,unsigned argc,JS::Value * vp)158 static bool DumpXPC(JSContext* cx, unsigned argc, JS::Value* vp) {
159 JS::CallArgs args = CallArgsFromVp(argc, vp);
160
161 uint16_t depth = 2;
162 if (args.length() > 0) {
163 if (!JS::ToUint16(cx, args[0], &depth)) return false;
164 }
165
166 nsCOMPtr<nsIXPConnect> xpc = nsIXPConnect::XPConnect();
167 if (xpc) xpc->DebugDump(int16_t(depth));
168 args.rval().setUndefined();
169 return true;
170 }
171
GC(JSContext * cx,unsigned argc,JS::Value * vp)172 static bool GC(JSContext* cx, unsigned argc, JS::Value* vp) {
173 JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
174
175 JS_GC(cx);
176
177 args.rval().setUndefined();
178 return true;
179 }
180
181 #ifdef JS_GC_ZEAL
GCZeal(JSContext * cx,unsigned argc,JS::Value * vp)182 static bool GCZeal(JSContext* cx, unsigned argc, JS::Value* vp) {
183 CallArgs args = CallArgsFromVp(argc, vp);
184
185 uint32_t zeal;
186 if (!ToUint32(cx, args.get(0), &zeal)) return false;
187
188 JS_SetGCZeal(cx, uint8_t(zeal), JS_DEFAULT_ZEAL_FREQ);
189 return true;
190 }
191 #endif
192
193 const JSFunctionSpec gGlobalFunctions[] = {JS_FN("print", Print, 0, 0),
194 JS_FN("load", Load, 1, 0),
195 JS_FN("quit", Quit, 0, 0),
196 JS_FN("dumpXPC", DumpXPC, 1, 0),
197 JS_FN("dump", Dump, 1, 0),
198 JS_FN("gc", GC, 0, 0),
199 #ifdef JS_GC_ZEAL
200 JS_FN("gczeal", GCZeal, 1, 0),
201 #endif
202 JS_FS_END};
203
204 typedef enum JSShellErrNum {
205 #define MSG_DEF(name, number, count, exception, format) name = number,
206 #include "jsshell.msg"
207 #undef MSG_DEF
208 JSShellErr_Limit
209 #undef MSGDEF
210 } JSShellErrNum;
211
212 } /* anonymous namespace */
213
ProcessFile(JSContext * cx,const char * filename,FILE * file,bool forceTTY)214 void XPCShellEnvironment::ProcessFile(JSContext* cx, const char* filename,
215 FILE* file, bool forceTTY) {
216 XPCShellEnvironment* env = this;
217
218 JS::Rooted<JS::Value> result(cx);
219 int lineno, startline;
220 bool ok, hitEOF;
221 char *bufp, buffer[4096];
222 JSString* str;
223
224 JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
225 MOZ_ASSERT(global);
226
227 if (forceTTY) {
228 file = stdin;
229 } else if (!isatty(fileno(file))) {
230 /*
231 * It's not interactive - just execute it.
232 *
233 * Support the UNIX #! shell hack; gobble the first line if it starts
234 * with '#'. TODO - this isn't quite compatible with sharp variables,
235 * as a legal js program (using sharp variables) might start with '#'.
236 * But that would require multi-character lookahead.
237 */
238 int ch = fgetc(file);
239 if (ch == '#') {
240 while ((ch = fgetc(file)) != EOF) {
241 if (ch == '\n' || ch == '\r') break;
242 }
243 }
244 ungetc(ch, file);
245
246 JS::CompileOptions options(cx);
247 options.setFileAndLine(filename, 1);
248
249 JS::Rooted<JSScript*> script(cx, JS::CompileUtf8File(cx, options, file));
250 if (script) {
251 (void)JS_ExecuteScript(cx, script, &result);
252 }
253
254 return;
255 }
256
257 /* It's an interactive filehandle; drop into read-eval-print loop. */
258 lineno = 1;
259 hitEOF = false;
260 do {
261 bufp = buffer;
262 *bufp = '\0';
263
264 /*
265 * Accumulate lines until we get a 'compilable unit' - one that either
266 * generates an error (before running out of source) or that compiles
267 * cleanly. This should be whenever we get a complete statement that
268 * coincides with the end of a line.
269 */
270 startline = lineno;
271 do {
272 if (!GetLine(bufp, file, startline == lineno ? "js> " : "")) {
273 hitEOF = true;
274 break;
275 }
276 bufp += strlen(bufp);
277 lineno++;
278 } while (
279 !JS_Utf8BufferIsCompilableUnit(cx, global, buffer, strlen(buffer)));
280
281 /* Clear any pending exception from previous failed compiles. */
282 JS_ClearPendingException(cx);
283
284 JS::CompileOptions options(cx);
285 options.setFileAndLine("typein", startline);
286
287 JS::SourceText<mozilla::Utf8Unit> srcBuf;
288 JS::Rooted<JSScript*> script(cx);
289
290 if (srcBuf.init(cx, buffer, strlen(buffer),
291 JS::SourceOwnership::Borrowed) &&
292 (script = JS::Compile(cx, options, srcBuf))) {
293 ok = JS_ExecuteScript(cx, script, &result);
294 if (ok && !result.isUndefined()) {
295 /* Suppress warnings from JS::ToString(). */
296 JS::AutoSuppressWarningReporter suppressWarnings(cx);
297 str = JS::ToString(cx, result);
298 JS::UniqueChars bytes;
299 if (str) bytes = JS_EncodeStringToLatin1(cx, str);
300
301 if (!!bytes)
302 fprintf(stdout, "%s\n", bytes.get());
303 else
304 ok = false;
305 }
306 }
307 } while (!hitEOF && !env->IsQuitting());
308
309 fprintf(stdout, "\n");
310 }
311
312 // static
CreateEnvironment()313 XPCShellEnvironment* XPCShellEnvironment::CreateEnvironment() {
314 auto* env = new XPCShellEnvironment();
315 if (env && !env->Init()) {
316 delete env;
317 env = nullptr;
318 }
319 return env;
320 }
321
XPCShellEnvironment()322 XPCShellEnvironment::XPCShellEnvironment() : mQuitting(false) {}
323
~XPCShellEnvironment()324 XPCShellEnvironment::~XPCShellEnvironment() {
325 if (GetGlobalObject()) {
326 AutoJSAPI jsapi;
327 if (!jsapi.Init(GetGlobalObject())) {
328 return;
329 }
330 JS_SetAllNonReservedSlotsToUndefined(mGlobalHolder);
331 mGlobalHolder.reset();
332
333 JS_GC(jsapi.cx());
334 }
335 }
336
Init()337 bool XPCShellEnvironment::Init() {
338 nsresult rv;
339
340 // unbuffer stdout so that output is in the correct order; note that stderr
341 // is unbuffered by default
342 setbuf(stdout, 0);
343
344 AutoSafeJSContext cx;
345
346 mGlobalHolder.init(cx);
347
348 nsCOMPtr<nsIPrincipal> principal;
349 nsCOMPtr<nsIScriptSecurityManager> securityManager =
350 do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID, &rv);
351 if (NS_SUCCEEDED(rv) && securityManager) {
352 rv = securityManager->GetSystemPrincipal(getter_AddRefs(principal));
353 if (NS_FAILED(rv)) {
354 fprintf(stderr,
355 "+++ Failed to obtain SystemPrincipal from ScriptSecurityManager "
356 "service.\n");
357 }
358 } else {
359 fprintf(stderr,
360 "+++ Failed to get ScriptSecurityManager service, running without "
361 "principals");
362 }
363
364 auto backstagePass = MakeRefPtr<BackstagePass>();
365
366 JS::RealmOptions options;
367 options.creationOptions().setNewCompartmentInSystemZone();
368 xpc::SetPrefableRealmOptions(options);
369
370 JS::Rooted<JSObject*> globalObj(cx);
371 rv = xpc::InitClassesWithNewWrappedGlobal(
372 cx, static_cast<nsIGlobalObject*>(backstagePass), principal, 0, options,
373 &globalObj);
374 if (NS_FAILED(rv)) {
375 NS_ERROR("InitClassesWithNewWrappedGlobal failed!");
376 return false;
377 }
378
379 if (!globalObj) {
380 NS_ERROR("Failed to get global JSObject!");
381 return false;
382 }
383 JSAutoRealm ar(cx, globalObj);
384
385 backstagePass->SetGlobalObject(globalObj);
386
387 JS::Rooted<Value> privateVal(cx, PrivateValue(this));
388 if (!JS_DefineProperty(cx, globalObj, "__XPCShellEnvironment", privateVal,
389 JSPROP_READONLY | JSPROP_PERMANENT) ||
390 !JS_DefineFunctions(cx, globalObj, gGlobalFunctions)) {
391 NS_ERROR("JS_DefineFunctions failed!");
392 return false;
393 }
394
395 mGlobalHolder = globalObj;
396
397 FILE* runtimeScriptFile = fopen(kDefaultRuntimeScriptFilename, "r");
398 if (runtimeScriptFile) {
399 fprintf(stdout, "[loading '%s'...]\n", kDefaultRuntimeScriptFilename);
400 ProcessFile(cx, kDefaultRuntimeScriptFilename, runtimeScriptFile, false);
401 fclose(runtimeScriptFile);
402 }
403
404 return true;
405 }
406
EvaluateString(const nsString & aString,nsString * aResult)407 bool XPCShellEnvironment::EvaluateString(const nsString& aString,
408 nsString* aResult) {
409 AutoEntryScript aes(GetGlobalObject(),
410 "ipc XPCShellEnvironment::EvaluateString");
411 JSContext* cx = aes.cx();
412
413 JS::CompileOptions options(cx);
414 options.setFileAndLine("typein", 0);
415
416 JS::SourceText<char16_t> srcBuf;
417 if (!srcBuf.init(cx, aString.get(), aString.Length(),
418 JS::SourceOwnership::Borrowed)) {
419 return false;
420 }
421
422 JS::Rooted<JSScript*> script(cx, JS::Compile(cx, options, srcBuf));
423 if (!script) {
424 return false;
425 }
426
427 if (aResult) {
428 aResult->Truncate();
429 }
430
431 JS::Rooted<JS::Value> result(cx);
432 bool ok = JS_ExecuteScript(cx, script, &result);
433 if (ok && !result.isUndefined()) {
434 /* Suppress warnings from JS::ToString(). */
435 JS::AutoSuppressWarningReporter suppressWarnings(cx);
436 JSString* str = JS::ToString(cx, result);
437 nsAutoJSString autoStr;
438 if (str) autoStr.init(cx, str);
439
440 if (!autoStr.IsEmpty() && aResult) {
441 aResult->Assign(autoStr);
442 }
443 }
444
445 return true;
446 }
447