UNPKG

35.3 kBJavaScriptView Raw
1/*
2Copyright 2015, 2016 OpenMarket Ltd
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16"use strict";
17/**
18 * This is an internal module. See {@link MatrixHttpApi} for the public class.
19 * @module http-api
20 */
21
22var _create = require('babel-runtime/core-js/object/create');
23
24var _create2 = _interopRequireDefault(_create);
25
26var _stringify = require('babel-runtime/core-js/json/stringify');
27
28var _stringify2 = _interopRequireDefault(_stringify);
29
30var _typeof2 = require('babel-runtime/helpers/typeof');
31
32var _typeof3 = _interopRequireDefault(_typeof2);
33
34var _bluebird = require('bluebird');
35
36var _bluebird2 = _interopRequireDefault(_bluebird);
37
38function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
39
40var parseContentType = require('content-type').parse;
41
42var utils = require("./utils");
43
44// we use our own implementation of setTimeout, so that if we get suspended in
45// the middle of a /sync, we cancel the sync as soon as we awake, rather than
46// waiting for the delay to elapse.
47var callbacks = require("./realtime-callbacks");
48
49/*
50TODO:
51- CS: complete register function (doing stages)
52- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
53*/
54
55/**
56 * A constant representing the URI path for release 0 of the Client-Server HTTP API.
57 */
58module.exports.PREFIX_R0 = "/_matrix/client/r0";
59
60/**
61 * A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
62 */
63module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
64
65/**
66 * URI path for the identity API
67 */
68module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
69
70/**
71 * URI path for the media repo API
72 */
73module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
74
75/**
76 * Construct a MatrixHttpApi.
77 * @constructor
78 * @param {EventEmitter} event_emitter The event emitter to use for emitting events
79 * @param {Object} opts The options to use for this HTTP API.
80 * @param {string} opts.baseUrl Required. The base client-server URL e.g.
81 * 'http://localhost:8008'.
82 * @param {Function} opts.request Required. The function to call for HTTP
83 * requests. This function must look like function(opts, callback){ ... }.
84 * @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
85 * '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
86 *
87 * @param {boolean} opts.onlyData True to return only the 'data' component of the
88 * response (e.g. the parsed HTTP body). If false, requests will return an
89 * object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
90 *
91 * @param {string} opts.accessToken The access_token to send with requests. Can be
92 * null to not send an access token.
93 * @param {Object=} opts.extraParams Optional. Extra query parameters to send on
94 * requests.
95 * @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
96 * before timing out the request. If not specified, there is no timeout.
97 * @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
98 * Authorization header instead of query param to send the access token to the server.
99 */
100module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
101 utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
102 opts.onlyData = opts.onlyData || false;
103 this.event_emitter = event_emitter;
104 this.opts = opts;
105 this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
106 this.uploads = [];
107};
108
109module.exports.MatrixHttpApi.prototype = {
110
111 /**
112 * Get the content repository url with query parameters.
113 * @return {Object} An object with a 'base', 'path' and 'params' for base URL,
114 * path and query parameters respectively.
115 */
116 getContentUri: function getContentUri() {
117 var params = {
118 access_token: this.opts.accessToken
119 };
120 return {
121 base: this.opts.baseUrl,
122 path: "/_matrix/media/v1/upload",
123 params: params
124 };
125 },
126
127 /**
128 * Upload content to the Home Server
129 *
130 * @param {object} file The object to upload. On a browser, something that
131 * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
132 * a Buffer, String or ReadStream.
133 *
134 * @param {object} opts options object
135 *
136 * @param {string=} opts.name Name to give the file on the server. Defaults
137 * to <tt>file.name</tt>.
138 *
139 * @param {string=} opts.type Content-type for the upload. Defaults to
140 * <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
141 *
142 * @param {boolean=} opts.rawResponse Return the raw body, rather than
143 * parsing the JSON. Defaults to false (except on node.js, where it
144 * defaults to true for backwards compatibility).
145 *
146 * @param {boolean=} opts.onlyContentUri Just return the content URI,
147 * rather than the whole body. Defaults to false (except on browsers,
148 * where it defaults to true for backwards compatibility). Ignored if
149 * opts.rawResponse is true.
150 *
151 * @param {Function=} opts.callback Deprecated. Optional. The callback to
152 * invoke on success/failure. See the promise return values for more
153 * information.
154 *
155 * @param {Function=} opts.progressHandler Optional. Called when a chunk of
156 * data has been uploaded, with an object containing the fields `loaded`
157 * (number of bytes transferred) and `total` (total size, if known).
158 *
159 * @return {module:client.Promise} Resolves to response object, as
160 * determined by this.opts.onlyData, opts.rawResponse, and
161 * opts.onlyContentUri. Rejects with an error (usually a MatrixError).
162 */
163 uploadContent: function uploadContent(file, opts) {
164 var _this = this;
165
166 if (utils.isFunction(opts)) {
167 // opts used to be callback
168 opts = {
169 callback: opts
170 };
171 } else if (opts === undefined) {
172 opts = {};
173 }
174
175 // if the file doesn't have a mime type, use a default since
176 // the HS errors if we don't supply one.
177 var contentType = opts.type || file.type || 'application/octet-stream';
178 var fileName = opts.name || file.name;
179
180 // we used to recommend setting file.stream to the thing to upload on
181 // nodejs.
182 var body = file.stream ? file.stream : file;
183
184 // backwards-compatibility hacks where we used to do different things
185 // between browser and node.
186 var rawResponse = opts.rawResponse;
187 if (rawResponse === undefined) {
188 if (global.XMLHttpRequest) {
189 rawResponse = false;
190 } else {
191 console.warn("Returning the raw JSON from uploadContent(). Future " + "versions of the js-sdk will change this default, to " + "return the parsed object. Set opts.rawResponse=false " + "to change this behaviour now.");
192 rawResponse = true;
193 }
194 }
195
196 var onlyContentUri = opts.onlyContentUri;
197 if (!rawResponse && onlyContentUri === undefined) {
198 if (global.XMLHttpRequest) {
199 console.warn("Returning only the content-uri from uploadContent(). " + "Future versions of the js-sdk will change this " + "default, to return the whole response object. Set " + "opts.onlyContentUri=false to change this behaviour now.");
200 onlyContentUri = true;
201 } else {
202 onlyContentUri = false;
203 }
204 }
205
206 // browser-request doesn't support File objects because it deep-copies
207 // the options using JSON.parse(JSON.stringify(options)). Instead of
208 // loading the whole file into memory as a string and letting
209 // browser-request base64 encode and then decode it again, we just
210 // use XMLHttpRequest directly.
211 // (browser-request doesn't support progress either, which is also kind
212 // of important here)
213
214 var upload = { loaded: 0, total: 0 };
215 var promise = void 0;
216
217 // XMLHttpRequest doesn't parse JSON for us. request normally does, but
218 // we're setting opts.json=false so that it doesn't JSON-encode the
219 // request, which also means it doesn't JSON-decode the response. Either
220 // way, we have to JSON-parse the response ourselves.
221 var bodyParser = null;
222 if (!rawResponse) {
223 bodyParser = function bodyParser(rawBody) {
224 var body = JSON.parse(rawBody);
225 if (onlyContentUri) {
226 body = body.content_uri;
227 if (body === undefined) {
228 throw Error('Bad response');
229 }
230 }
231 return body;
232 };
233 }
234
235 if (global.XMLHttpRequest) {
236 (function () {
237 var defer = _bluebird2.default.defer();
238 var xhr = new global.XMLHttpRequest();
239 upload.xhr = xhr;
240 var cb = requestCallback(defer, opts.callback, _this.opts.onlyData);
241
242 var timeout_fn = function timeout_fn() {
243 xhr.abort();
244 cb(new Error('Timeout'));
245 };
246
247 // set an initial timeout of 30s; we'll advance it each time we get
248 // a progress notification
249 xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
250
251 xhr.onreadystatechange = function () {
252 switch (xhr.readyState) {
253 case global.XMLHttpRequest.DONE:
254 callbacks.clearTimeout(xhr.timeout_timer);
255 var resp;
256 try {
257 if (!xhr.responseText) {
258 throw new Error('No response body.');
259 }
260 resp = xhr.responseText;
261 if (bodyParser) {
262 resp = bodyParser(resp);
263 }
264 } catch (err) {
265 err.http_status = xhr.status;
266 cb(err);
267 return;
268 }
269 cb(undefined, xhr, resp);
270 break;
271 }
272 };
273 xhr.upload.addEventListener("progress", function (ev) {
274 callbacks.clearTimeout(xhr.timeout_timer);
275 upload.loaded = ev.loaded;
276 upload.total = ev.total;
277 xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
278 if (opts.progressHandler) {
279 opts.progressHandler({
280 loaded: ev.loaded,
281 total: ev.total
282 });
283 }
284 });
285 var url = _this.opts.baseUrl + "/_matrix/media/v1/upload";
286 url += "?access_token=" + encodeURIComponent(_this.opts.accessToken);
287 url += "&filename=" + encodeURIComponent(fileName);
288
289 xhr.open("POST", url);
290 xhr.setRequestHeader("Content-Type", contentType);
291 xhr.send(body);
292 promise = defer.promise;
293
294 // dirty hack (as per _request) to allow the upload to be cancelled.
295 promise.abort = xhr.abort.bind(xhr);
296 })();
297 } else {
298 var queryParams = {
299 filename: fileName
300 };
301
302 promise = this.authedRequest(opts.callback, "POST", "/upload", queryParams, body, {
303 prefix: "/_matrix/media/v1",
304 headers: { "Content-Type": contentType },
305 json: false,
306 bodyParser: bodyParser
307 });
308 }
309
310 var self = this;
311
312 // remove the upload from the list on completion
313 var promise0 = promise.finally(function () {
314 for (var i = 0; i < self.uploads.length; ++i) {
315 if (self.uploads[i] === upload) {
316 self.uploads.splice(i, 1);
317 return;
318 }
319 }
320 });
321
322 // copy our dirty abort() method to the new promise
323 promise0.abort = promise.abort;
324
325 upload.promise = promise0;
326 this.uploads.push(upload);
327
328 return promise0;
329 },
330
331 cancelUpload: function cancelUpload(promise) {
332 if (promise.abort) {
333 promise.abort();
334 return true;
335 }
336 return false;
337 },
338
339 getCurrentUploads: function getCurrentUploads() {
340 return this.uploads;
341 },
342
343 idServerRequest: function idServerRequest(callback, method, path, params, prefix) {
344 var fullUri = this.opts.idBaseUrl + prefix + path;
345
346 if (callback !== undefined && !utils.isFunction(callback)) {
347 throw Error("Expected callback to be a function but got " + (typeof callback === 'undefined' ? 'undefined' : (0, _typeof3.default)(callback)));
348 }
349
350 var opts = {
351 uri: fullUri,
352 method: method,
353 withCredentials: false,
354 json: false,
355 _matrix_opts: this.opts
356 };
357 if (method == 'GET') {
358 opts.qs = params;
359 } else {
360 opts.form = params;
361 }
362
363 var defer = _bluebird2.default.defer();
364 this.opts.request(opts, requestCallback(defer, callback, this.opts.onlyData));
365 // ID server does not always take JSON, so we can't use requests' 'json'
366 // option as we do with the home server, but it does return JSON, so
367 // parse it manually
368 return defer.promise.then(function (response) {
369 return JSON.parse(response);
370 });
371 },
372
373 /**
374 * Perform an authorised request to the homeserver.
375 * @param {Function} callback Optional. The callback to invoke on
376 * success/failure. See the promise return values for more information.
377 * @param {string} method The HTTP method e.g. "GET".
378 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
379 * "/createRoom".
380 *
381 * @param {Object=} queryParams A dict of query params (these will NOT be
382 * urlencoded). If unspecified, there will be no query params.
383 *
384 * @param {Object} data The HTTP JSON body.
385 *
386 * @param {Object|Number=} opts additional options. If a number is specified,
387 * this is treated as `opts.localTimeoutMs`.
388 *
389 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
390 * timing out the request. If not specified, there is no timeout.
391 *
392 * @param {sting=} opts.prefix The full prefix to use e.g.
393 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
394 *
395 * @param {Object=} opts.headers map of additional request headers
396 *
397 * @return {module:client.Promise} Resolves to <code>{data: {Object},
398 * headers: {Object}, code: {Number}}</code>.
399 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
400 * object only.
401 * @return {module:http-api.MatrixError} Rejects with an error if a problem
402 * occurred. This includes network problems and Matrix-specific error JSON.
403 */
404 authedRequest: function authedRequest(callback, method, path, queryParams, data, opts) {
405 if (!queryParams) {
406 queryParams = {};
407 }
408 if (this.useAuthorizationHeader) {
409 if (isFinite(opts)) {
410 // opts used to be localTimeoutMs
411 opts = {
412 localTimeoutMs: opts
413 };
414 }
415 if (!opts) {
416 opts = {};
417 }
418 if (!opts.headers) {
419 opts.headers = {};
420 }
421 if (!opts.headers.Authorization) {
422 opts.headers.Authorization = "Bearer " + this.opts.accessToken;
423 }
424 if (queryParams.access_token) {
425 delete queryParams.access_token;
426 }
427 } else {
428 if (!queryParams.access_token) {
429 queryParams.access_token = this.opts.accessToken;
430 }
431 }
432
433 var requestPromise = this.request(callback, method, path, queryParams, data, opts);
434
435 var self = this;
436 requestPromise.catch(function (err) {
437 if (err.errcode == 'M_UNKNOWN_TOKEN') {
438 self.event_emitter.emit("Session.logged_out");
439 } else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
440 self.event_emitter.emit("no_consent", err.message, err.data.consent_uri);
441 }
442 });
443
444 // return the original promise, otherwise tests break due to it having to
445 // go around the event loop one more time to process the result of the request
446 return requestPromise;
447 },
448
449 /**
450 * Perform a request to the homeserver without any credentials.
451 * @param {Function} callback Optional. The callback to invoke on
452 * success/failure. See the promise return values for more information.
453 * @param {string} method The HTTP method e.g. "GET".
454 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
455 * "/createRoom".
456 *
457 * @param {Object=} queryParams A dict of query params (these will NOT be
458 * urlencoded). If unspecified, there will be no query params.
459 *
460 * @param {Object} data The HTTP JSON body.
461 *
462 * @param {Object=} opts additional options
463 *
464 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
465 * timing out the request. If not specified, there is no timeout.
466 *
467 * @param {sting=} opts.prefix The full prefix to use e.g.
468 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
469 *
470 * @param {Object=} opts.headers map of additional request headers
471 *
472 * @return {module:client.Promise} Resolves to <code>{data: {Object},
473 * headers: {Object}, code: {Number}}</code>.
474 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
475 * object only.
476 * @return {module:http-api.MatrixError} Rejects with an error if a problem
477 * occurred. This includes network problems and Matrix-specific error JSON.
478 */
479 request: function request(callback, method, path, queryParams, data, opts) {
480 opts = opts || {};
481 var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
482 var fullUri = this.opts.baseUrl + prefix + path;
483
484 return this.requestOtherUrl(callback, method, fullUri, queryParams, data, opts);
485 },
486
487 /**
488 * Perform an authorised request to the homeserver with a specific path
489 * prefix which overrides the default for this call only. Useful for hitting
490 * different Matrix Client-Server versions.
491 * @param {Function} callback Optional. The callback to invoke on
492 * success/failure. See the promise return values for more information.
493 * @param {string} method The HTTP method e.g. "GET".
494 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
495 * "/createRoom".
496 * @param {Object} queryParams A dict of query params (these will NOT be
497 * urlencoded).
498 * @param {Object} data The HTTP JSON body.
499 * @param {string} prefix The full prefix to use e.g.
500 * "/_matrix/client/v2_alpha".
501 * @param {Number=} localTimeoutMs The maximum amount of time to wait before
502 * timing out the request. If not specified, there is no timeout.
503 * @return {module:client.Promise} Resolves to <code>{data: {Object},
504 * headers: {Object}, code: {Number}}</code>.
505 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
506 * object only.
507 * @return {module:http-api.MatrixError} Rejects with an error if a problem
508 * occurred. This includes network problems and Matrix-specific error JSON.
509 *
510 * @deprecated prefer authedRequest with opts.prefix
511 */
512 authedRequestWithPrefix: function authedRequestWithPrefix(callback, method, path, queryParams, data, prefix, localTimeoutMs) {
513 return this.authedRequest(callback, method, path, queryParams, data, {
514 localTimeoutMs: localTimeoutMs,
515 prefix: prefix
516 });
517 },
518
519 /**
520 * Perform a request to the homeserver without any credentials but with a
521 * specific path prefix which overrides the default for this call only.
522 * Useful for hitting different Matrix Client-Server versions.
523 * @param {Function} callback Optional. The callback to invoke on
524 * success/failure. See the promise return values for more information.
525 * @param {string} method The HTTP method e.g. "GET".
526 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
527 * "/createRoom".
528 * @param {Object} queryParams A dict of query params (these will NOT be
529 * urlencoded).
530 * @param {Object} data The HTTP JSON body.
531 * @param {string} prefix The full prefix to use e.g.
532 * "/_matrix/client/v2_alpha".
533 * @param {Number=} localTimeoutMs The maximum amount of time to wait before
534 * timing out the request. If not specified, there is no timeout.
535 * @return {module:client.Promise} Resolves to <code>{data: {Object},
536 * headers: {Object}, code: {Number}}</code>.
537 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
538 * object only.
539 * @return {module:http-api.MatrixError} Rejects with an error if a problem
540 * occurred. This includes network problems and Matrix-specific error JSON.
541 *
542 * @deprecated prefer request with opts.prefix
543 */
544 requestWithPrefix: function requestWithPrefix(callback, method, path, queryParams, data, prefix, localTimeoutMs) {
545 return this.request(callback, method, path, queryParams, data, {
546 localTimeoutMs: localTimeoutMs,
547 prefix: prefix
548 });
549 },
550
551 /**
552 * Perform a request to an arbitrary URL.
553 * @param {Function} callback Optional. The callback to invoke on
554 * success/failure. See the promise return values for more information.
555 * @param {string} method The HTTP method e.g. "GET".
556 * @param {string} uri The HTTP URI
557 *
558 * @param {Object=} queryParams A dict of query params (these will NOT be
559 * urlencoded). If unspecified, there will be no query params.
560 *
561 * @param {Object} data The HTTP JSON body.
562 *
563 * @param {Object=} opts additional options
564 *
565 * @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
566 * timing out the request. If not specified, there is no timeout.
567 *
568 * @param {sting=} opts.prefix The full prefix to use e.g.
569 * "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
570 *
571 * @param {Object=} opts.headers map of additional request headers
572 *
573 * @return {module:client.Promise} Resolves to <code>{data: {Object},
574 * headers: {Object}, code: {Number}}</code>.
575 * If <code>onlyData</code> is set, this will resolve to the <code>data</code>
576 * object only.
577 * @return {module:http-api.MatrixError} Rejects with an error if a problem
578 * occurred. This includes network problems and Matrix-specific error JSON.
579 */
580 requestOtherUrl: function requestOtherUrl(callback, method, uri, queryParams, data, opts) {
581 if (opts === undefined || opts === null) {
582 opts = {};
583 } else if (isFinite(opts)) {
584 // opts used to be localTimeoutMs
585 opts = {
586 localTimeoutMs: opts
587 };
588 }
589
590 return this._request(callback, method, uri, queryParams, data, opts);
591 },
592
593 /**
594 * Form and return a homeserver request URL based on the given path
595 * params and prefix.
596 * @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
597 * "/createRoom".
598 * @param {Object} queryParams A dict of query params (these will NOT be
599 * urlencoded).
600 * @param {string} prefix The full prefix to use e.g.
601 * "/_matrix/client/v2_alpha".
602 * @return {string} URL
603 */
604 getUrl: function getUrl(path, queryParams, prefix) {
605 var queryString = "";
606 if (queryParams) {
607 queryString = "?" + utils.encodeParams(queryParams);
608 }
609 return this.opts.baseUrl + prefix + path + queryString;
610 },
611
612 /**
613 * @private
614 *
615 * @param {function} callback
616 * @param {string} method
617 * @param {string} uri
618 * @param {object} queryParams
619 * @param {object|string} data
620 * @param {object=} opts
621 *
622 * @param {boolean} [opts.json =true] Json-encode data before sending, and
623 * decode response on receipt. (We will still json-decode error
624 * responses, even if this is false.)
625 *
626 * @param {object=} opts.headers extra request headers
627 *
628 * @param {number=} opts.localTimeoutMs client-side timeout for the
629 * request. Default timeout if falsy.
630 *
631 * @param {function=} opts.bodyParser function to parse the body of the
632 * response before passing it to the promise and callback.
633 *
634 * @return {module:client.Promise} a promise which resolves to either the
635 * response object (if this.opts.onlyData is truthy), or the parsed
636 * body. Rejects
637 */
638 _request: function _request(callback, method, uri, queryParams, data, opts) {
639 if (callback !== undefined && !utils.isFunction(callback)) {
640 throw Error("Expected callback to be a function but got " + (typeof callback === 'undefined' ? 'undefined' : (0, _typeof3.default)(callback)));
641 }
642 opts = opts || {};
643
644 var self = this;
645 if (this.opts.extraParams) {
646 for (var key in this.opts.extraParams) {
647 if (!this.opts.extraParams.hasOwnProperty(key)) {
648 continue;
649 }
650 queryParams[key] = this.opts.extraParams[key];
651 }
652 }
653
654 var headers = utils.extend({}, opts.headers || {});
655 var json = opts.json === undefined ? true : opts.json;
656 var bodyParser = opts.bodyParser;
657
658 // we handle the json encoding/decoding here, because request and
659 // browser-request make a mess of it. Specifically, they attempt to
660 // json-decode plain-text error responses, which in turn means that the
661 // actual error gets swallowed by a SyntaxError.
662
663 if (json) {
664 if (data) {
665 data = (0, _stringify2.default)(data);
666 headers['content-type'] = 'application/json';
667 }
668
669 if (!headers['accept']) {
670 headers['accept'] = 'application/json';
671 }
672
673 if (bodyParser === undefined) {
674 bodyParser = function bodyParser(rawBody) {
675 return JSON.parse(rawBody);
676 };
677 }
678 }
679
680 var defer = _bluebird2.default.defer();
681
682 var timeoutId = void 0;
683 var timedOut = false;
684 var req = void 0;
685 var localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
686
687 var resetTimeout = function resetTimeout() {
688 if (localTimeoutMs) {
689 if (timeoutId) {
690 callbacks.clearTimeout(timeoutId);
691 }
692 timeoutId = callbacks.setTimeout(function () {
693 timedOut = true;
694 if (req && req.abort) {
695 req.abort();
696 }
697 defer.reject(new module.exports.MatrixError({
698 error: "Locally timed out waiting for a response",
699 errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
700 timeout: localTimeoutMs
701 }));
702 }, localTimeoutMs);
703 }
704 };
705 resetTimeout();
706
707 var reqPromise = defer.promise;
708
709 try {
710 req = this.opts.request({
711 uri: uri,
712 method: method,
713 withCredentials: false,
714 qs: queryParams,
715 body: data,
716 json: false,
717 timeout: localTimeoutMs,
718 headers: opts.headers || {},
719 _matrix_opts: this.opts
720 }, function (err, response, body) {
721 if (localTimeoutMs) {
722 callbacks.clearTimeout(timeoutId);
723 if (timedOut) {
724 return; // already rejected promise
725 }
726 }
727
728 var handlerFn = requestCallback(defer, callback, self.opts.onlyData, bodyParser);
729 handlerFn(err, response, body);
730 });
731 if (req) {
732 // This will only work in a browser, where opts.request is the
733 // `browser-request` import. Currently `request` does not support progress
734 // updates - see https://github.com/request/request/pull/2346.
735 // `browser-request` returns an XHRHttpRequest which exposes `onprogress`
736 if ('onprogress' in req) {
737 req.onprogress = function (e) {
738 // Prevent the timeout from rejecting the deferred promise if progress is
739 // seen with the request
740 resetTimeout();
741 };
742 }
743
744 // FIXME: This is EVIL, but I can't think of a better way to expose
745 // abort() operations on underlying HTTP requests :(
746 if (req.abort) reqPromise.abort = req.abort.bind(req);
747 }
748 } catch (ex) {
749 defer.reject(ex);
750 if (callback) {
751 callback(ex);
752 }
753 }
754 return reqPromise;
755 }
756};
757
758/*
759 * Returns a callback that can be invoked by an HTTP request on completion,
760 * that will either resolve or reject the given defer as well as invoke the
761 * given userDefinedCallback (if any).
762 *
763 * HTTP errors are transformed into javascript errors and the deferred is rejected.
764 *
765 * If bodyParser is given, it is used to transform the body of the successful
766 * responses before passing to the defer/callback.
767 *
768 * If onlyData is true, the defer/callback is invoked with the body of the
769 * response, otherwise the result object (with `code` and `data` fields)
770 *
771 */
772var requestCallback = function requestCallback(defer, userDefinedCallback, onlyData, bodyParser) {
773 userDefinedCallback = userDefinedCallback || function () {};
774
775 return function (err, response, body) {
776 if (!err) {
777 try {
778 if (response.statusCode >= 400) {
779 err = parseErrorResponse(response, body);
780 } else if (bodyParser) {
781 body = bodyParser(body);
782 }
783 } catch (e) {
784 err = new Error('Error parsing server response: ' + e);
785 }
786 }
787
788 if (err) {
789 defer.reject(err);
790 userDefinedCallback(err);
791 } else {
792 var res = {
793 code: response.statusCode,
794
795 // XXX: why do we bother with this? it doesn't work for
796 // XMLHttpRequest, so clearly we don't use it.
797 headers: response.headers,
798 data: body
799 };
800 defer.resolve(onlyData ? body : res);
801 userDefinedCallback(null, onlyData ? body : res);
802 }
803 };
804};
805
806/**
807 * Attempt to turn an HTTP error response into a Javascript Error.
808 *
809 * If it is a JSON response, we will parse it into a MatrixError. Otherwise
810 * we return a generic Error.
811 *
812 * @param {XMLHttpRequest|http.IncomingMessage} response response object
813 * @param {String} body raw body of the response
814 * @returns {Error}
815 */
816function parseErrorResponse(response, body) {
817 var httpStatus = response.statusCode;
818 var contentType = getResponseContentType(response);
819
820 var err = void 0;
821 if (contentType) {
822 if (contentType.type === 'application/json') {
823 err = new module.exports.MatrixError(JSON.parse(body));
824 } else if (contentType.type === 'text/plain') {
825 err = new Error('Server returned ' + httpStatus + ' error: ' + body);
826 }
827 }
828
829 if (!err) {
830 err = new Error('Server returned ' + httpStatus + ' error');
831 }
832 err.httpStatus = httpStatus;
833 return err;
834}
835
836/**
837 * extract the Content-Type header from the response object, and
838 * parse it to a `{type, parameters}` object.
839 *
840 * returns null if no content-type header could be found.
841 *
842 * @param {XMLHttpRequest|http.IncomingMessage} response response object
843 * @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
844 */
845function getResponseContentType(response) {
846 var contentType = void 0;
847 if (response.getResponseHeader) {
848 // XMLHttpRequest provides getResponseHeader
849 contentType = response.getResponseHeader("Content-Type");
850 } else if (response.headers) {
851 // request provides http.IncomingMessage which has a message.headers map
852 contentType = response.headers['content-type'] || null;
853 }
854
855 if (!contentType) {
856 return null;
857 }
858
859 try {
860 return parseContentType(contentType);
861 } catch (e) {
862 throw new Error('Error parsing Content-Type \'' + contentType + '\': ' + e);
863 }
864}
865
866/**
867 * Construct a Matrix error. This is a JavaScript Error with additional
868 * information specific to the standard Matrix error response.
869 * @constructor
870 * @param {Object} errorJson The Matrix error JSON returned from the homeserver.
871 * @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
872 * @prop {string} name Same as MatrixError.errcode but with a default unknown string.
873 * @prop {string} message The Matrix 'error' value, e.g. "Missing token."
874 * @prop {Object} data The raw Matrix error JSON used to construct this object.
875 * @prop {integer} httpStatus The numeric HTTP status code given
876 */
877module.exports.MatrixError = function MatrixError(errorJson) {
878 errorJson = errorJson || {};
879 this.errcode = errorJson.errcode;
880 this.name = errorJson.errcode || "Unknown error code";
881 this.message = errorJson.error || "Unknown message";
882 this.data = errorJson;
883};
884module.exports.MatrixError.prototype = (0, _create2.default)(Error.prototype);
885/** */
886module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
887//# sourceMappingURL=http-api.js.map
\No newline at end of file