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