Source: ongoingrequest.js

/**
 * OngoingRequest
 *
 * @author Martin Giger
 * @license MPL-2.0
 * @module lib/ongoingrequest
 */

"use strict";

const { Class } = require("sdk/core/heritage");
const { Disposable } = require("sdk/core/disposable");
const { Ci, Cr } = require("chrome");
const { newURI } = require("sdk/url/utils");
const { InputStream } = require("./inputstream");
const { contract } = require("sdk/util/contract");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
const { Headers } = require("./headers");
const { INCOMING, OUTGOING } = require("./const");
const { ChannelListener } = require("./channellistener");

/**
 * @const {Error} NS_ERROR_NOT_AVAILABLE
 */
const NS_ERROR_NOT_AVAILABLE = new Error("Operation could not be completed because some other necessary component or resource was not available.");

let channels = new WeakMap();
let models = new WeakMap();

let channelFor = (oreq) => channels.get(oreq);
let modelFor = (oreq) => models.get(oreq);

let isDirection = (v) => v === INCOMING || v === OUTGOING;

const requestContract = contract({
    channel: {
        is: ['object'],
        ok: (channel) => channel instanceof Ci.nsIHttpChannel,
        msg: 'The `channel` option must always implement nsIHttpChannel.'
    },
    direction: {
        is: ['number'],
        ok: (direction) => isDirection(direction),
        msg: 'The `direction` option must always be a single direction constant.'
    }
});

