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