History

Use fetch helpers instead of fetch (#27026)

WIP because: - [x] Some calls set a `content-type` but send no body, can likely remove the header - [x] Need to check whether `charset=utf-8` has any significance on the webauthn calls, I assume not as it is the default for json content. - [x] Maybe `no-restricted-globals` is better for eslint, but will require a lot of duplication in the yaml or moving eslint config to a `.js` extension. - [x] Maybe export `request` as `fetch`, shadowing the global.

silverwind committed 3 years ago

Commit ae8e8f01 parent 809923817 files · +70 −98Browse files

Unified · Split

17 files
  1. .eslintrc.yaml +4 −1
  2. docs/content/contributing/guidelines-frontend.en-us.md +1 −1
  3. web_src/js/components/DashboardRepoList.vue +3 −2
  4. web_src/js/components/DiffCommitSelector.vue +2 −1
  5. web_src/js/components/RepoBranchTagSelector.vue +2 −2
  6. web_src/js/features/common-global.js +4 −3
  7. web_src/js/features/common-issue-list.js +3 −2
  8. web_src/js/features/comp/ImagePaste.js +2 −7
  9. web_src/js/features/comp/ReactionSelector.js +3 −11
  10. web_src/js/features/copycontent.js +2 −1
  11. web_src/js/features/install.js +2 −1
  12. web_src/js/features/pull-view-file.js +3 −6
  13. web_src/js/features/repo-diff-commit.js +2 −1
  14. web_src/js/features/repo-issue-list.js +3 −15
  15. web_src/js/features/repo-migrate.js +4 −9
  16. web_src/js/features/user-auth-webauthn.js +13 −25
  17. web_src/js/modules/fetch.js +17 −10
.eslintrc.yaml+4−1
@@ -46,6 +46,9 @@ overrides: · 45 unmodified lines
4646 - files: ["*.config.*"]
4747 rules:
4848 import/no-unused-modules: [0]
49+ - files: ["web_src/js/modules/fetch.js", "web_src/js/standalone/**/*"]
50+ rules:
51+ no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
4952
5053 rules:
5154 "@eslint-community/eslint-comments/disable-enable-pair": [2]
@@ -420,7 +423,7 @@ rules: · 368 unmodified lines
420423 no-restricted-exports: [0]
421424 no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, location, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, self, status, statusbar, stop, toolbar, top, __dirname, __filename]
422425 no-restricted-imports: [0]
423− no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
426+ no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.js instead"}]
424427 no-return-assign: [0]
425428 no-script-url: [2]
426429 no-self-assign: [2, {props: true}]
docs/content/contributing/guidelines-frontend.en-us.md+1−1
@@ -95,7 +95,7 @@ Some lint rules and IDEs also have warnings if the returned Promise is not handl · 94 unmodified lines
9595 ### Fetching data
9696
9797 To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
98−accept a `data` option for the content, will automatically set CSFR token and return a
98+accept a `data` option for the content, will automatically set CSRF token and return a
9999 Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
100100
101101 ### HTML Attributes and `dataset`
web_src/js/components/DashboardRepoList.vue+3−2
@@ -2,6 +2,7 @@ · 1 unmodified line
22 import {createApp, nextTick} from 'vue';
33 import $ from 'jquery';
44 import {SvgIcon} from '../svg.js';
5+import {GET} from '../modules/fetch.js';
56
67 const {appSubUrl, assetUrlPrefix, pageData} = window.config;
78
@@ -233,11 +234,11 @@ const sfc = { · 225 unmodified lines
233234 try {
234235 if (!this.reposTotalCount) {
235236 const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
236− response = await fetch(totalCountSearchURL);
237+ response = await GET(totalCountSearchURL);
237238 this.reposTotalCount = response.headers.get('X-Total-Count');
238239 }
239240
240− response = await fetch(searchedURL);
241+ response = await GET(searchedURL);
241242 json = await response.json();
242243 } catch {
243244 if (searchedURL === this.searchURL) {
web_src/js/components/DiffCommitSelector.vue+2−1
@@ -1,5 +1,6 @@
11 <script>
22 import {SvgIcon} from '../svg.js';
3+import {GET} from '../modules/fetch.js';
34
45 export default {
56 components: {SvgIcon},
@@ -123,7 +124,7 @@ export default { · 117 unmodified lines
123124 },
124125 /** Load the commits to show in this dropdown */
125126 async fetchCommits() {
126− const resp = await fetch(`${this.issueLink}/commits/list`);
127+ const resp = await GET(`${this.issueLink}/commits/list`);
127128 const results = await resp.json();
128129 this.commits.push(...results.commits.map((x) => {
129130 x.hovered = false;
web_src/js/components/RepoBranchTagSelector.vue+2−2
@@ -4,6 +4,7 @@ import $ from 'jquery'; · 3 unmodified lines
44 import {SvgIcon} from '../svg.js';
55 import {pathEscapeSegments} from '../utils/url.js';
66 import {showErrorToast} from '../modules/toast.js';
7+import {GET} from '../modules/fetch.js';
78
89 const sfc = {
910 components: {SvgIcon},
@@ -190,8 +191,7 @@ const sfc = { · 180 unmodified lines
190191 }
191192 this.isLoading = true;
192193 try {
193− const reqUrl = `${this.repoLink}/${this.mode}/list`;
194− const resp = await fetch(reqUrl);
194+ const resp = await GET(`${this.repoLink}/${this.mode}/list`);
195195 const {results} = await resp.json();
196196 for (const result of results) {
197197 let selected = false;
web_src/js/features/common-global.js+4−3
@@ -11,6 +11,7 @@ import {htmlEscape} from 'escape-goat'; · 10 unmodified lines
1111 import {showTemporaryTooltip} from '../modules/tippy.js';
1212 import {confirmModal} from './comp/ConfirmModal.js';
1313 import {showErrorToast} from '../modules/toast.js';
14+import {request} from '../modules/fetch.js';
1415
1516 const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
1617
@@ -81,7 +82,7 @@ function fetchActionDoRedirect(redirect) { · 64 unmodified lines
8182
8283 async function fetchActionDoRequest(actionElem, url, opt) {
8384 try {
84− const resp = await fetch(url, opt);
85+ const resp = await request(url, opt);
8586 if (resp.status === 200) {
8687 let {redirect} = await resp.json();
8788 redirect = redirect || actionElem.getAttribute('data-redirect');
@@ -127,7 +128,7 @@ async function formFetchAction(e) { · 39 unmodified lines
127128 }
128129
129130 let reqUrl = formActionUrl;
130− const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}};
131+ const reqOpt = {method: formMethod.toUpperCase()};
131132 if (formMethod.toLowerCase() === 'get') {
132133 const params = new URLSearchParams();
133134 for (const [key, value] of formData) {
@@ -264,7 +265,7 @@ async function linkAction(e) { · 130 unmodified lines
264265 const url = el.getAttribute('data-url');
265266 const doRequest = async () => {
266267 el.disabled = true;
267− await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}});
268+ await fetchActionDoRequest(el, url, {method: 'POST'});
268269 el.disabled = false;
269270 };
270271
web_src/js/features/common-issue-list.js+3−2
@@ -1,7 +1,8 @@
11 import $ from 'jquery';
22 import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js';
3−const {appSubUrl} = window.config;
3+import {GET} from '../modules/fetch.js';
44
5+const {appSubUrl} = window.config;
56 const reIssueIndex = /^(\d+)$/; // eg: "123"
67 const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
78 const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}"
@@ -54,7 +55,7 @@ export function initCommonIssueListQuickGoto() { · 46 unmodified lines
5455 // try to check whether the parsed goto link is valid
5556 let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
5657 if (targetUrl) {
57− const res = await fetch(`${targetUrl}/info`);
58+ const res = await GET(`${targetUrl}/info`);
5859 if (res.status !== 200) targetUrl = '';
5960 }
6061
web_src/js/features/comp/ImagePaste.js+2−7
@@ -1,16 +1,11 @@
11 import $ from 'jquery';
2−
3−const {csrfToken} = window.config;
2+import {POST} from '../../modules/fetch.js';
43
54 async function uploadFile(file, uploadUrl) {
65 const formData = new FormData();
76 formData.append('file', file, file.name);
87
9− const res = await fetch(uploadUrl, {
10− method: 'POST',
11− headers: {'X-Csrf-Token': csrfToken},
12− body: formData,
13− });
8+ const res = await POST(uploadUrl, {data: formData});
149 return await res.json();
1510 }
1611
web_src/js/features/comp/ReactionSelector.js+3−11
@@ -1,6 +1,5 @@
11 import $ from 'jquery';
2−
3−const {csrfToken} = window.config;
2+import {POST} from '../../modules/fetch.js';
43
54 export function initCompReactionSelector($parent) {
65 $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
@@ -12,15 +11,8 @@ export function initCompReactionSelector($parent) { · 5 unmodified lines
1211 const reactionContent = $(this).attr('data-reaction-content');
1312 const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';
1413
15− const res = await fetch(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
16− method: 'POST',
17− headers: {
18− 'content-type': 'application/x-www-form-urlencoded',
19− },
20− body: new URLSearchParams({
21− _csrf: csrfToken,
22− content: reactionContent,
23− }),
14+ const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
15+ data: new URLSearchParams({content: reactionContent}),
2416 });
2517
2618 const data = await res.json();
web_src/js/features/copycontent.js+2−1
@@ -1,6 +1,7 @@
11 import {clippie} from 'clippie';
22 import {showTemporaryTooltip} from '../modules/tippy.js';
33 import {convertImage} from '../utils.js';
4+import {GET} from '../modules/fetch.js';
45
56 const {i18n} = window.config;
67
@@ -20,7 +21,7 @@ export function initCopyContent() { · 13 unmodified lines
2021 if (link) {
2122 btn.classList.add('is-loading', 'small-loading-icon');
2223 try {
23− const res = await fetch(link, {credentials: 'include', redirect: 'follow'});
24+ const res = await GET(link, {credentials: 'include', redirect: 'follow'});
2425 const contentType = res.headers.get('content-type');
2526
2627 if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
web_src/js/features/install.js+2−1
@@ -1,5 +1,6 @@
11 import $ from 'jquery';
22 import {hideElem, showElem} from '../utils/dom.js';
3+import {GET} from '../modules/fetch.js';
34
45 export function initInstall() {
56 const $page = $('.page-content.install');
@@ -111,7 +112,7 @@ function initPostInstall() { · 105 unmodified lines
111112 const targetUrl = el.getAttribute('href');
112113 let tid = setInterval(async () => {
113114 try {
114− const resp = await fetch(targetUrl);
115+ const resp = await GET(targetUrl);
115116 if (tid && resp.status === 200) {
116117 clearInterval(tid);
117118 tid = null;
web_src/js/features/pull-view-file.js+3−6
@@ -1,7 +1,8 @@
11 import {diffTreeStore} from '../modules/stores.js';
22 import {setFileFolding} from './file-fold.js';
3+import {POST} from '../modules/fetch.js';
34
4−const {csrfToken, pageData} = window.config;
5+const {pageData} = window.config;
56 const prReview = pageData.prReview || {};
67 const viewedStyleClass = 'viewed-file-checked-form';
78 const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found
@@ -68,11 +69,7 @@ export function initViewedCheckboxListenerFor() { · 60 unmodified lines
6869 const data = {files};
6970 const headCommitSHA = form.getAttribute('data-headcommit');
7071 if (headCommitSHA) data.headCommitSHA = headCommitSHA;
71− fetch(form.getAttribute('data-link'), {
72− method: 'POST',
73− headers: {'X-Csrf-Token': csrfToken},
74− body: JSON.stringify(data),
75− });
72+ POST(form.getAttribute('data-link'), {data});
7673
7774 // Fold the file accordingly
7875 const parentBox = form.closest('.diff-file-header');
web_src/js/features/repo-diff-commit.js+2−1
@@ -1,9 +1,10 @@
11 import {hideElem, showElem, toggleElem} from '../utils/dom.js';
2+import {GET} from '../modules/fetch.js';
23
34 async function loadBranchesAndTags(area, loadingButton) {
45 loadingButton.classList.add('disabled');
56 try {
6− const res = await fetch(loadingButton.getAttribute('data-fetch-url'));
7+ const res = await GET(loadingButton.getAttribute('data-fetch-url'));
78 const data = await res.json();
89 hideElem(loadingButton);
910 addTags(area, data.tags);
web_src/js/features/repo-issue-list.js+3−15
@@ -5,6 +5,7 @@ import {htmlEscape} from 'escape-goat'; · 4 unmodified lines
55 import {confirmModal} from './comp/ConfirmModal.js';
66 import {showErrorToast} from '../modules/toast.js';
77 import {createSortable} from '../modules/sortable.js';
8+import {DELETE, POST} from '../modules/fetch.js';
89
910 function initRepoIssueListCheckboxes() {
1011 const $issueSelectAll = $('.issue-checkbox-all');
@@ -146,13 +147,7 @@ function initPinRemoveButton() { · 135 unmodified lines
146147 const id = Number(el.getAttribute('data-issue-id'));
147148
148149 // Send the unpin request
149− const response = await fetch(el.getAttribute('data-unpin-url'), {
150− method: 'delete',
151− headers: {
152− 'X-Csrf-Token': window.config.csrfToken,
153− 'Content-Type': 'application/json',
154− },
155− });
150+ const response = await DELETE(el.getAttribute('data-unpin-url'));
156151 if (response.ok) {
157152 // Delete the tooltip
158153 el._tippy.destroy();
@@ -166,14 +161,7 @@ function initPinRemoveButton() { · 7 unmodified lines
166161 async function pinMoveEnd(e) {
167162 const url = e.item.getAttribute('data-move-url');
168163 const id = Number(e.item.getAttribute('data-issue-id'));
169− await fetch(url, {
170− method: 'post',
171− body: JSON.stringify({id, position: e.newIndex + 1}),
172− headers: {
173− 'X-Csrf-Token': window.config.csrfToken,
174− 'Content-Type': 'application/json',
175− },
176− });
164+ await POST(url, {data: {id, position: e.newIndex + 1}});
177165 }
178166
179167 async function initIssuePinSort() {
web_src/js/features/repo-migrate.js+4−9
@@ -1,7 +1,8 @@
11 import $ from 'jquery';
22 import {hideElem, showElem} from '../utils/dom.js';
3+import {GET, POST} from '../modules/fetch.js';
34
4−const {appSubUrl, csrfToken} = window.config;
5+const {appSubUrl} = window.config;
56
67 export function initRepoMigrationStatusChecker() {
78 const $repoMigrating = $('#repo_migrating');
@@ -13,7 +14,7 @@ export function initRepoMigrationStatusChecker() { · 5 unmodified lines
1314
1415 // returns true if the refresh still need to be called after a while
1516 const refresh = async () => {
16− const res = await fetch(`${appSubUrl}/user/task/${task}`);
17+ const res = await GET(`${appSubUrl}/user/task/${task}`);
1718 if (res.status !== 200) return true; // continue to refresh if network error occurs
1819
1920 const data = await res.json();
@@ -58,12 +59,6 @@ export function initRepoMigrationStatusChecker() { · 38 unmodified lines
5859 }
5960
6061 async function doMigrationRetry(e) {
61− await fetch($(e.target).attr('data-migrating-task-retry-url'), {
62− method: 'post',
63− headers: {
64− 'X-Csrf-Token': csrfToken,
65− 'Content-Type': 'application/json',
66− },
67− });
62+ await POST($(e.target).attr('data-migrating-task-retry-url'));
6863 window.location.reload();
6964 }
web_src/js/features/user-auth-webauthn.js+13−25
@@ -1,7 +1,8 @@
11 import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
22 import {showElem} from '../utils/dom.js';
3+import {GET, POST} from '../modules/fetch.js';
34
4−const {appSubUrl, csrfToken} = window.config;
5+const {appSubUrl} = window.config;
56
67 export async function initUserAuthWebAuthn() {
78 const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
@@ -13,7 +14,7 @@ export async function initUserAuthWebAuthn() { · 5 unmodified lines
1314 return;
1415 }
1516
16− const res = await fetch(`${appSubUrl}/user/webauthn/assertion`);
17+ const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
1718 if (res.status !== 200) {
1819 webAuthnError('unknown');
1920 return;
@@ -53,12 +54,8 @@ async function verifyAssertion(assertedCredential) { · 33 unmodified lines
5354 const sig = new Uint8Array(assertedCredential.response.signature);
5455 const userHandle = new Uint8Array(assertedCredential.response.userHandle);
5556
56− const res = await fetch(`${appSubUrl}/user/webauthn/assertion`, {
57− method: 'POST',
58− headers: {
59− 'Content-Type': 'application/json; charset=utf-8'
60− },
61− body: JSON.stringify({
57+ const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
58+ data: {
6259 id: assertedCredential.id,
6360 rawId: encodeURLEncodedBase64(rawId),
6461 type: assertedCredential.type,
@@ -69,7 +66,7 @@ async function verifyAssertion(assertedCredential) { · 4 unmodified lines
6966 signature: encodeURLEncodedBase64(sig),
7067 userHandle: encodeURLEncodedBase64(userHandle),
7168 },
72− }),
69+ },
7370 });
7471 if (res.status === 500) {
7572 webAuthnError('unknown');
@@ -88,13 +85,8 @@ async function webauthnRegistered(newCredential) { · 12 unmodified lines
8885 const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
8986 const rawId = new Uint8Array(newCredential.rawId);
9087
91− const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/register`, {
92− method: 'POST',
93− headers: {
94− 'X-Csrf-Token': csrfToken,
95− 'Content-Type': 'application/json; charset=utf-8',
96− },
97− body: JSON.stringify({
88+ const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
89+ data: {
9890 id: newCredential.id,
9991 rawId: encodeURLEncodedBase64(rawId),
10092 type: newCredential.type,
@@ -102,7 +94,7 @@ async function webauthnRegistered(newCredential) { · 1 unmodified line
10294 attestationObject: encodeURLEncodedBase64(attestationObject),
10395 clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
10496 },
105− }),
97+ },
10698 });
10799
108100 if (res.status === 409) {
@@ -165,15 +157,11 @@ export function initUserAuthWebAuthnRegister() { · 56 unmodified lines
165157 async function webAuthnRegisterRequest() {
166158 const elNickname = document.getElementById('nickname');
167159
168− const body = new FormData();
169− body.append('name', elNickname.value);
160+ const formData = new FormData();
161+ formData.append('name', elNickname.value);
170162
171− const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
172− method: 'POST',
173− headers: {
174− 'X-Csrf-Token': csrfToken,
175− },
176− body,
163+ const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
164+ data: formData,
177165 });
178166
179167 if (res.status === 409) {
web_src/js/modules/fetch.js+17−10
@@ -2,17 +2,18 @@ import {isObject} from '../utils.js'; · 1 unmodified line
22
33 const {csrfToken} = window.config;
44
5+// safe HTTP methods that don't need a csrf token
6+const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
7+
58 // fetch wrapper, use below method name functions and the `data` option to pass in data
6−// which will automatically set an appropriate content-type header. For json content,
7−// only object and array types are currently supported.
8−function request(url, {headers, data, body, ...other} = {}) {
9+// which will automatically set an appropriate headers. For json content, only object
10+// and array types are currently supported.
11+export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
912 let contentType;
1013 if (!body) {
1114 if (data instanceof FormData) {
12− contentType = 'multipart/form-data';
1315 body = data;
1416 } else if (data instanceof URLSearchParams) {
15− contentType = 'application/x-www-form-urlencoded';
1617 body = data;
1718 } else if (isObject(data) || Array.isArray(data)) {
1819 contentType = 'application/json';
@@ -20,12 +21,18 @@ function request(url, {headers, data, body, ...other} = {}) { · 1 unmodified line
2021 }
2122 }
2223
24+ const headersMerged = new Headers({
25+ ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
26+ ...(contentType && {'content-type': contentType}),
27+ });
28+
29+ for (const [name, value] of Object.entries(headers)) {
30+ headersMerged.set(name, value);
31+ }
32+
2333 return fetch(url, {
24− headers: {
25− 'x-csrf-token': csrfToken,
26− ...(contentType && {'content-type': contentType}),
27− ...headers,
28− },
34+ method,
35+ headers: headersMerged,
2936 ...(body && {body}),
3037 ...other,
3138 });