const OngoingRequest = Class(
/** @lends module:lib/ongoingrequest.OngoingRequest.prototype */
{
    extends: Disposable,
    /**
     * @typedef {Object} OngoingRequestOptions
     * @property {external:nsIHttpChannel} channel
     * @property {(module:requestmod.INCOMING|module:requestmod.OUTGOING)} direction
     */
    /**
     * Represents an incoming or outgoing request.
     * @constructs OngoingRequest
     * @extends external:sdk/core/disposable.Disposable
     * @argument {module:lib/ongoingrequest~OngoingRequestOptions} options - Specifies the channel, direction
     * @alias module:lib/ongoingrequest.OngoingRequest
     */
    setup: function(options) {
        requestContract(options);
        // In theory the contract already did this check
        options.channel.QueryInterface(Ci.nsIHttpChannel);

        channels.set(this, options.channel);
        models.set(this, { direction: options.direction, headers: Headers(options.channel, options.direction) });

        if(options.direction === INCOMING) {
            options.channel.QueryInterface(Ci.nsITraceableChannel);
            let model = modelFor(this);
            model.listener = ChannelListener();
            model.listener.oldListener = options.channel.setNewListener(model.listener);
        }
    },
    /**
     * The URL of the request. Setting this property factually redirects the
     * request to a different URL, opening a new request.
     * @type {string}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} When trying to set the URL of an
     * incoming request.
     */
    get url() {
        return channelFor(this).URI.spec;
    },
    set url(val) {
        if(this.direction === OUTGOING) {
            channelFor(this).redirectTo(newURI(val));
        }
        else {
            throw NS_ERROR_NOT_AVAILABLE;
        }
    },
    /**
     * Referer of the request.
     * @type {string}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} When trying to set the referer of an
     * incoming request.
     */
    get referrer() {
        return channelFor(this).referrer && channelFor(this).referrer.spec;
    },
    set referrer(val) {
        if(this.direction === OUTGOING) {
            channelFor(this).referrer = newURI(val);
        }
        else {
            throw NS_ERROR_NOT_AVAILABLE;
        }
    },
    /**
     * The request method.
     * @type {string}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} When trying to set the method of an
     * incoming request.
     */
    get method() {
        return channelFor(this).requestMethod;
    },
    set method(val) {
        if(this.direction === OUTGOING) {
            channelFor(this).requestMethod = val;
        }
        else {
            throw NS_ERROR_NOT_AVAILABLE;
        }
    },
    /**
     * The request status code
     * @type {number}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} For outgoing requests.
     * @readonly
     */
    get status() {
        if(this.direction === INCOMING) {
            return channelFor(this).responseStatus;
        }
        else {
            throw NS_ERROR_NOT_AVAILABLE;
        }
    },
    /**
     * Access and modify request headers.
     * @type {module:lib/headers.Headers}
     * @readonly
     */
    get headers() {
        return modelFor(this).headers;
    },
    /**
     * Read and set the content. The reading part is not reliable for incoming
     * requests, due to the content's streaming nature. Consider the
     * processContent callback as an alternative.
     *
     * The content can be set to an SDK buffer, TypedArrays, ArrayBuffers, any string, object or a
     * nsIInputStream directly.
     *
     * The returned value will always be parsed to a string by default. If the stream
     * is outgoing and the content type contains "json" it will be parsed to an object.
     * @type {(Object|string)?}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} Whenever the content of an incoming
     * request is not complete yet.
     */
    get content() {
        if(this.direction === OUTGOING) {
            let channel = channelFor(this);
            channel.QueryInterface(Ci.nsIUploadChannel);
            let stream = channel.uploadStream;

            if(stream === null) {
                return null;
            }
            else {
                if(stream.available() > 0) {
                    let options = {};
                    options.charset = this.charset;

                    let value = NetUtil.readInputStreamToString(stream, stream.available(), options);

                    if(this.type.includes("json")) {
                        value = JSON.parse(value);
                    }

                    return value;
                }
                else {
                    return null;
                }
            }
        }
        else {
            let { listener } = modelFor(this);
            if(!listener.called && !listener.done) {
                throw NS_ERROR_NOT_AVAILABLE;
            }
            else {
                return modelFor(this).listener.value;
            }
        }
    },
    set content(val) {
        if(this.direction === OUTGOING) {
            let channel = channelFor(this);
            let method = this.method;
            channel.QueryInterface(Ci.nsIUploadChannel);

            let { stream } = InputStream(val);

            // The MIME InputStream already contains all the info headers.
            if(val === null || stream instanceof Ci.nsIMIMEInputStream) {
                channel.setUploadStream(stream, "", -1);
            }
            else {
                channel.setUploadStream(stream, this.type, -1);
            }
            // Re-set method
            this.method = method;
        }
        else {
            modelFor(this).listener.value = val;
            modelFor(this).listener.called = true;
        }
    },
    /**
     * Content Type.
     * @type {string}
     * @default "text/plain"
     */
    get type() {
        var type;
        try {
            type = channelFor(this).contentType;
        }
        catch(e) {
            // Try to get the content type from the header then
            type = this.headers.get("Content-Type");

            if(type === undefined)
                type = "text/plain";
        }
        return type;
    },
    set type(val) {
        channelFor(this).contentType = val;
    },
    /**
     * @type {string}
     * @default "UTF-8"
     */
    get charset() {
        let charset;
        try {
            charset = channelFor(this).contentCharset;
        }
        catch(e) {
            charset = "UTF-8";
        }
        return charset;
    },
    set charset(val) {
        channelFor(this).contentCharset = val;
    },
    /**
     * Direction of the request.
     * @type {module:requestmod.INCOMING|module:requestmod.OUTGOING}
     * @readonly
     */
    get direction() {
        return modelFor(this).direction;
    },
    /**
     * Is true if the request is for sure not cached, else null.
     * (not false, as it's not sure that it is in fact coming from the cache).
     * @type {?boolean}
     * @throws {module:lib/ongoingrequest.NS_ERROR_NOT_AVAILABLE} For outgoing requests.
     */
    get notCached() {
        if(this.direction === INCOMING) {
            let channel = channelFor(this);
            return channel.isNoStoreResponse() && channel.isNoCacheResponse() || null;
        }
        else {
            throw NS_ERROR_NOT_AVAILABLE;
        }
    },
    /**
     * Abort the request immediately.
     */
    abort: function() {
        let channel = channelFor(this);
        channel.QueryInterface(Ci.nsIRequest);
        channel.cancel(Cr.NS_BINDING_ABORTED);
    },
    /**
     * @callback processContentCallback
     * @argument {(string|Object)?} content
     * @return {(string|Object)?} The new content.
     */
    /**
     * Allows to process the full content into new content, however the callback
     * is possibly executed asynchronously and the `OngoingRequest` object might
     * have been destroyed by then.
     * @argument {module:lib/ongoingrequest~processContentCallback}
     */
    processContent: function(callback) {
        if(this.direction === OUTGOING) {
            this.content = callback(this.content);
        }
        else {
            modelFor(this).listener.processor = callback;
        }
    },
    dispose: function() {
        modelFor(this).headers.destroy();
        models.delete(this);
        channels.delete(this);
    }
});

exports.OngoingRequest = OngoingRequest;