]> BookStack Code Mirror - bookstack/blob - resources/js/services/http.js
Merge branch 'feature/mail-verify-peer' into development
[bookstack] / resources / js / services / http.js
1 /**
2  * @typedef FormattedResponse
3  * @property {Headers} headers
4  * @property {Response} original
5  * @property {Object|String} data
6  * @property {Boolean} redirected
7  * @property {Number} status
8  * @property {string} statusText
9  * @property {string} url
10  */
11
12 /**
13  * Get the content from a fetch response.
14  * Checks the content-type header to determine the format.
15  * @param {Response} response
16  * @returns {Promise<Object|String>}
17  */
18 async function getResponseContent(response) {
19     if (response.status === 204) {
20         return null;
21     }
22
23     const responseContentType = response.headers.get('Content-Type') || '';
24     const subType = responseContentType.split(';')[0].split('/').pop();
25
26     if (subType === 'javascript' || subType === 'json') {
27         return response.json();
28     }
29
30     return response.text();
31 }
32
33 export class HttpError extends Error {
34
35     constructor(response, content) {
36         super(response.statusText);
37         this.data = content;
38         this.headers = response.headers;
39         this.redirected = response.redirected;
40         this.status = response.status;
41         this.statusText = response.statusText;
42         this.url = response.url;
43         this.original = response;
44     }
45
46 }
47
48 /**
49  * Create a new HTTP request, setting the required CSRF information
50  * to communicate with the back-end. Parses & formats the response.
51  * @param {String} url
52  * @param {Object} options
53  * @returns {Promise<FormattedResponse>}
54  */
55 async function request(url, options = {}) {
56     let requestUrl = url;
57
58     if (!requestUrl.startsWith('http')) {
59         requestUrl = window.baseUrl(requestUrl);
60     }
61
62     if (options.params) {
63         const urlObj = new URL(requestUrl);
64         for (const paramName of Object.keys(options.params)) {
65             const value = options.params[paramName];
66             if (typeof value !== 'undefined' && value !== null) {
67                 urlObj.searchParams.set(paramName, value);
68             }
69         }
70         requestUrl = urlObj.toString();
71     }
72
73     const csrfToken = document.querySelector('meta[name=token]').getAttribute('content');
74     const requestOptions = {...options, credentials: 'same-origin'};
75     requestOptions.headers = {
76         ...requestOptions.headers || {},
77         baseURL: window.baseUrl(''),
78         'X-CSRF-TOKEN': csrfToken,
79     };
80
81     const response = await fetch(requestUrl, requestOptions);
82     const content = await getResponseContent(response);
83     const returnData = {
84         data: content,
85         headers: response.headers,
86         redirected: response.redirected,
87         status: response.status,
88         statusText: response.statusText,
89         url: response.url,
90         original: response,
91     };
92
93     if (!response.ok) {
94         throw new HttpError(response, content);
95     }
96
97     return returnData;
98 }
99
100 /**
101  * Perform a HTTP request to the back-end that includes data in the body.
102  * Parses the body to JSON if an object, setting the correct headers.
103  * @param {String} method
104  * @param {String} url
105  * @param {Object} data
106  * @returns {Promise<FormattedResponse>}
107  */
108 async function dataRequest(method, url, data = null) {
109     const options = {
110         method,
111         body: data,
112     };
113
114     // Send data as JSON if a plain object
115     if (typeof data === 'object' && !(data instanceof FormData)) {
116         options.headers = {
117             'Content-Type': 'application/json',
118             'X-Requested-With': 'XMLHttpRequest',
119         };
120         options.body = JSON.stringify(data);
121     }
122
123     // Ensure FormData instances are sent over POST
124     // Since Laravel does not read multipart/form-data from other types
125     // of request. Hence the addition of the magic _method value.
126     if (data instanceof FormData && method !== 'post') {
127         data.append('_method', method);
128         options.method = 'post';
129     }
130
131     return request(url, options);
132 }
133
134 /**
135  * Perform a HTTP GET request.
136  * Can easily pass query parameters as the second parameter.
137  * @param {String} url
138  * @param {Object} params
139  * @returns {Promise<FormattedResponse>}
140  */
141 export async function get(url, params = {}) {
142     return request(url, {
143         method: 'GET',
144         params,
145     });
146 }
147
148 /**
149  * Perform a HTTP POST request.
150  * @param {String} url
151  * @param {Object} data
152  * @returns {Promise<FormattedResponse>}
153  */
154 export async function post(url, data = null) {
155     return dataRequest('POST', url, data);
156 }
157
158 /**
159  * Perform a HTTP PUT request.
160  * @param {String} url
161  * @param {Object} data
162  * @returns {Promise<FormattedResponse>}
163  */
164 export async function put(url, data = null) {
165     return dataRequest('PUT', url, data);
166 }
167
168 /**
169  * Perform a HTTP PATCH request.
170  * @param {String} url
171  * @param {Object} data
172  * @returns {Promise<FormattedResponse>}
173  */
174 export async function patch(url, data = null) {
175     return dataRequest('PATCH', url, data);
176 }
177
178 /**
179  * Perform a HTTP DELETE request.
180  * @param {String} url
181  * @param {Object} data
182  * @returns {Promise<FormattedResponse>}
183  */
184 async function performDelete(url, data = null) {
185     return dataRequest('DELETE', url, data);
186 }
187
188 export {performDelete as delete};