RESTController.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /**
  2. * Copyright (c) 2015-present, Parse, LLC.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the BSD-style license found in the
  6. * LICENSE file in the root directory of this source tree. An additional grant
  7. * of patent rights can be found in the PATENTS file in the same directory.
  8. *
  9. * @flow
  10. */
  11. /* global XMLHttpRequest, XDomainRequest */
  12. import CoreManager from './CoreManager';
  13. import ParseError from './ParseError';
  14. /*:: export type RequestOptions = {
  15. useMasterKey?: boolean;
  16. sessionToken?: string;
  17. installationId?: string;
  18. returnStatus?: boolean;
  19. batchSize?: number;
  20. include?: any;
  21. progress?: any;
  22. };*/
  23. /*:: export type FullOptions = {
  24. success?: any;
  25. error?: any;
  26. useMasterKey?: boolean;
  27. sessionToken?: string;
  28. installationId?: string;
  29. progress?: any;
  30. };*/
  31. let XHR = null;
  32. if (typeof XMLHttpRequest !== 'undefined') {
  33. XHR = XMLHttpRequest;
  34. }
  35. let useXDomainRequest = false;
  36. if (typeof XDomainRequest !== 'undefined' && !('withCredentials' in new XMLHttpRequest())) {
  37. useXDomainRequest = true;
  38. }
  39. function ajaxIE9(method
  40. /*: string*/
  41. , url
  42. /*: string*/
  43. , data
  44. /*: any*/
  45. , options
  46. /*:: ?: FullOptions*/
  47. ) {
  48. return new Promise((resolve, reject) => {
  49. const xdr = new XDomainRequest();
  50. xdr.onload = function () {
  51. let response;
  52. try {
  53. response = JSON.parse(xdr.responseText);
  54. } catch (e) {
  55. reject(e);
  56. }
  57. if (response) {
  58. resolve({
  59. response
  60. });
  61. }
  62. };
  63. xdr.onerror = xdr.ontimeout = function () {
  64. // Let's fake a real error message.
  65. const fakeResponse = {
  66. responseText: JSON.stringify({
  67. code: ParseError.X_DOMAIN_REQUEST,
  68. error: 'IE\'s XDomainRequest does not supply error info.'
  69. })
  70. };
  71. reject(fakeResponse);
  72. };
  73. xdr.onprogress = function () {
  74. if (options && typeof options.progress === 'function') {
  75. options.progress(xdr.responseText);
  76. }
  77. };
  78. xdr.open(method, url);
  79. xdr.send(data);
  80. });
  81. }
  82. const RESTController = {
  83. ajax(method
  84. /*: string*/
  85. , url
  86. /*: string*/
  87. , data
  88. /*: any*/
  89. , headers
  90. /*:: ?: any*/
  91. , options
  92. /*:: ?: FullOptions*/
  93. ) {
  94. if (useXDomainRequest) {
  95. return ajaxIE9(method, url, data, headers, options);
  96. }
  97. let res, rej;
  98. const promise = new Promise((resolve, reject) => {
  99. res = resolve;
  100. rej = reject;
  101. });
  102. promise.resolve = res;
  103. promise.reject = rej;
  104. let attempts = 0;
  105. const dispatch = function () {
  106. if (XHR == null) {
  107. throw new Error('Cannot make a request: No definition of XMLHttpRequest was found.');
  108. }
  109. let handled = false;
  110. const xhr = new XHR();
  111. xhr.onreadystatechange = function () {
  112. if (xhr.readyState !== 4 || handled) {
  113. return;
  114. }
  115. handled = true;
  116. if (xhr.status >= 200 && xhr.status < 300) {
  117. let response;
  118. try {
  119. response = JSON.parse(xhr.responseText);
  120. if (typeof xhr.getResponseHeader === 'function') {
  121. if ((xhr.getAllResponseHeaders() || '').includes('x-parse-job-status-id: ')) {
  122. response = xhr.getResponseHeader('x-parse-job-status-id');
  123. }
  124. }
  125. } catch (e) {
  126. promise.reject(e.toString());
  127. }
  128. if (response) {
  129. promise.resolve({
  130. response,
  131. status: xhr.status,
  132. xhr
  133. });
  134. }
  135. } else if (xhr.status >= 500 || xhr.status === 0) {
  136. // retry on 5XX or node-xmlhttprequest error
  137. if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) {
  138. // Exponentially-growing random delay
  139. const delay = Math.round(Math.random() * 125 * Math.pow(2, attempts));
  140. setTimeout(dispatch, delay);
  141. } else if (xhr.status === 0) {
  142. promise.reject('Unable to connect to the Parse API');
  143. } else {
  144. // After the retry limit is reached, fail
  145. promise.reject(xhr);
  146. }
  147. } else {
  148. promise.reject(xhr);
  149. }
  150. };
  151. headers = headers || {};
  152. if (typeof headers['Content-Type'] !== 'string') {
  153. headers['Content-Type'] = 'text/plain'; // Avoid pre-flight
  154. }
  155. if (CoreManager.get('IS_NODE')) {
  156. headers['User-Agent'] = 'Parse/' + CoreManager.get('VERSION') + ' (NodeJS ' + process.versions.node + ')';
  157. }
  158. if (CoreManager.get('SERVER_AUTH_TYPE') && CoreManager.get('SERVER_AUTH_TOKEN')) {
  159. headers['Authorization'] = CoreManager.get('SERVER_AUTH_TYPE') + ' ' + CoreManager.get('SERVER_AUTH_TOKEN');
  160. }
  161. if (options && typeof options.progress === 'function') {
  162. if (xhr.upload) {
  163. xhr.upload.addEventListener('progress', oEvent => {
  164. if (oEvent.lengthComputable) {
  165. options.progress(oEvent.loaded / oEvent.total);
  166. } else {
  167. options.progress(null);
  168. }
  169. });
  170. } else if (xhr.addEventListener) {
  171. xhr.addEventListener('progress', oEvent => {
  172. if (oEvent.lengthComputable) {
  173. options.progress(oEvent.loaded / oEvent.total);
  174. } else {
  175. options.progress(null);
  176. }
  177. });
  178. }
  179. }
  180. xhr.open(method, url, true);
  181. for (const h in headers) {
  182. xhr.setRequestHeader(h, headers[h]);
  183. }
  184. xhr.send(data);
  185. };
  186. dispatch();
  187. return promise;
  188. },
  189. request(method
  190. /*: string*/
  191. , path
  192. /*: string*/
  193. , data
  194. /*: mixed*/
  195. , options
  196. /*:: ?: RequestOptions*/
  197. ) {
  198. options = options || {};
  199. let url = CoreManager.get('SERVER_URL');
  200. if (url[url.length - 1] !== '/') {
  201. url += '/';
  202. }
  203. url += path;
  204. const payload = {};
  205. if (data && typeof data === 'object') {
  206. for (const k in data) {
  207. payload[k] = data[k];
  208. }
  209. }
  210. if (method !== 'POST') {
  211. payload._method = method;
  212. method = 'POST';
  213. }
  214. payload._ApplicationId = CoreManager.get('APPLICATION_ID');
  215. const jsKey = CoreManager.get('JAVASCRIPT_KEY');
  216. if (jsKey) {
  217. payload._JavaScriptKey = jsKey;
  218. }
  219. payload._ClientVersion = CoreManager.get('VERSION');
  220. let useMasterKey = options.useMasterKey;
  221. if (typeof useMasterKey === 'undefined') {
  222. useMasterKey = CoreManager.get('USE_MASTER_KEY');
  223. }
  224. if (useMasterKey) {
  225. if (CoreManager.get('MASTER_KEY')) {
  226. delete payload._JavaScriptKey;
  227. payload._MasterKey = CoreManager.get('MASTER_KEY');
  228. } else {
  229. throw new Error('Cannot use the Master Key, it has not been provided.');
  230. }
  231. }
  232. if (CoreManager.get('FORCE_REVOCABLE_SESSION')) {
  233. payload._RevocableSession = '1';
  234. }
  235. const installationId = options.installationId;
  236. let installationIdPromise;
  237. if (installationId && typeof installationId === 'string') {
  238. installationIdPromise = Promise.resolve(installationId);
  239. } else {
  240. const installationController = CoreManager.getInstallationController();
  241. installationIdPromise = installationController.currentInstallationId();
  242. }
  243. return installationIdPromise.then(iid => {
  244. payload._InstallationId = iid;
  245. const userController = CoreManager.getUserController();
  246. if (options && typeof options.sessionToken === 'string') {
  247. return Promise.resolve(options.sessionToken);
  248. } else if (userController) {
  249. return userController.currentUserAsync().then(user => {
  250. if (user) {
  251. return Promise.resolve(user.getSessionToken());
  252. }
  253. return Promise.resolve(null);
  254. });
  255. }
  256. return Promise.resolve(null);
  257. }).then(token => {
  258. if (token) {
  259. payload._SessionToken = token;
  260. }
  261. const payloadString = JSON.stringify(payload);
  262. return RESTController.ajax(method, url, payloadString, {}, options).then(({
  263. response,
  264. status
  265. }) => {
  266. if (options.returnStatus) {
  267. return { ...response,
  268. _status: status
  269. };
  270. } else {
  271. return response;
  272. }
  273. });
  274. }).catch(function (response
  275. /*: { responseText: string }*/
  276. ) {
  277. // Transform the error into an instance of ParseError by trying to parse
  278. // the error string as JSON
  279. let error;
  280. if (response && response.responseText) {
  281. try {
  282. const errorJSON = JSON.parse(response.responseText);
  283. error = new ParseError(errorJSON.code, errorJSON.error);
  284. } catch (e) {
  285. // If we fail to parse the error text, that's okay.
  286. error = new ParseError(ParseError.INVALID_JSON, 'Received an error with invalid JSON from Parse: ' + response.responseText);
  287. }
  288. } else {
  289. error = new ParseError(ParseError.CONNECTION_FAILED, 'XMLHttpRequest failed: ' + JSON.stringify(response));
  290. }
  291. return Promise.reject(error);
  292. });
  293. },
  294. _setXHR(xhr
  295. /*: any*/
  296. ) {
  297. XHR = xhr;
  298. }
  299. };
  300. module.exports = RESTController;