diff options
author | silverwind <me@silverwind.io> | 2023-09-19 02:50:30 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-19 00:50:30 +0000 |
commit | ae8e8f055e9edfe258e641df8752a070ffdd6823 (patch) | |
tree | 1181f5dec6a7e9cfff020aa08d073582cfcc5e43 | |
parent | 8099238618f6573f1eb5cfeeb0902b641e7121ab (diff) | |
download | gitea-ae8e8f055e9edfe258e641df8752a070ffdd6823.tar.gz gitea-ae8e8f055e9edfe258e641df8752a070ffdd6823.zip |
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.
-rw-r--r-- | .eslintrc.yaml | 5 | ||||
-rw-r--r-- | docs/content/contributing/guidelines-frontend.en-us.md | 2 | ||||
-rw-r--r-- | web_src/js/components/DashboardRepoList.vue | 5 | ||||
-rw-r--r-- | web_src/js/components/DiffCommitSelector.vue | 3 | ||||
-rw-r--r-- | web_src/js/components/RepoBranchTagSelector.vue | 4 | ||||
-rw-r--r-- | web_src/js/features/common-global.js | 7 | ||||
-rw-r--r-- | web_src/js/features/common-issue-list.js | 5 | ||||
-rw-r--r-- | web_src/js/features/comp/ImagePaste.js | 9 | ||||
-rw-r--r-- | web_src/js/features/comp/ReactionSelector.js | 14 | ||||
-rw-r--r-- | web_src/js/features/copycontent.js | 3 | ||||
-rw-r--r-- | web_src/js/features/install.js | 3 | ||||
-rw-r--r-- | web_src/js/features/pull-view-file.js | 9 | ||||
-rw-r--r-- | web_src/js/features/repo-diff-commit.js | 3 | ||||
-rw-r--r-- | web_src/js/features/repo-issue-list.js | 18 | ||||
-rw-r--r-- | web_src/js/features/repo-migrate.js | 13 | ||||
-rw-r--r-- | web_src/js/features/user-auth-webauthn.js | 38 | ||||
-rw-r--r-- | web_src/js/modules/fetch.js | 27 |
17 files changed, 70 insertions, 98 deletions
diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 846823abc7..c9ea481af4 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -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}] diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index 921c2b0233..0d9e510e70 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -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` diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 5b8075f07a..5ff51168cb 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -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) { diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 48dc9d72ff..3f7100d201 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -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; diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 30bff6d23f..bc7d979d99 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -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; diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 96fad7eee6..bc775ae545 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -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; }; diff --git a/web_src/js/features/common-issue-list.js b/web_src/js/features/common-issue-list.js index ecada11988..3a28cf900c 100644 --- a/web_src/js/features/common-issue-list.js +++ b/web_src/js/features/common-issue-list.js @@ -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 = ''; } diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index dc335495a3..cae42f3d5f 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -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(); } diff --git a/web_src/js/features/comp/ReactionSelector.js b/web_src/js/features/comp/ReactionSelector.js index 336634a582..76834f8844 100644 --- a/web_src/js/features/comp/ReactionSelector.js +++ b/web_src/js/features/comp/ReactionSelector.js @@ -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(); diff --git a/web_src/js/features/copycontent.js b/web_src/js/features/copycontent.js index c1419a524b..3d3b2a697e 100644 --- a/web_src/js/features/copycontent.js +++ b/web_src/js/features/copycontent.js @@ -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')) { diff --git a/web_src/js/features/install.js b/web_src/js/features/install.js index 23122ca4c3..9fda7f7d27 100644 --- a/web_src/js/features/install.js +++ b/web_src/js/features/install.js @@ -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; diff --git a/web_src/js/features/pull-view-file.js b/web_src/js/features/pull-view-file.js index 90ea805160..86b65f68cf 100644 --- a/web_src/js/features/pull-view-file.js +++ b/web_src/js/features/pull-view-file.js @@ -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'); diff --git a/web_src/js/features/repo-diff-commit.js b/web_src/js/features/repo-diff-commit.js index bc591fa37d..3d4f0f677a 100644 --- a/web_src/js/features/repo-diff-commit.js +++ b/web_src/js/features/repo-diff-commit.js @@ -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); diff --git a/web_src/js/features/repo-issue-list.js b/web_src/js/features/repo-issue-list.js index 64343a8d22..af4586121e 100644 --- a/web_src/js/features/repo-issue-list.js +++ b/web_src/js/features/repo-issue-list.js @@ -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() { diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js index de9f7b023c..cae28fdd1b 100644 --- a/web_src/js/features/repo-migrate.js +++ b/web_src/js/features/repo-migrate.js @@ -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(); } diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index c4c2356cb3..363e039760 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -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) { diff --git a/web_src/js/modules/fetch.js b/web_src/js/modules/fetch.js index 63732206c6..9fccf4a81e 100644 --- a/web_src/js/modules/fetch.js +++ b/web_src/js/modules/fetch.js @@ -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, }); |