UNPKG

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