X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/a6633642232efd164d4708967ab59e498fbff896..refs/pull/5153/head:/resources/js/services/http.js diff --git a/resources/js/services/http.js b/resources/js/services/http.js index b05dd23bf..d95e4a59a 100644 --- a/resources/js/services/http.js +++ b/resources/js/services/http.js @@ -1,56 +1,121 @@ - /** - * Perform a HTTP GET request. - * Can easily pass query parameters as the second parameter. - * @param {String} url - * @param {Object} params - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * @typedef FormattedResponse + * @property {Headers} headers + * @property {Response} original + * @property {Object|String} data + * @property {Boolean} redirected + * @property {Number} status + * @property {string} statusText + * @property {string} url */ -async function get(url, params = {}) { - return request(url, { - method: 'GET', - params, - }); -} /** - * Perform a HTTP POST request. - * @param {String} url - * @param {Object} data - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * Get the content from a fetch response. + * Checks the content-type header to determine the format. + * @param {Response} response + * @returns {Promise} */ -async function post(url, data = null) { - return dataRequest('POST', url, data); +async function getResponseContent(response) { + if (response.status === 204) { + return null; + } + + const responseContentType = response.headers.get('Content-Type') || ''; + const subType = responseContentType.split(';')[0].split('/').pop(); + + if (subType === 'javascript' || subType === 'json') { + return response.json(); + } + + return response.text(); } -/** - * Perform a HTTP PUT request. - * @param {String} url - * @param {Object} data - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} - */ -async function put(url, data = null) { - return dataRequest('PUT', url, data); +export class HttpError extends Error { + + constructor(response, content) { + super(response.statusText); + this.data = content; + this.headers = response.headers; + this.redirected = response.redirected; + this.status = response.status; + this.statusText = response.statusText; + this.url = response.url; + this.original = response; + } + } /** - * Perform a HTTP PATCH request. + * @param {String} method * @param {String} url - * @param {Object} data - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * @param {Object} events + * @return {XMLHttpRequest} */ -async function patch(url, data = null) { - return dataRequest('PATCH', url, data); +export function createXMLHttpRequest(method, url, events = {}) { + const csrfToken = document.querySelector('meta[name=token]').getAttribute('content'); + const req = new XMLHttpRequest(); + + for (const [eventName, callback] of Object.entries(events)) { + req.addEventListener(eventName, callback.bind(req)); + } + + req.open(method, url); + req.withCredentials = true; + req.setRequestHeader('X-CSRF-TOKEN', csrfToken); + + return req; } /** - * Perform a HTTP DELETE request. + * Create a new HTTP request, setting the required CSRF information + * to communicate with the back-end. Parses & formats the response. * @param {String} url - * @param {Object} data - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * @param {Object} options + * @returns {Promise} */ -async function performDelete(url, data = null) { - return dataRequest('DELETE', url, data); +async function request(url, options = {}) { + let requestUrl = url; + + if (!requestUrl.startsWith('http')) { + requestUrl = window.baseUrl(requestUrl); + } + + if (options.params) { + const urlObj = new URL(requestUrl); + for (const paramName of Object.keys(options.params)) { + const value = options.params[paramName]; + if (typeof value !== 'undefined' && value !== null) { + urlObj.searchParams.set(paramName, value); + } + } + requestUrl = urlObj.toString(); + } + + const csrfToken = document.querySelector('meta[name=token]').getAttribute('content'); + const requestOptions = {...options, credentials: 'same-origin'}; + requestOptions.headers = { + ...requestOptions.headers || {}, + baseURL: window.baseUrl(''), + 'X-CSRF-TOKEN': csrfToken, + }; + + const response = await fetch(requestUrl, requestOptions); + const content = await getResponseContent(response); + const returnData = { + data: content, + headers: response.headers, + redirected: response.redirected, + status: response.status, + statusText: response.statusText, + url: response.url, + original: response, + }; + + if (!response.ok) { + throw new HttpError(response, content); + } + + return returnData; } /** @@ -59,11 +124,11 @@ async function performDelete(url, data = null) { * @param {String} method * @param {String} url * @param {Object} data - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * @returns {Promise} */ async function dataRequest(method, url, data = null) { const options = { - method: method, + method, body: data, }; @@ -84,85 +149,90 @@ async function dataRequest(method, url, data = null) { options.method = 'post'; } - return request(url, options) + return request(url, options); } /** - * Create a new HTTP request, setting the required CSRF information - * to communicate with the back-end. Parses & formats the response. + * Perform a HTTP GET request. + * Can easily pass query parameters as the second parameter. * @param {String} url - * @param {Object} options - * @returns {Promise<{headers: Headers, original: Response, data: (Object|String), redirected: boolean, statusText: string, url: string, status: number}>} + * @param {Object} params + * @returns {Promise} */ -async function request(url, options = {}) { - if (!url.startsWith('http')) { - url = window.baseUrl(url); - } - - if (options.params) { - const urlObj = new URL(url); - for (let paramName of Object.keys(options.params)) { - const value = options.params[paramName]; - if (typeof value !== 'undefined' && value !== null) { - urlObj.searchParams.set(paramName, value); - } - } - url = urlObj.toString(); - } - - const csrfToken = document.querySelector('meta[name=token]').getAttribute('content'); - options = Object.assign({}, options, { - 'credentials': 'same-origin', - }); - options.headers = Object.assign({}, options.headers || {}, { - 'baseURL': window.baseUrl(''), - 'X-CSRF-TOKEN': csrfToken, +export async function get(url, params = {}) { + return request(url, { + method: 'GET', + params, }); +} - const response = await fetch(url, options); - const content = await getResponseContent(response); - const returnData = { - data: content, - headers: response.headers, - redirected: response.redirected, - status: response.status, - statusText: response.statusText, - url: response.url, - original: response, - }; +/** + * Perform a HTTP POST request. + * @param {String} url + * @param {Object} data + * @returns {Promise} + */ +export async function post(url, data = null) { + return dataRequest('POST', url, data); +} - if (!response.ok) { - throw returnData; - } +/** + * Perform a HTTP PUT request. + * @param {String} url + * @param {Object} data + * @returns {Promise} + */ +export async function put(url, data = null) { + return dataRequest('PUT', url, data); +} - return returnData; +/** + * Perform a HTTP PATCH request. + * @param {String} url + * @param {Object} data + * @returns {Promise} + */ +export async function patch(url, data = null) { + return dataRequest('PATCH', url, data); } /** - * Get the content from a fetch response. - * Checks the content-type header to determine the format. - * @param {Response} response - * @returns {Promise} + * Perform a HTTP DELETE request. + * @param {String} url + * @param {Object} data + * @returns {Promise} */ -async function getResponseContent(response) { - if (response.status === 204) { - return null; +async function performDelete(url, data = null) { + return dataRequest('DELETE', url, data); +} + +export {performDelete as delete}; + +/** + * Parse the response text for an error response to a user + * presentable string. Handles a range of errors responses including + * validation responses & server response text. + * @param {String} text + * @returns {String} + */ +export function formatErrorResponseText(text) { + const data = text.startsWith('{') ? JSON.parse(text) : {message: text}; + if (!data) { + return text; } - const responseContentType = response.headers.get('Content-Type'); - const subType = responseContentType.split('/').pop(); + if (data.message || data.error) { + return data.message || data.error; + } - if (subType === 'javascript' || subType === 'json') { - return await response.json(); + const values = Object.values(data); + const isValidation = values.every(val => { + return Array.isArray(val) || val.every(x => typeof x === 'string'); + }); + + if (isValidation) { + return values.flat().join(' '); } - return await response.text(); + return text; } - -export default { - get: get, - post: post, - put: put, - patch: patch, - delete: performDelete, -}; \ No newline at end of file