diff --git a/lib/http-api.js b/lib/http-api.js index 42bc54e303b..bc5ac51ed88 100644 --- a/lib/http-api.js +++ b/lib/http-api.js @@ -113,8 +113,6 @@ module.exports.MatrixHttpApi.prototype = { "Expected callback to be a function but got " + typeof callback ); } - var defer = q.defer(); - var url = this.opts.baseUrl + "/_matrix/media/v1/upload"; // browser-request doesn't support File objects because it deep-copies // the options using JSON.parse(JSON.stringify(options)). Instead of // loading the whole file into memory as a string and letting @@ -124,8 +122,9 @@ module.exports.MatrixHttpApi.prototype = { // of important here) var upload = { loaded: 0, total: 0 }; - + var promise; if (global.XMLHttpRequest) { + var defer = q.defer(); var xhr = new global.XMLHttpRequest(); upload.xhr = xhr; var cb = requestCallback(defer, callback, this.opts.onlyData); @@ -168,6 +167,7 @@ module.exports.MatrixHttpApi.prototype = { xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000); defer.notify(ev); }); + var url = this.opts.baseUrl + "/_matrix/media/v1/upload"; url += "?access_token=" + encodeURIComponent(this.opts.accessToken); url += "&filename=" + encodeURIComponent(file.name); @@ -180,47 +180,48 @@ module.exports.MatrixHttpApi.prototype = { xhr.setRequestHeader("Content-Type", 'application/octet-stream'); } xhr.send(file); + promise = defer.promise; + + // dirty hack (as per _request) to allow the upload to be cancelled. + promise.abort = xhr.abort.bind(xhr); } else { var queryParams = { filename: file.name, - access_token: this.opts.accessToken }; - upload.request = this.opts.request({ - uri: url, - qs: queryParams, - method: "POST", - headers: {"Content-Type": file.type}, - body: file.stream - }, requestCallback(defer, callback, this.opts.onlyData)); + promise = this.authedRequest( + callback, "POST", "/upload", queryParams, file.stream, { + prefix: "/_matrix/media/v1", + localTimeoutMs: 30000, + headers: {"Content-Type": file.type}, + } + ); } - this.uploads.push(upload); - var self = this; - upload.promise = defer.promise.finally(function() { - var uploadsKeys = Object.keys(self.uploads); - for (var i = 0; i < uploadsKeys.length; ++i) { - if (self.uploads[uploadsKeys[i]].promise === defer.promise) { - self.uploads.splice(uploadsKeys[i], 1); + + // remove the upload from the list on completion + var promise0 = promise.finally(function() { + for (var i = 0; i < self.uploads.length; ++i) { + if (self.uploads[i] === upload) { + self.uploads.splice(i, 1); + return; } } }); - return upload.promise; + + // copy our dirty abort() method to the new promise + promise0.abort = promise.abort; + + upload.promise = promise0; + this.uploads.push(upload); + + return promise0; }, cancelUpload: function(promise) { - var uploadsKeys = Object.keys(this.uploads); - for (var i = 0; i < uploadsKeys.length; ++i) { - var upload = this.uploads[uploadsKeys[i]]; - if (upload.promise === promise) { - if (upload.xhr !== undefined) { - upload.xhr.abort(); - return true; - } else if (upload.request !== undefined) { - upload.request.abort(); - return true; - } - } + if (promise.abort) { + promise.abort(); + return true; } return false; }, @@ -271,11 +272,22 @@ module.exports.MatrixHttpApi.prototype = { * @param {string} method The HTTP method e.g. "GET". * @param {string} path The HTTP path after the supplied prefix e.g. * "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be - * urlencoded). + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * * @param {Object} data The HTTP JSON body. - * @param {Number=} localTimeoutMs The maximum amount of time to wait before + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data @@ -283,18 +295,25 @@ module.exports.MatrixHttpApi.prototype = { * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ - authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) { - if (!queryParams) { queryParams = {}; } - queryParams.access_token = this.opts.accessToken; - var self = this; + authedRequest: function(callback, method, path, queryParams, data, opts) { + if (!queryParams) { + queryParams = {}; + } + if (!queryParams.access_token) { + queryParams.access_token = this.opts.accessToken; + } + var request_promise = this.request( - callback, method, path, queryParams, data, localTimeoutMs + callback, method, path, queryParams, data, opts ); + + var self = this; request_promise.catch(function(err) { if (err.errcode == 'M_UNKNOWN_TOKEN') { self.event_emitter.emit("Session.logged_out"); } }); + // return the original promise, otherwise tests break due to it having to // go around the event loop one more time to process the result of the request return request_promise; @@ -307,11 +326,22 @@ module.exports.MatrixHttpApi.prototype = { * @param {string} method The HTTP method e.g. "GET". * @param {string} path The HTTP path after the supplied prefix e.g. * "/createRoom". - * @param {Object} queryParams A dict of query params (these will NOT be - * urlencoded). + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * * @param {Object} data The HTTP JSON body. - * @param {Number=} localTimeoutMs The maximum amount of time to wait before + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data @@ -319,9 +349,13 @@ module.exports.MatrixHttpApi.prototype = { * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. */ - request: function(callback, method, path, queryParams, data, localTimeoutMs) { - return this.requestWithPrefix( - callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs + request: function(callback, method, path, queryParams, data, opts) { + opts = opts || {}; + var prefix = opts.prefix || this.opts.prefix; + var fullUri = this.opts.baseUrl + prefix + path; + + return this.requestOtherUrl( + callback, method, fullUri, queryParams, data, opts ); }, @@ -347,16 +381,16 @@ module.exports.MatrixHttpApi.prototype = { * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. + * + * @deprecated prefer authedRequest with opts.prefix */ authedRequestWithPrefix: function(callback, method, path, queryParams, data, prefix, localTimeoutMs) { - var fullUri = this.opts.baseUrl + prefix + path; - if (!queryParams) { - queryParams = {}; - } - queryParams.access_token = this.opts.accessToken; - return this._request( - callback, method, fullUri, queryParams, data, localTimeoutMs + return this.authedRequest( + callback, method, path, queryParams, data, { + localTimeoutMs: localTimeoutMs, + prefix: prefix, + } ); }, @@ -382,15 +416,16 @@ module.exports.MatrixHttpApi.prototype = { * object only. * @return {module:http-api.MatrixError} Rejects with an error if a problem * occurred. This includes network problems and Matrix-specific error JSON. + * + * @deprecated prefer request with opts.prefix */ requestWithPrefix: function(callback, method, path, queryParams, data, prefix, localTimeoutMs) { - var fullUri = this.opts.baseUrl + prefix + path; - if (!queryParams) { - queryParams = {}; - } - return this._request( - callback, method, fullUri, queryParams, data, localTimeoutMs + return this.request( + callback, method, path, queryParams, data, { + localTimeoutMs: localTimeoutMs, + prefix: prefix, + } ); }, @@ -400,11 +435,22 @@ module.exports.MatrixHttpApi.prototype = { * success/failure. See the promise return values for more information. * @param {string} method The HTTP method e.g. "GET". * @param {string} uri The HTTP URI - * @param {Object} queryParams A dict of query params (these will NOT be - * urlencoded). + * + * @param {Object=} queryParams A dict of query params (these will NOT be + * urlencoded). If unspecified, there will be no query params. + * * @param {Object} data The HTTP JSON body. - * @param {Number=} localTimeoutMs The maximum amount of time to wait before + * + * @param {Object=} opts additional options + * + * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before * timing out the request. If not specified, there is no timeout. + * + * @param {sting=} opts.prefix The full prefix to use e.g. + * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix. + * + * @param {Object=} opts.headers map of additional request headers + * * @return {module:client.Promise} Resolves to {data: {Object}, * headers: {Object}, code: {Number}}. * If onlyData is set, this will resolve to the data @@ -413,12 +459,18 @@ module.exports.MatrixHttpApi.prototype = { * occurred. This includes network problems and Matrix-specific error JSON. */ requestOtherUrl: function(callback, method, uri, queryParams, data, - localTimeoutMs) { - if (!queryParams) { - queryParams = {}; + opts) { + if (opts === undefined || opts === null) { + opts = {}; + } else if (isFinite(opts)) { + // opts used to be localTimeoutMs + opts = { + localTimeoutMs: opts + }; } + return this._request( - callback, method, uri, queryParams, data, localTimeoutMs + callback, method, uri, queryParams, data, opts ); }, @@ -441,16 +493,15 @@ module.exports.MatrixHttpApi.prototype = { return this.opts.baseUrl + prefix + path + queryString; }, - _request: function(callback, method, uri, queryParams, data, localTimeoutMs) { + _request: function(callback, method, uri, queryParams, data, opts) { if (callback !== undefined && !utils.isFunction(callback)) { throw Error( "Expected callback to be a function but got " + typeof callback ); } + opts = opts || {}; + var self = this; - if (!queryParams) { - queryParams = {}; - } if (this.opts.extraParams) { for (var key in this.opts.extraParams) { if (!this.opts.extraParams.hasOwnProperty(key)) { continue; } @@ -462,6 +513,7 @@ module.exports.MatrixHttpApi.prototype = { var timeoutId; var timedOut = false; var req; + var localTimeoutMs = opts.localTimeoutMs; if (localTimeoutMs) { timeoutId = callbacks.setTimeout(function() { timedOut = true; @@ -488,6 +540,7 @@ module.exports.MatrixHttpApi.prototype = { body: data, json: true, timeout: localTimeoutMs, + headers: opts.headers || {}, _matrix_opts: this.opts }, function(err, response, body) {