1/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
2 * vim: sw=4 ts=4 sts=4 et filetype=javascript
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
7var EXPORTED_SYMBOLS = [
8  "NetUtil",
9];
10
11/**
12 * Necko utilities
13 */
14
15////////////////////////////////////////////////////////////////////////////////
16//// Constants
17
18const PR_UINT32_MAX = 0xffffffff;
19
20ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
21ChromeUtils.import("resource://gre/modules/Services.jsm");
22
23const BinaryInputStream = Components.Constructor("@mozilla.org/binaryinputstream;1",
24                                                 "nsIBinaryInputStream", "setInputStream");
25
26////////////////////////////////////////////////////////////////////////////////
27//// NetUtil Object
28
29var NetUtil = {
30    /**
31     * Function to perform simple async copying from aSource (an input stream)
32     * to aSink (an output stream).  The copy will happen on some background
33     * thread.  Both streams will be closed when the copy completes.
34     *
35     * @param aSource
36     *        The input stream to read from
37     * @param aSink
38     *        The output stream to write to
39     * @param aCallback [optional]
40     *        A function that will be called at copy completion with a single
41     *        argument: the nsresult status code for the copy operation.
42     *
43     * @return An nsIRequest representing the copy operation (for example, this
44     *         can be used to cancel the copying).  The consumer can ignore the
45     *         return value if desired.
46     */
47    asyncCopy: function NetUtil_asyncCopy(aSource, aSink,
48                                          aCallback = null)
49    {
50        if (!aSource || !aSink) {
51            let exception = new Components.Exception(
52                "Must have a source and a sink",
53                Cr.NS_ERROR_INVALID_ARG,
54                Components.stack.caller
55            );
56            throw exception;
57        }
58
59        // make a stream copier
60        var copier = Cc["@mozilla.org/network/async-stream-copier;1"].
61            createInstance(Ci.nsIAsyncStreamCopier2);
62        copier.init(aSource, aSink,
63                    null /* Default event target */,
64                    0 /* Default length */,
65                    true, true /* Auto-close */);
66
67        var observer;
68        if (aCallback) {
69            observer = {
70                onStartRequest: function(aRequest, aContext) {},
71                onStopRequest: function(aRequest, aContext, aStatusCode) {
72                    aCallback(aStatusCode);
73                }
74            }
75        } else {
76            observer = null;
77        }
78
79        // start the copying
80        copier.QueryInterface(Ci.nsIAsyncStreamCopier).asyncCopy(observer, null);
81        return copier;
82    },
83
84    /**
85     * Asynchronously opens a source and fetches the response.  While the fetch
86     * is asynchronous, I/O may happen on the main thread.  When reading from
87     * a local file, prefer using "OS.File" methods instead.
88     *
89     * @param aSource
90     *        This argument can be one of the following:
91     *         - An options object that will be passed to NetUtil.newChannel.
92     *         - An existing nsIChannel.
93     *         - An existing nsIInputStream.
94     *        Using an nsIURI, nsIFile, or string spec directly is deprecated.
95     * @param aCallback
96     *        The callback function that will be notified upon completion.  It
97     *        will get these arguments:
98     *        1) An nsIInputStream containing the data from aSource, if any.
99     *        2) The status code from opening the source.
100     *        3) Reference to the nsIRequest.
101     */
102    asyncFetch: function NetUtil_asyncFetch(aSource, aCallback)
103    {
104        if (!aSource || !aCallback) {
105            let exception = new Components.Exception(
106                "Must have a source and a callback",
107                Cr.NS_ERROR_INVALID_ARG,
108                Components.stack.caller
109            );
110            throw exception;
111        }
112
113        // Create a pipe that will create our output stream that we can use once
114        // we have gotten all the data.
115        let pipe = Cc["@mozilla.org/pipe;1"].
116                   createInstance(Ci.nsIPipe);
117        pipe.init(true, true, 0, PR_UINT32_MAX, null);
118
119        // Create a listener that will give data to the pipe's output stream.
120        let listener = Cc["@mozilla.org/network/simple-stream-listener;1"].
121                       createInstance(Ci.nsISimpleStreamListener);
122        listener.init(pipe.outputStream, {
123            onStartRequest: function(aRequest, aContext) {},
124            onStopRequest: function(aRequest, aContext, aStatusCode) {
125                pipe.outputStream.close();
126                aCallback(pipe.inputStream, aStatusCode, aRequest);
127            }
128        });
129
130        // Input streams are handled slightly differently from everything else.
131        if (aSource instanceof Ci.nsIInputStream) {
132            let pump = Cc["@mozilla.org/network/input-stream-pump;1"].
133                       createInstance(Ci.nsIInputStreamPump);
134            pump.init(aSource, 0, 0, true);
135            pump.asyncRead(listener, null);
136            return;
137        }
138
139        let channel = aSource;
140        if (!(channel instanceof Ci.nsIChannel)) {
141            channel = this.newChannel(aSource);
142        }
143
144        try {
145            // Open the channel using asyncOpen2() if the loadinfo contains one
146            // of the security mode flags, otherwise fall back to use asyncOpen().
147            if (channel.loadInfo &&
148                channel.loadInfo.securityMode != 0) {
149                channel.asyncOpen2(listener);
150            }
151            else {
152                // Log deprecation warning to console to make sure all channels
153                // are created providing the correct security flags in the loadinfo.
154                // See nsILoadInfo for all available security flags and also the API
155                // of NetUtil.newChannel() for details above.
156                Cu.reportError("NetUtil.jsm: asyncFetch() requires the channel to have " +
157                    "one of the security flags set in the loadinfo (see nsILoadInfo). " +
158                    "Please create channel using NetUtil.newChannel()");
159                channel.asyncOpen(listener, null);
160            }
161        }
162        catch (e) {
163            let exception = new Components.Exception(
164                "Failed to open input source '" + channel.originalURI.spec + "'",
165                e.result,
166                Components.stack.caller,
167                aSource,
168                e
169            );
170            throw exception;
171        }
172    },
173
174    /**
175     * Constructs a new URI for the given spec, character set, and base URI, or
176     * an nsIFile.
177     *
178     * @param aTarget
179     *        The string spec for the desired URI or an nsIFile.
180     * @param aOriginCharset [optional]
181     *        The character set for the URI.  Only used if aTarget is not an
182     *        nsIFile.
183     * @param aBaseURI [optional]
184     *        The base URI for the spec.  Only used if aTarget is not an
185     *        nsIFile.
186     *
187     * @return an nsIURI object.
188     */
189    newURI: function NetUtil_newURI(aTarget, aOriginCharset, aBaseURI)
190    {
191        if (!aTarget) {
192            let exception = new Components.Exception(
193                "Must have a non-null string spec or nsIFile object",
194                Cr.NS_ERROR_INVALID_ARG,
195                Components.stack.caller
196            );
197            throw exception;
198        }
199
200        if (aTarget instanceof Ci.nsIFile) {
201            return this.ioService.newFileURI(aTarget);
202        }
203
204        return this.ioService.newURI(aTarget, aOriginCharset, aBaseURI);
205    },
206
207    /**
208     * Constructs a new channel for the given source.
209     *
210     * Keep in mind that URIs coming from a webpage should *never* use the
211     * systemPrincipal as the loadingPrincipal.
212     *
213     * @param aWhatToLoad
214     *        This argument used to be a string spec for the desired URI, an
215     *        nsIURI, or an nsIFile.  Now it should be an options object with
216     *        the following properties:
217     *        {
218     *          uri:
219     *            The full URI spec string, nsIURI or nsIFile to create the
220     *            channel for.
221     *            Note that this cannot be an nsIFile if you have to specify a
222     *            non-default charset or base URI.  Call NetUtil.newURI first if
223     *            you need to construct an URI using those options.
224     *          loadingNode:
225     *          loadingPrincipal:
226     *          triggeringPrincipal:
227     *          securityFlags:
228     *          contentPolicyType:
229     *            These will be used as values for the nsILoadInfo object on the
230     *            created channel. For details, see nsILoadInfo in nsILoadInfo.idl
231     *          loadUsingSystemPrincipal:
232     *            Set this to true to use the system principal as
233     *            loadingPrincipal.  This must be omitted if loadingPrincipal or
234     *            loadingNode are present.
235     *            This should be used with care as it skips security checks.
236     *        }
237     * @param aOriginCharset [deprecated]
238     *        The character set for the URI.  Only used if aWhatToLoad is a
239     *        string, which is a deprecated API.  Must be undefined otherwise.
240     *        Use NetUtil.newURI if you need to use this option.
241     * @param aBaseURI [deprecated]
242     *        The base URI for the spec.  Only used if aWhatToLoad is a string,
243     *        which is a deprecated API.  Must be undefined otherwise.  Use
244     *        NetUtil.newURI if you need to use this option.
245     * @return an nsIChannel object.
246     */
247    newChannel: function NetUtil_newChannel(aWhatToLoad, aOriginCharset, aBaseURI)
248    {
249        // Check for the deprecated API first.
250        if (typeof aWhatToLoad == "string" ||
251            (aWhatToLoad instanceof Ci.nsIFile) ||
252            (aWhatToLoad instanceof Ci.nsIURI)) {
253
254            let uri = (aWhatToLoad instanceof Ci.nsIURI)
255                      ? aWhatToLoad
256                      : this.newURI(aWhatToLoad, aOriginCharset, aBaseURI);
257
258            // log deprecation warning for developers.
259            Services.console.logStringMessage(
260              "Warning: NetUtil.newChannel(uri) deprecated, please provide argument 'aWhatToLoad'");
261
262            // Provide default loadinfo arguments and call the new API.
263            let systemPrincipal =
264              Services.scriptSecurityManager.getSystemPrincipal();
265
266            return this.ioService.newChannelFromURI2(
267                     uri,
268                     null, // loadingNode
269                     systemPrincipal, // loadingPrincipal
270                     null, // triggeringPrincipal
271                     Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
272                     Ci.nsIContentPolicy.TYPE_OTHER);
273        }
274
275        // We are using the updated API, that requires only the options object.
276        if (typeof aWhatToLoad != "object" ||
277             aOriginCharset !== undefined ||
278             aBaseURI !== undefined) {
279            throw new Components.Exception(
280                "newChannel requires a single object argument",
281                Cr.NS_ERROR_INVALID_ARG,
282                Components.stack.caller
283            );
284        }
285
286        let { uri,
287              loadingNode,
288              loadingPrincipal,
289              loadUsingSystemPrincipal,
290              triggeringPrincipal,
291              securityFlags,
292              contentPolicyType } = aWhatToLoad;
293
294        if (!uri) {
295            throw new Components.Exception(
296                "newChannel requires the 'uri' property on the options object.",
297                Cr.NS_ERROR_INVALID_ARG,
298                Components.stack.caller
299            );
300        }
301
302        if (typeof uri == "string" || uri instanceof Ci.nsIFile) {
303            uri = this.newURI(uri);
304        }
305
306        if (!loadingNode && !loadingPrincipal && !loadUsingSystemPrincipal) {
307            throw new Components.Exception(
308                "newChannel requires at least one of the 'loadingNode'," +
309                " 'loadingPrincipal', or 'loadUsingSystemPrincipal'" +
310                " properties on the options object.",
311                Cr.NS_ERROR_INVALID_ARG,
312                Components.stack.caller
313            );
314        }
315
316        if (loadUsingSystemPrincipal === true) {
317            if (loadingNode || loadingPrincipal) {
318                throw new Components.Exception(
319                    "newChannel does not accept 'loadUsingSystemPrincipal'" +
320                    " if the 'loadingNode' or 'loadingPrincipal' properties" +
321                    " are present on the options object.",
322                    Cr.NS_ERROR_INVALID_ARG,
323                    Components.stack.caller
324                );
325            }
326            loadingPrincipal = Services.scriptSecurityManager
327                                       .getSystemPrincipal();
328        } else if (loadUsingSystemPrincipal !== undefined) {
329            throw new Components.Exception(
330                "newChannel requires the 'loadUsingSystemPrincipal'" +
331                " property on the options object to be 'true' or 'undefined'.",
332                Cr.NS_ERROR_INVALID_ARG,
333                Components.stack.caller
334            );
335        }
336
337        if (securityFlags === undefined) {
338            if (!loadUsingSystemPrincipal) {
339                throw new Components.Exception(
340                    "newChannel requires the 'securityFlags' property on" +
341                    " the options object unless loading from system principal.",
342                    Cr.NS_ERROR_INVALID_ARG,
343                    Components.stack.caller
344                );
345            }
346            securityFlags = Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL;
347        }
348
349        if (contentPolicyType === undefined) {
350            if (!loadUsingSystemPrincipal) {
351                throw new Components.Exception(
352                    "newChannel requires the 'contentPolicyType' property on" +
353                    " the options object unless loading from system principal.",
354                    Cr.NS_ERROR_INVALID_ARG,
355                    Components.stack.caller
356                );
357            }
358            contentPolicyType = Ci.nsIContentPolicy.TYPE_OTHER;
359        }
360
361        return this.ioService.newChannelFromURI2(uri,
362                                                 loadingNode || null,
363                                                 loadingPrincipal || null,
364                                                 triggeringPrincipal || null,
365                                                 securityFlags,
366                                                 contentPolicyType);
367    },
368
369    /**
370     * Reads aCount bytes from aInputStream into a string.
371     *
372     * @param aInputStream
373     *        The input stream to read from.
374     * @param aCount
375     *        The number of bytes to read from the stream.
376     * @param aOptions [optional]
377     *        charset
378     *          The character encoding of stream data.
379     *        replacement
380     *          The character to replace unknown byte sequences.
381     *          If unset, it causes an exceptions to be thrown.
382     *
383     * @return the bytes from the input stream in string form.
384     *
385     * @throws NS_ERROR_INVALID_ARG if aInputStream is not an nsIInputStream.
386     * @throws NS_BASE_STREAM_WOULD_BLOCK if reading from aInputStream would
387     *         block the calling thread (non-blocking mode only).
388     * @throws NS_ERROR_FAILURE if there are not enough bytes available to read
389     *         aCount amount of data.
390     * @throws NS_ERROR_ILLEGAL_INPUT if aInputStream has invalid sequences
391     */
392    readInputStreamToString: function NetUtil_readInputStreamToString(aInputStream,
393                                                                      aCount,
394                                                                      aOptions)
395    {
396        if (!(aInputStream instanceof Ci.nsIInputStream)) {
397            let exception = new Components.Exception(
398                "First argument should be an nsIInputStream",
399                Cr.NS_ERROR_INVALID_ARG,
400                Components.stack.caller
401            );
402            throw exception;
403        }
404
405        if (!aCount) {
406            let exception = new Components.Exception(
407                "Non-zero amount of bytes must be specified",
408                Cr.NS_ERROR_INVALID_ARG,
409                Components.stack.caller
410            );
411            throw exception;
412        }
413
414        if (aOptions && "charset" in aOptions) {
415          let cis = Cc["@mozilla.org/intl/converter-input-stream;1"].
416                    createInstance(Ci.nsIConverterInputStream);
417          try {
418            // When replacement is set, the character that is unknown sequence
419            // replaces with aOptions.replacement character.
420            if (!("replacement" in aOptions)) {
421              // aOptions.replacement isn't set.
422              // If input stream has unknown sequences for aOptions.charset,
423              // throw NS_ERROR_ILLEGAL_INPUT.
424              aOptions.replacement = 0;
425            }
426
427            cis.init(aInputStream, aOptions.charset, aCount,
428                     aOptions.replacement);
429            let str = {};
430            cis.readString(-1, str);
431            cis.close();
432            return str.value;
433          }
434          catch (e) {
435            // Adjust the stack so it throws at the caller's location.
436            throw new Components.Exception(e.message, e.result,
437                                           Components.stack.caller, e.data);
438          }
439        }
440
441        let sis = Cc["@mozilla.org/scriptableinputstream;1"].
442                  createInstance(Ci.nsIScriptableInputStream);
443        sis.init(aInputStream);
444        try {
445            return sis.readBytes(aCount);
446        }
447        catch (e) {
448            // Adjust the stack so it throws at the caller's location.
449            throw new Components.Exception(e.message, e.result,
450                                           Components.stack.caller, e.data);
451        }
452    },
453
454    /**
455     * Reads aCount bytes from aInputStream into a string.
456     *
457     * @param {nsIInputStream} aInputStream
458     *        The input stream to read from.
459     * @param {integer} [aCount = aInputStream.available()]
460     *        The number of bytes to read from the stream.
461     *
462     * @return the bytes from the input stream in string form.
463     *
464     * @throws NS_ERROR_INVALID_ARG if aInputStream is not an nsIInputStream.
465     * @throws NS_BASE_STREAM_WOULD_BLOCK if reading from aInputStream would
466     *         block the calling thread (non-blocking mode only).
467     * @throws NS_ERROR_FAILURE if there are not enough bytes available to read
468     *         aCount amount of data.
469     */
470    readInputStream(aInputStream, aCount)
471    {
472        if (!(aInputStream instanceof Ci.nsIInputStream)) {
473            let exception = new Components.Exception(
474                "First argument should be an nsIInputStream",
475                Cr.NS_ERROR_INVALID_ARG,
476                Components.stack.caller
477            );
478            throw exception;
479        }
480
481        if (!aCount) {
482            aCount = aInputStream.available();
483        }
484
485        let stream = new BinaryInputStream(aInputStream);
486        let result = new ArrayBuffer(aCount);
487        stream.readArrayBuffer(result.byteLength, result);
488        return result;
489    },
490
491    /**
492     * Returns a reference to nsIIOService.
493     *
494     * @return a reference to nsIIOService.
495     */
496    get ioService()
497    {
498        delete this.ioService;
499        return this.ioService = Cc["@mozilla.org/network/io-service;1"].
500                                getService(Ci.nsIIOService);
501    },
502};
503