]> source.dussan.org Git - gitea.git/commitdiff
Use fetch helpers instead of fetch (#27026)
authorsilverwind <me@silverwind.io>
Tue, 19 Sep 2023 00:50:30 +0000 (02:50 +0200)
committerGitHub <noreply@github.com>
Tue, 19 Sep 2023 00:50:30 +0000 (00:50 +0000)
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.

17 files changed:
.eslintrc.yaml
docs/content/contributing/guidelines-frontend.en-us.md
web_src/js/components/DashboardRepoList.vue
web_src/js/components/DiffCommitSelector.vue
web_src/js/components/RepoBranchTagSelector.vue
web_src/js/features/common-global.js
web_src/js/features/common-issue-list.js
web_src/js/features/comp/ImagePaste.js
web_src/js/features/comp/ReactionSelector.js
web_src/js/features/copycontent.js
web_src/js/features/install.js
web_src/js/features/pull-view-file.js
web_src/js/features/repo-diff-commit.js
web_src/js/features/repo-issue-list.js
web_src/js/features/repo-migrate.js
web_src/js/features/user-auth-webauthn.js
web_src/js/modules/fetch.js

index 846823abc77447eb0b7f6c7f2c5006a16564fe0c..c9ea481af4bffd2df2d16faadb54e517f360349b 100644 (file)
@@ -46,6 +46,9 @@ overrides:
   - files: ["*.config.*"]
     rules:
       import/no-unused-modules: [0]
+  - files: ["web_src/js/modules/fetch.js", "web_src/js/standalone/**/*"]
+    rules:
+      no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
 
 rules:
   "@eslint-community/eslint-comments/disable-enable-pair": [2]
@@ -420,7 +423,7 @@ rules:
   no-restricted-exports: [0]
   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]
   no-restricted-imports: [0]
-  no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression]
+  no-restricted-syntax: [2, WithStatement, ForInStatement, LabeledStatement, SequenceExpression, {selector: "CallExpression[callee.name='fetch']", message: "use modules/fetch.js instead"}]
   no-return-assign: [0]
   no-script-url: [2]
   no-self-assign: [2, {props: true}]
index 921c2b0233690ef7da9d79614308a4745a58f33e..0d9e510e70039d46d81de3e6dd96f26d8219ba93 100644 (file)
@@ -95,7 +95,7 @@ Some lint rules and IDEs also have warnings if the returned Promise is not handl
 ### Fetching data
 
 To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They
-accept a `data` option for the content, will automatically set CSFR token and return a
+accept a `data` option for the content, will automatically set CSRF token and return a
 Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response).
 
 ### HTML Attributes and `dataset`
index 5b8075f07a06743a16ae3312204dd7f8e8f62d11..5ff51168cbd9fdb28cdeef858eda17cb9f13e367 100644 (file)
@@ -2,6 +2,7 @@
 import {createApp, nextTick} from 'vue';
 import $ from 'jquery';
 import {SvgIcon} from '../svg.js';
+import {GET} from '../modules/fetch.js';
 
 const {appSubUrl, assetUrlPrefix, pageData} = window.config;
 
