]> BookStack Code Mirror - bookstack/blobdiff - resources/js/services/http.js
Update sponsor image URLs in readme
[bookstack] / resources / js / services / http.js
index b05dd23bfd7222834eade6395f655d5916b1b2c1..d95e4a59a29b1a1bdc2ffc508f145803b469fcf6 100644 (file)
-
 /**
- * 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<Object|String>}
  */
-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<FormattedResponse>}
  */
-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<FormattedResponse>}
  */
 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<FormattedResponse>}
  */
-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<FormattedResponse>}
+ */
+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<FormattedResponse>}
+ */
+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<FormattedResponse>}
+ */
+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<Object|String>}
+ * Perform a HTTP DELETE request.
+ * @param {String} url
+ * @param {Object} data
+ * @returns {Promise<FormattedResponse>}
  */
-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