/** * Copyright (c) 2015-present, Parse, LLC. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @flow */ /* global XMLHttpRequest, XDomainRequest */ import CoreManager from './CoreManager'; import ParseError from './ParseError'; /*:: export type RequestOptions = { useMasterKey?: boolean; sessionToken?: string; installationId?: string; returnStatus?: boolean; batchSize?: number; include?: any; progress?: any; };*/ /*:: export type FullOptions = { success?: any; error?: any; useMasterKey?: boolean; sessionToken?: string; installationId?: string; progress?: any; };*/ let XHR = null; if (typeof XMLHttpRequest !== 'undefined') { XHR = XMLHttpRequest; } let useXDomainRequest = false; if (typeof XDomainRequest !== 'undefined' && !('withCredentials' in new XMLHttpRequest())) { useXDomainRequest = true; } function ajaxIE9(method /*: string*/ , url /*: string*/ , data /*: any*/ , options /*:: ?: FullOptions*/ ) { return new Promise((resolve, reject) => { const xdr = new XDomainRequest(); xdr.onload = function () { let response; try { response = JSON.parse(xdr.responseText); } catch (e) { reject(e); } if (response) { resolve({ response }); } }; xdr.onerror = xdr.ontimeout = function () { // Let's fake a real error message. const fakeResponse = { responseText: JSON.stringify({ code: ParseError.X_DOMAIN_REQUEST, error: 'IE\'s XDomainRequest does not supply error info.' }) }; reject(fakeResponse); }; xdr.onprogress = function () { if (options && typeof options.progress === 'function') { options.progress(xdr.responseText); } }; xdr.open(method, url); xdr.send(data); }); } const RESTController = { ajax(method /*: string*/ , url /*: string*/ , data /*: any*/ , headers /*:: ?: any*/ , options /*:: ?: FullOptions*/ ) { if (useXDomainRequest) { return ajaxIE9(method, url, data, headers, options); } let res, rej; const promise = new Promise((resolve, reject) => { res = resolve; rej = reject; }); promise.resolve = res; promise.reject = rej; let attempts = 0; const dispatch = function () { if (XHR == null) { throw new Error('Cannot make a request: No definition of XMLHttpRequest was found.'); } let handled = false; const xhr = new XHR(); xhr.onreadystatechange = function () { if (xhr.readyState !== 4 || handled) { return; } handled = true; if (xhr.status >= 200 && xhr.status < 300) { let response; try { response = JSON.parse(xhr.responseText); if (typeof xhr.getResponseHeader === 'function') { if ((xhr.getAllResponseHeaders() || '').includes('x-parse-job-status-id: ')) { response = xhr.getResponseHeader('x-parse-job-status-id'); } } } catch (e) { promise.reject(e.toString()); } if (response) { promise.resolve({ response, status: xhr.status, xhr }); } } else if (xhr.status >= 500 || xhr.status === 0) { // retry on 5XX or node-xmlhttprequest error if (++attempts < CoreManager.get('REQUEST_ATTEMPT_LIMIT')) { // Exponentially-growing random delay const delay = Math.round(Math.random() * 125 * Math.pow(2, attempts)); setTimeout(dispatch, delay); } else if (xhr.status === 0) { promise.reject('Unable to connect to the Parse API'); } else { // After the retry limit is reached, fail promise.reject(xhr); } } else { promise.reject(xhr); } }; headers = headers || {}; if (typeof headers['Content-Type'] !== 'string') { headers['Content-Type'] = 'text/plain'; // Avoid pre-flight } if (CoreManager.get('IS_NODE')) { headers['User-Agent'] = 'Parse/' + CoreManager.get('VERSION') + ' (NodeJS ' + process.versions.node + ')'; } if (CoreManager.get('SERVER_AUTH_TYPE') && CoreManager.get('SERVER_AUTH_TOKEN')) { headers['Authorization'] = CoreManager.get('SERVER_AUTH_TYPE') + ' ' + CoreManager.get('SERVER_AUTH_TOKEN'); } if (options && typeof options.progress === 'function') { if (xhr.upload) { xhr.upload.addEventListener('progress', oEvent => { if (oEvent.lengthComputable) { options.progress(oEvent.loaded / oEvent.total); } else { options.progress(null); } }); } else if (xhr.addEventListener) { xhr.addEventListener('progress', oEvent => { if (oEvent.lengthComputable) { options.progress(oEvent.loaded / oEvent.total); } else { options.progress(null); } }); } } xhr.open(method, url, true); for (const h in headers) { xhr.setRequestHeader(h, headers[h]); } xhr.send(data); }; dispatch(); return promise; }, request(method /*: string*/ , path /*: string*/ , data /*: mixed*/ , options /*:: ?: RequestOptions*/ ) { options = options || {}; let url = CoreManager.get('SERVER_URL'); if (url[url.length - 1] !== '/') { url += '/'; } url += path; const payload = {}; if (data && typeof data === 'object') { for (const k in data) { payload[k] = data[k]; } } if (method !== 'POST') { payload._method = method; method = 'POST'; } payload._ApplicationId = CoreManager.get('APPLICATION_ID'); const jsKey = CoreManager.get('JAVASCRIPT_KEY'); if (jsKey) { payload._JavaScriptKey = jsKey; } payload._ClientVersion = CoreManager.get('VERSION'); let useMasterKey = options.useMasterKey; if (typeof useMasterKey === 'undefined') { useMasterKey = CoreManager.get('USE_MASTER_KEY'); } if (useMasterKey) { if (CoreManager.get('MASTER_KEY')) { delete payload._JavaScriptKey; payload._MasterKey = CoreManager.get('MASTER_KEY'); } else { throw new Error('Cannot use the Master Key, it has not been provided.'); } } if (CoreManager.get('FORCE_REVOCABLE_SESSION')) { payload._RevocableSession = '1'; } const installationId = options.installationId; let installationIdPromise; if (installationId && typeof installationId === 'string') { installationIdPromise = Promise.resolve(installationId); } else { const installationController = CoreManager.getInstallationController(); installationIdPromise = installationController.currentInstallationId(); } return installationIdPromise.then(iid => { payload._InstallationId = iid; const userController = CoreManager.getUserController(); if (options && typeof options.sessionToken === 'string') { return Promise.resolve(options.sessionToken); } else if (userController) { return userController.currentUserAsync().then(user => { if (user) { return Promise.resolve(user.getSessionToken()); } return Promise.resolve(null); }); } return Promise.resolve(null); }).then(token => { if (token) { payload._SessionToken = token; } const payloadString = JSON.stringify(payload); return RESTController.ajax(method, url, payloadString, {}, options).then(({ response, status }) => { if (options.returnStatus) { return { ...response, _status: status }; } else { return response; } }); }).catch(function (response /*: { responseText: string }*/ ) { // Transform the error into an instance of ParseError by trying to parse // the error string as JSON let error; if (response && response.responseText) { try { const errorJSON = JSON.parse(response.responseText); error = new ParseError(errorJSON.code, errorJSON.error); } catch (e) { // If we fail to parse the error text, that's okay. error = new ParseError(ParseError.INVALID_JSON, 'Received an error with invalid JSON from Parse: ' + response.responseText); } } else { error = new ParseError(ParseError.CONNECTION_FAILED, 'XMLHttpRequest failed: ' + JSON.stringify(response)); } return Promise.reject(error); }); }, _setXHR(xhr /*: any*/ ) { XHR = xhr; } }; module.exports = RESTController;