@@ -233,11 +234,11 @@ const sfc = {
       try {
         if (!this.reposTotalCount) {
           const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
-          response = await fetch(totalCountSearchURL);
+          response = await GET(totalCountSearchURL);
           this.reposTotalCount = response.headers.get('X-Total-Count');
         }
 
-        response = await fetch(searchedURL);
+        response = await GET(searchedURL);
         json = await response.json();
       } catch {
         if (searchedURL === this.searchURL) {
index 48dc9d72ff4aface1f730971b27358a7396a6e29..3f7100d201db4fc20e2675789640dc8e48617113 100644 (file)
@@ -1,5 +1,6 @@
 <script>
 import {SvgIcon} from '../svg.js';
+import {GET} from '../modules/fetch.js';
 
 export default {
   components: {SvgIcon},
@@ -123,7 +124,7 @@ export default {
     },
     /** Load the commits to show in this dropdown */
     async fetchCommits() {
-      const resp = await fetch(`${this.issueLink}/commits/list`);
+      const resp = await GET(`${this.issueLink}/commits/list`);
       const results = await resp.json();
       this.commits.push(...results.commits.map((x) => {
         x.hovered = false;
index 30bff6d23f1ce08ee6c2fa5e929f43edb1261d03..bc7d979d9987da71db7396fc5ddf34d98d0a1617 100644 (file)
@@ -4,6 +4,7 @@ import $ from 'jquery';
 import {SvgIcon} from '../svg.js';
 import {pathEscapeSegments} from '../utils/url.js';
 import {showErrorToast} from '../modules/toast.js';
+import {GET} from '../modules/fetch.js';
 
 const sfc = {
   components: {SvgIcon},
@@ -190,8 +191,7 @@ const sfc = {
       }
       this.isLoading = true;
       try {
-        const reqUrl = `${this.repoLink}/${this.mode}/list`;
-        const resp = await fetch(reqUrl);
+        const resp = await GET(`${this.repoLink}/${this.mode}/list`);
         const {results} = await resp.json();
         for (const result of results) {
           let selected = false;
index 96fad7eee6c708f258d8d5d5eede489ad8f23b9e..bc775ae545296f5b482f47312c95e98221d6bd6a 100644 (file)
@@ -11,6 +11,7 @@ import {htmlEscape} from 'escape-goat';
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
+import {request} from '../modules/fetch.js';
 
 const {appUrl, appSubUrl, csrfToken, i18n} = window.config;
 
@@ -81,7 +82,7 @@ function fetchActionDoRedirect(redirect) {
 
 async function fetchActionDoRequest(actionElem, url, opt) {
   try {
-    const resp = await fetch(url, opt);
+    const resp = await request(url, opt);
     if (resp.status === 200) {
       let {redirect} = await resp.json();
       redirect = redirect || actionElem.getAttribute('data-redirect');
@@ -127,7 +128,7 @@ async function formFetchAction(e) {
   }
 
   let reqUrl = formActionUrl;
-  const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}};
+  const reqOpt = {method: formMethod.toUpperCase()};
   if (formMethod.toLowerCase() === 'get') {
     const params = new URLSearchParams();
     for (const [key, value] of formData) {
@@ -264,7 +265,7 @@ async function linkAction(e) {
   const url = el.getAttribute('data-url');
   const doRequest = async () => {
     el.disabled = true;
-    await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}});
+    await fetchActionDoRequest(el, url, {method: 'POST'});
     el.disabled = false;
   };
 
index ecada11988cf5c66080f5adee67cd91a0e191e8c..3a28cf900ca5c4734beda9d13c9b682ba1f2c1bd 100644 (file)
@@ -1,7 +1,8 @@
 import $ from 'jquery';
 import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js';
-const {appSubUrl} = window.config;
+import {GET} from '../modules/fetch.js';
 
+const {appSubUrl} = window.config;
 const reIssueIndex = /^(\d+)$/; // eg: "123"
 const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
 const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/;  // eg: "{owner}/{repo}#{index}"
@@ -54,7 +55,7 @@ export function initCommonIssueListQuickGoto() {
     // try to check whether the parsed goto link is valid
     let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText);
     if (targetUrl) {
-      const res = await fetch(`${targetUrl}/info`);
+      const res = await GET(`${targetUrl}/info`);
       if (res.status !== 200) targetUrl = '';
     }
 
index dc335495a310f371be0d1923dc7393dcff0eb0e9..cae42f3d5f7c756854c05f731c2dc04eea7ef3d7 100644 (file)
@@ -1,16 +1,11 @@
 import $ from 'jquery';
-
-const {csrfToken} = window.config;
+import {POST} from '../../modules/fetch.js';
 
 async function uploadFile(file, uploadUrl) {
   const formData = new FormData();
   formData.append('file', file, file.name);
 
-  const res = await fetch(uploadUrl, {
-    method: 'POST',
-    headers: {'X-Csrf-Token': csrfToken},
-    body: formData,
-  });
+  const res = await POST(uploadUrl, {data: formData});
   return await res.json();
 }
 
index 336634a582fe29aebb3e925c926763c2a49a1b5b..76834f88440763bff57566745bf6b071bb01b970 100644 (file)
@@ -1,6 +1,5 @@
 import $ from 'jquery';
-
-const {csrfToken} = window.config;
+import {POST} from '../../modules/fetch.js';
 
 export function initCompReactionSelector($parent) {
   $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) {
@@ -12,15 +11,8 @@ export function initCompReactionSelector($parent) {
     const reactionContent = $(this).attr('data-reaction-content');
     const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true';
 
-    const res = await fetch(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
-      method: 'POST',
-      headers: {
-        'content-type': 'application/x-www-form-urlencoded',
-      },
-      body: new URLSearchParams({
-        _csrf: csrfToken,
-        content: reactionContent,
-      }),
+    const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, {
+      data: new URLSearchParams({content: reactionContent}),
     });
 
     const data = await res.json();
index c1419a524bb5b3293415aff6c7126eaa07fcc362..3d3b2a697ecbf9b46b41d1554a42421bc3622fe2 100644 (file)
@@ -1,6 +1,7 @@
 import {clippie} from 'clippie';
 import {showTemporaryTooltip} from '../modules/tippy.js';
 import {convertImage} from '../utils.js';
+import {GET} from '../modules/fetch.js';
 
 const {i18n} = window.config;
 
@@ -20,7 +21,7 @@ export function initCopyContent() {
     if (link) {
       btn.classList.add('is-loading', 'small-loading-icon');
       try {
-        const res = await fetch(link, {credentials: 'include', redirect: 'follow'});
+        const res = await GET(link, {credentials: 'include', redirect: 'follow'});
         const contentType = res.headers.get('content-type');
 
         if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
index 23122ca4c3834671f4ce69985c6fe8269f189d8e..9fda7f7d27f52fef4f50fe36518c3f972565e0eb 100644 (file)
@@ -1,5 +1,6 @@
 import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
 
 export function initInstall() {
   const $page = $('.page-content.install');
@@ -111,7 +112,7 @@ function initPostInstall() {
   const targetUrl = el.getAttribute('href');
   let tid = setInterval(async () => {
     try {
-      const resp = await fetch(targetUrl);
+      const resp = await GET(targetUrl);
       if (tid && resp.status === 200) {
         clearInterval(tid);
         tid = null;
index 90ea805160781b00a8cef2745eac2a9e44357087..86b65f68cfb7666cde4724ffcc0072e80d63b865 100644 (file)
@@ -1,7 +1,8 @@
 import {diffTreeStore} from '../modules/stores.js';
 import {setFileFolding} from './file-fold.js';
+import {POST} from '../modules/fetch.js';
 
-const {csrfToken, pageData} = window.config;
+const {pageData} = window.config;
 const prReview = pageData.prReview || {};
 const viewedStyleClass = 'viewed-file-checked-form';
 const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found
@@ -68,11 +69,7 @@ export function initViewedCheckboxListenerFor() {
       const data = {files};
       const headCommitSHA = form.getAttribute('data-headcommit');
       if (headCommitSHA) data.headCommitSHA = headCommitSHA;
-      fetch(form.getAttribute('data-link'), {
-        method: 'POST',
-        headers: {'X-Csrf-Token': csrfToken},
-        body: JSON.stringify(data),
-      });
+      POST(form.getAttribute('data-link'), {data});
 
       // Fold the file accordingly
       const parentBox = form.closest('.diff-file-header');
index bc591fa37d5f034deb8aec9c9c7c5d40bcb386d6..3d4f0f677ae15e8a4a2e2d88fbe3b98b09ed30d4 100644 (file)
@@ -1,9 +1,10 @@
 import {hideElem, showElem, toggleElem} from '../utils/dom.js';
+import {GET} from '../modules/fetch.js';
 
 async function loadBranchesAndTags(area, loadingButton) {
   loadingButton.classList.add('disabled');
   try {
-    const res = await fetch(loadingButton.getAttribute('data-fetch-url'));
+    const res = await GET(loadingButton.getAttribute('data-fetch-url'));
     const data = await res.json();
     hideElem(loadingButton);
     addTags(area, data.tags);
index 64343a8d22abac90afee7318743164f93b3fce01..af4586121edc40dc0263a384dbd8d4c27a6a7390 100644 (file)
@@ -5,6 +5,7 @@ import {htmlEscape} from 'escape-goat';
 import {confirmModal} from './comp/ConfirmModal.js';
 import {showErrorToast} from '../modules/toast.js';
 import {createSortable} from '../modules/sortable.js';
+import {DELETE, POST} from '../modules/fetch.js';
 
 function initRepoIssueListCheckboxes() {
   const $issueSelectAll = $('.issue-checkbox-all');
@@ -146,13 +147,7 @@ function initPinRemoveButton() {
       const id = Number(el.getAttribute('data-issue-id'));
 
       // Send the unpin request
-      const response = await fetch(el.getAttribute('data-unpin-url'), {
-        method: 'delete',
-        headers: {
-          'X-Csrf-Token': window.config.csrfToken,
-          'Content-Type': 'application/json',
-        },
-      });
+      const response = await DELETE(el.getAttribute('data-unpin-url'));
       if (response.ok) {
         // Delete the tooltip
         el._tippy.destroy();
@@ -166,14 +161,7 @@ function initPinRemoveButton() {
 async function pinMoveEnd(e) {
   const url = e.item.getAttribute('data-move-url');
   const id = Number(e.item.getAttribute('data-issue-id'));
-  await fetch(url, {
-    method: 'post',
-    body: JSON.stringify({id, position: e.newIndex + 1}),
-    headers: {
-      'X-Csrf-Token': window.config.csrfToken,
-      'Content-Type': 'application/json',
-    },
-  });
+  await POST(url, {data: {id, position: e.newIndex + 1}});
 }
 
 async function initIssuePinSort() {
index de9f7b023c1aab69a2668a395fff8d3b4aad0873..cae28fdd1b1fc472334276b696f63cb904208e43 100644 (file)
@@ -1,7 +1,8 @@
 import $ from 'jquery';
 import {hideElem, showElem} from '../utils/dom.js';
+import {GET, POST} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export function initRepoMigrationStatusChecker() {
   const $repoMigrating = $('#repo_migrating');
@@ -13,7 +14,7 @@ export function initRepoMigrationStatusChecker() {
 
   // returns true if the refresh still need to be called after a while
   const refresh = async () => {
-    const res = await fetch(`${appSubUrl}/user/task/${task}`);
+    const res = await GET(`${appSubUrl}/user/task/${task}`);
     if (res.status !== 200) return true; // continue to refresh if network error occurs
 
     const data = await res.json();
@@ -58,12 +59,6 @@ export function initRepoMigrationStatusChecker() {
 }
 
 async function doMigrationRetry(e) {
-  await fetch($(e.target).attr('data-migrating-task-retry-url'), {
-    method: 'post',
-    headers: {
-      'X-Csrf-Token': csrfToken,
-      'Content-Type': 'application/json',
-    },
-  });
+  await POST($(e.target).attr('data-migrating-task-retry-url'));
   window.location.reload();
 }
index c4c2356cb3727746798223246dfead9ac8d84092..363e039760225c53e62d969999aaf4cdf7ad6aec 100644 (file)
@@ -1,7 +1,8 @@
 import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
 import {showElem} from '../utils/dom.js';
+import {GET, POST} from '../modules/fetch.js';
 
-const {appSubUrl, csrfToken} = window.config;
+const {appSubUrl} = window.config;
 
 export async function initUserAuthWebAuthn() {
   const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
@@ -13,7 +14,7 @@ export async function initUserAuthWebAuthn() {
     return;
   }
 
-  const res = await fetch(`${appSubUrl}/user/webauthn/assertion`);
+  const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
   if (res.status !== 200) {
     webAuthnError('unknown');
     return;
@@ -53,12 +54,8 @@ async function verifyAssertion(assertedCredential) {
   const sig = new Uint8Array(assertedCredential.response.signature);
   const userHandle = new Uint8Array(assertedCredential.response.userHandle);
 
-  const res = await fetch(`${appSubUrl}/user/webauthn/assertion`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json; charset=utf-8'
-    },
-    body: JSON.stringify({
+  const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
+    data: {
       id: assertedCredential.id,
       rawId: encodeURLEncodedBase64(rawId),
       type: assertedCredential.type,
@@ -69,7 +66,7 @@ async function verifyAssertion(assertedCredential) {
         signature: encodeURLEncodedBase64(sig),
         userHandle: encodeURLEncodedBase64(userHandle),
       },
-    }),
+    },
   });
   if (res.status === 500) {
     webAuthnError('unknown');
@@ -88,13 +85,8 @@ async function webauthnRegistered(newCredential) {
   const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
   const rawId = new Uint8Array(newCredential.rawId);
 
-  const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/register`, {
-    method: 'POST',
-    headers: {
-      'X-Csrf-Token': csrfToken,
-      'Content-Type': 'application/json; charset=utf-8',
-    },
-    body: JSON.stringify({
+  const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
+    data: {
       id: newCredential.id,
       rawId: encodeURLEncodedBase64(rawId),
       type: newCredential.type,
@@ -102,7 +94,7 @@ async function webauthnRegistered(newCredential) {
         attestationObject: encodeURLEncodedBase64(attestationObject),
         clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
       },
-    }),
+    },
   });
 
   if (res.status === 409) {
@@ -165,15 +157,11 @@ export function initUserAuthWebAuthnRegister() {
 async function webAuthnRegisterRequest() {
   const elNickname = document.getElementById('nickname');
 
-  const body = new FormData();
-  body.append('name', elNickname.value);
+  const formData = new FormData();
+  formData.append('name', elNickname.value);
 
-  const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
-    method: 'POST',
-    headers: {
-      'X-Csrf-Token': csrfToken,
-    },
-    body,
+  const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
+    data: formData,
   });
 
   if (res.status === 409) {
index 63732206c6b62911d7bc62bc4c44662f4e781b94..9fccf4a81ef5d96beb995788a7f5f39bd51c0256 100644 (file)
@@ -2,17 +2,18 @@ import {isObject} from '../utils.js';
 
 const {csrfToken} = window.config;
 
+// safe HTTP methods that don't need a csrf token
+const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
+
 // fetch wrapper, use below method name functions and the `data` option to pass in data
-// which will automatically set an appropriate content-type header. For json content,
-// only object and array types are currently supported.
-function request(url, {headers, data, body, ...other} = {}) {
+// which will automatically set an appropriate headers. For json content, only object
+// and array types are currently supported.
+export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
   let contentType;
   if (!body) {
     if (data instanceof FormData) {
-      contentType = 'multipart/form-data';
       body = data;
     } else if (data instanceof URLSearchParams) {
-      contentType = 'application/x-www-form-urlencoded';
       body = data;
     } else if (isObject(data) || Array.isArray(data)) {
       contentType = 'application/json';
@@ -20,12 +21,18 @@ function request(url, {headers, data, body, ...other} = {}) {
     }
   }
 
+  const headersMerged = new Headers({
+    ...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
+    ...(contentType && {'content-type': contentType}),
+  });
+
+  for (const [name, value] of Object.entries(headers)) {
+    headersMerged.set(name, value);
+  }
+
   return fetch(url, {
-    headers: {
-      'x-csrf-token': csrfToken,
-      ...(contentType && {'content-type': contentType}),
-      ...headers,
-    },
+    method,
+    headers: headersMerged,
     ...(body && {body}),
     ...other,
   });