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.
Unified · Split
| @@ -46,6 +46,9 @@ overrides: · 45 unmodified lines | |||
| 46 | - files: ["*.config.*"] | 46 | - files: ["*.config.*"] |
| 47 | rules: | 47 | rules: |
| 48 | import/no-unused-modules: [0] | 48 | 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] | ||
| 49 | 52 | ||
| 50 | rules: | 53 | rules: |
| 51 | "@eslint-community/eslint-comments/disable-enable-pair": [2] | 54 | "@eslint-community/eslint-comments/disable-enable-pair": [2] |
| @@ -420,7 +423,7 @@ rules: · 368 unmodified lines | |||
| 420 | no-restricted-exports: [0] | 423 | no-restricted-exports: [0] |
| 421 | 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] | 424 | 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] |
| 422 | no-restricted-imports: [0] | 425 | 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"}] |
| 424 | no-return-assign: [0] | 427 | no-return-assign: [0] |
| 425 | no-script-url: [2] | 428 | no-script-url: [2] |
| 426 | no-self-assign: [2, {props: true}] | 429 | no-self-assign: [2, {props: true}] |
| @@ -95,7 +95,7 @@ Some lint rules and IDEs also have warnings if the returned Promise is not handl · 94 unmodified lines | |||
| 95 | ### Fetching data | 95 | ### Fetching data |
| 96 | 96 | ||
| 97 | To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They | 97 | 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 |
| 99 | Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). | 99 | Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). |
| 100 | 100 | ||
| 101 | ### HTML Attributes and `dataset` | 101 | ### HTML Attributes and `dataset` |
| @@ -2,6 +2,7 @@ · 1 unmodified line | |||
| 2 | import {createApp, nextTick} from 'vue'; | 2 | import {createApp, nextTick} from 'vue'; |
| 3 | import $ from 'jquery'; | 3 | import $ from 'jquery'; |
| 4 | import {SvgIcon} from '../svg.js'; | 4 | import {SvgIcon} from '../svg.js'; |
| 5 | import {GET} from '../modules/fetch.js'; | ||
| 5 | 6 | ||
| 6 | const {appSubUrl, assetUrlPrefix, pageData} = window.config; | 7 | const {appSubUrl, assetUrlPrefix, pageData} = window.config; |
| 7 | 8 | ||
| @@ -233,11 +234,11 @@ const sfc = { · 225 unmodified lines | |||
| 233 | try { | 234 | try { |
| 234 | if (!this.reposTotalCount) { | 235 | if (!this.reposTotalCount) { |
| 235 | const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | 236 | 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); |
| 237 | this.reposTotalCount = response.headers.get('X-Total-Count'); | 238 | this.reposTotalCount = response.headers.get('X-Total-Count'); |
| 238 | } | 239 | } |
| 239 | 240 | ||
| 240 | response = await fetch(searchedURL); | 241 | response = await GET(searchedURL); |
| 241 | json = await response.json(); | 242 | json = await response.json(); |
| 242 | } catch { | 243 | } catch { |
| 243 | if (searchedURL === this.searchURL) { | 244 | if (searchedURL === this.searchURL) { |
| @@ -1,5 +1,6 @@ | |||
| 1 | <script> | 1 | <script> |
| 2 | import {SvgIcon} from '../svg.js'; | 2 | import {SvgIcon} from '../svg.js'; |
| 3 | import {GET} from '../modules/fetch.js'; | ||
| 3 | 4 | ||
| 4 | export default { | 5 | export default { |
| 5 | components: {SvgIcon}, | 6 | components: {SvgIcon}, |
| @@ -123,7 +124,7 @@ export default { · 117 unmodified lines | |||
| 123 | }, | 124 | }, |
| 124 | /** Load the commits to show in this dropdown */ | 125 | /** Load the commits to show in this dropdown */ |
| 125 | async fetchCommits() { | 126 | async fetchCommits() { |
| 126 | const resp = await fetch(`${this.issueLink}/commits/list`); | 127 | const resp = await GET(`${this.issueLink}/commits/list`); |
| 127 | const results = await resp.json(); | 128 | const results = await resp.json(); |
| 128 | this.commits.push(...results.commits.map((x) => { | 129 | this.commits.push(...results.commits.map((x) => { |
| 129 | x.hovered = false; | 130 | x.hovered = false; |
| @@ -4,6 +4,7 @@ import $ from 'jquery'; · 3 unmodified lines | |||
| 4 | import {SvgIcon} from '../svg.js'; | 4 | import {SvgIcon} from '../svg.js'; |
| 5 | import {pathEscapeSegments} from '../utils/url.js'; | 5 | import {pathEscapeSegments} from '../utils/url.js'; |
| 6 | import {showErrorToast} from '../modules/toast.js'; | 6 | import {showErrorToast} from '../modules/toast.js'; |
| 7 | import {GET} from '../modules/fetch.js'; | ||
| 7 | 8 | ||
| 8 | const sfc = { | 9 | const sfc = { |
| 9 | components: {SvgIcon}, | 10 | components: {SvgIcon}, |
| @@ -190,8 +191,7 @@ const sfc = { · 180 unmodified lines | |||
| 190 | } | 191 | } |
| 191 | this.isLoading = true; | 192 | this.isLoading = true; |
| 192 | try { | 193 | try { |
| 193 | const reqUrl = `${this.repoLink}/${this.mode}/list`; | 194 | const resp = await GET(`${this.repoLink}/${this.mode}/list`); |
| 194 | const resp = await fetch(reqUrl); | ||
| 195 | const {results} = await resp.json(); | 195 | const {results} = await resp.json(); |
| 196 | for (const result of results) { | 196 | for (const result of results) { |
| 197 | let selected = false; | 197 | let selected = false; |
| @@ -11,6 +11,7 @@ import {htmlEscape} from 'escape-goat'; · 10 unmodified lines | |||
| 11 | import {showTemporaryTooltip} from '../modules/tippy.js'; | 11 | import {showTemporaryTooltip} from '../modules/tippy.js'; |
| 12 | import {confirmModal} from './comp/ConfirmModal.js'; | 12 | import {confirmModal} from './comp/ConfirmModal.js'; |
| 13 | import {showErrorToast} from '../modules/toast.js'; | 13 | import {showErrorToast} from '../modules/toast.js'; |
| 14 | import {request} from '../modules/fetch.js'; | ||
| 14 | 15 | ||
| 15 | const {appUrl, appSubUrl, csrfToken, i18n} = window.config; | 16 | const {appUrl, appSubUrl, csrfToken, i18n} = window.config; |
| 16 | 17 | ||
| @@ -81,7 +82,7 @@ function fetchActionDoRedirect(redirect) { · 64 unmodified lines | |||
| 81 | 82 | ||
| 82 | async function fetchActionDoRequest(actionElem, url, opt) { | 83 | async function fetchActionDoRequest(actionElem, url, opt) { |
| 83 | try { | 84 | try { |
| 84 | const resp = await fetch(url, opt); | 85 | const resp = await request(url, opt); |
| 85 | if (resp.status === 200) { | 86 | if (resp.status === 200) { |
| 86 | let {redirect} = await resp.json(); | 87 | let {redirect} = await resp.json(); |
| 87 | redirect = redirect || actionElem.getAttribute('data-redirect'); | 88 | redirect = redirect || actionElem.getAttribute('data-redirect'); |
| @@ -127,7 +128,7 @@ async function formFetchAction(e) { · 39 unmodified lines | |||
| 127 | } | 128 | } |
| 128 | 129 | ||
| 129 | let reqUrl = formActionUrl; | 130 | let reqUrl = formActionUrl; |
| 130 | const reqOpt = {method: formMethod.toUpperCase(), headers: {'X-Csrf-Token': csrfToken}}; | 131 | const reqOpt = {method: formMethod.toUpperCase()}; |
| 131 | if (formMethod.toLowerCase() === 'get') { | 132 | if (formMethod.toLowerCase() === 'get') { |
| 132 | const params = new URLSearchParams(); | 133 | const params = new URLSearchParams(); |
| 133 | for (const [key, value] of formData) { | 134 | for (const [key, value] of formData) { |
| @@ -264,7 +265,7 @@ async function linkAction(e) { · 130 unmodified lines | |||
| 264 | const url = el.getAttribute('data-url'); | 265 | const url = el.getAttribute('data-url'); |
| 265 | const doRequest = async () => { | 266 | const doRequest = async () => { |
| 266 | el.disabled = true; | 267 | el.disabled = true; |
| 267 | await fetchActionDoRequest(el, url, {method: 'POST', headers: {'X-Csrf-Token': csrfToken}}); | 268 | await fetchActionDoRequest(el, url, {method: 'POST'}); |
| 268 | el.disabled = false; | 269 | el.disabled = false; |
| 269 | }; | 270 | }; |
| 270 | 271 | ||
| @@ -1,7 +1,8 @@ | |||
| 1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
| 2 | import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js'; | 2 | import {isElemHidden, onInputDebounce, toggleElem} from '../utils/dom.js'; |
| 3 | const {appSubUrl} = window.config; | 3 | import {GET} from '../modules/fetch.js'; |
| 4 | 4 | ||
| 5 | const {appSubUrl} = window.config; | ||
| 5 | const reIssueIndex = /^(\d+)$/; // eg: "123" | 6 | const reIssueIndex = /^(\d+)$/; // eg: "123" |
| 6 | const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123" | 7 | const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123" |
| 7 | const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}" | 8 | const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}" |
| @@ -54,7 +55,7 @@ export function initCommonIssueListQuickGoto() { · 46 unmodified lines | |||
| 54 | // try to check whether the parsed goto link is valid | 55 | // try to check whether the parsed goto link is valid |
| 55 | let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText); | 56 | let targetUrl = parseIssueListQuickGotoLink(repoLink, searchText); |
| 56 | if (targetUrl) { | 57 | if (targetUrl) { |
| 57 | const res = await fetch(`${targetUrl}/info`); | 58 | const res = await GET(`${targetUrl}/info`); |
| 58 | if (res.status !== 200) targetUrl = ''; | 59 | if (res.status !== 200) targetUrl = ''; |
| 59 | } | 60 | } |
| 60 | 61 | ||
| @@ -1,16 +1,11 @@ | |||
| 1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
| 2 | 2 | import {POST} from '../../modules/fetch.js'; | |
| 3 | const {csrfToken} = window.config; | ||
| 4 | 3 | ||
| 5 | async function uploadFile(file, uploadUrl) { | 4 | async function uploadFile(file, uploadUrl) { |
| 6 | const formData = new FormData(); | 5 | const formData = new FormData(); |
| 7 | formData.append('file', file, file.name); | 6 | formData.append('file', file, file.name); |
| 8 | 7 | ||
| 9 | const res = await fetch(uploadUrl, { | 8 | const res = await POST(uploadUrl, {data: formData}); |
| 10 | method: 'POST', | ||
| 11 | headers: {'X-Csrf-Token': csrfToken}, | ||
| 12 | body: formData, | ||
| 13 | }); | ||
| 14 | return await res.json(); | 9 | return await res.json(); |
| 15 | } | 10 | } |
| 16 | 11 | ||
| @@ -1,6 +1,5 @@ | |||
| 1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
| 2 | 2 | import {POST} from '../../modules/fetch.js'; | |
| 3 | const {csrfToken} = window.config; | ||
| 4 | 3 | ||
| 5 | export function initCompReactionSelector($parent) { | 4 | export function initCompReactionSelector($parent) { |
| 6 | $parent.find(`.select-reaction .item.reaction, .comment-reaction-button`).on('click', async function (e) { | 5 | $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 | |||
| 12 | const reactionContent = $(this).attr('data-reaction-content'); | 11 | const reactionContent = $(this).attr('data-reaction-content'); |
| 13 | const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true'; | 12 | const hasReacted = $(this).closest('.ui.segment.reactions').find(`a[data-reaction-content="${reactionContent}"]`).attr('data-has-reacted') === 'true'; |
| 14 | 13 | ||
| 15 | const res = await fetch(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, { | 14 | const res = await POST(`${actionUrl}/${hasReacted ? 'unreact' : 'react'}`, { |
| 16 | method: 'POST', | 15 | data: new URLSearchParams({content: reactionContent}), |
| 17 | headers: { | ||
| 18 | 'content-type': 'application/x-www-form-urlencoded', | ||
| 19 | }, | ||
| 20 | body: new URLSearchParams({ | ||
| 21 | _csrf: csrfToken, | ||
| 22 | content: reactionContent, | ||
| 23 | }), | ||
| 24 | }); | 16 | }); |
| 25 | 17 | ||
| 26 | const data = await res.json(); | 18 | const data = await res.json(); |
| @@ -1,6 +1,7 @@ | |||
| 1 | import {clippie} from 'clippie'; | 1 | import {clippie} from 'clippie'; |
| 2 | import {showTemporaryTooltip} from '../modules/tippy.js'; | 2 | import {showTemporaryTooltip} from '../modules/tippy.js'; |
| 3 | import {convertImage} from '../utils.js'; | 3 | import {convertImage} from '../utils.js'; |
| 4 | import {GET} from '../modules/fetch.js'; | ||
| 4 | 5 | ||
| 5 | const {i18n} = window.config; | 6 | const {i18n} = window.config; |
| 6 | 7 | ||
| @@ -20,7 +21,7 @@ export function initCopyContent() { · 13 unmodified lines | |||
| 20 | if (link) { | 21 | if (link) { |
| 21 | btn.classList.add('is-loading', 'small-loading-icon'); | 22 | btn.classList.add('is-loading', 'small-loading-icon'); |
| 22 | try { | 23 | try { |
| 23 | const res = await fetch(link, {credentials: 'include', redirect: 'follow'}); | 24 | const res = await GET(link, {credentials: 'include', redirect: 'follow'}); |
| 24 | const contentType = res.headers.get('content-type'); | 25 | const contentType = res.headers.get('content-type'); |
| 25 | 26 | ||
| 26 | if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) { | 27 | if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) { |
| @@ -1,5 +1,6 @@ | |||
| 1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
| 2 | import {hideElem, showElem} from '../utils/dom.js'; | 2 | import {hideElem, showElem} from '../utils/dom.js'; |
| 3 | import {GET} from '../modules/fetch.js'; | ||
| 3 | 4 | ||
| 4 | export function initInstall() { | 5 | export function initInstall() { |
| 5 | const $page = $('.page-content.install'); | 6 | const $page = $('.page-content.install'); |
| @@ -111,7 +112,7 @@ function initPostInstall() { · 105 unmodified lines | |||
| 111 | const targetUrl = el.getAttribute('href'); | 112 | const targetUrl = el.getAttribute('href'); |
| 112 | let tid = setInterval(async () => { | 113 | let tid = setInterval(async () => { |
| 113 | try { | 114 | try { |
| 114 | const resp = await fetch(targetUrl); | 115 | const resp = await GET(targetUrl); |
| 115 | if (tid && resp.status === 200) { | 116 | if (tid && resp.status === 200) { |
| 116 | clearInterval(tid); | 117 | clearInterval(tid); |
| 117 | tid = null; | 118 | tid = null; |
| @@ -1,7 +1,8 @@ | |||
| 1 | import {diffTreeStore} from '../modules/stores.js'; | 1 | import {diffTreeStore} from '../modules/stores.js'; |
| 2 | import {setFileFolding} from './file-fold.js'; | 2 | import {setFileFolding} from './file-fold.js'; |
| 3 | import {POST} from '../modules/fetch.js'; | ||
| 3 | 4 | ||
| 4 | const {csrfToken, pageData} = window.config; | 5 | const {pageData} = window.config; |
| 5 | const prReview = pageData.prReview || {}; | 6 | const prReview = pageData.prReview || {}; |
| 6 | const viewedStyleClass = 'viewed-file-checked-form'; | 7 | const viewedStyleClass = 'viewed-file-checked-form'; |
| 7 | const viewedCheckboxSelector = '.viewed-file-form'; // Selector under which all "Viewed" checkbox forms can be found | 8 | 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 | |||
| 68 | const data = {files}; | 69 | const data = {files}; |
| 69 | const headCommitSHA = form.getAttribute('data-headcommit'); | 70 | const headCommitSHA = form.getAttribute('data-headcommit'); |
| 70 | if (headCommitSHA) data.headCommitSHA = headCommitSHA; | 71 | if (headCommitSHA) data.headCommitSHA = headCommitSHA; |
| 71 | fetch(form.getAttribute('data-link'), { | 72 | POST(form.getAttribute('data-link'), {data}); |
| 72 | method: 'POST', | ||
| 73 | headers: {'X-Csrf-Token': csrfToken}, | ||
| 74 | body: JSON.stringify(data), | ||
| 75 | }); | ||
| 76 | 73 | ||
| 77 | // Fold the file accordingly | 74 | // Fold the file accordingly |
| 78 | const parentBox = form.closest('.diff-file-header'); | 75 | const parentBox = form.closest('.diff-file-header'); |
| @@ -1,9 +1,10 @@ | |||
| 1 | import {hideElem, showElem, toggleElem} from '../utils/dom.js'; | 1 | import {hideElem, showElem, toggleElem} from '../utils/dom.js'; |
| 2 | import {GET} from '../modules/fetch.js'; | ||
| 2 | 3 | ||
| 3 | async function loadBranchesAndTags(area, loadingButton) { | 4 | async function loadBranchesAndTags(area, loadingButton) { |
| 4 | loadingButton.classList.add('disabled'); | 5 | loadingButton.classList.add('disabled'); |
| 5 | try { | 6 | try { |
| 6 | const res = await fetch(loadingButton.getAttribute('data-fetch-url')); | 7 | const res = await GET(loadingButton.getAttribute('data-fetch-url')); |
| 7 | const data = await res.json(); | 8 | const data = await res.json(); |
| 8 | hideElem(loadingButton); | 9 | hideElem(loadingButton); |
| 9 | addTags(area, data.tags); | 10 | addTags(area, data.tags); |
| @@ -5,6 +5,7 @@ import {htmlEscape} from 'escape-goat'; · 4 unmodified lines | |||
| 5 | import {confirmModal} from './comp/ConfirmModal.js'; | 5 | import {confirmModal} from './comp/ConfirmModal.js'; |
| 6 | import {showErrorToast} from '../modules/toast.js'; | 6 | import {showErrorToast} from '../modules/toast.js'; |
| 7 | import {createSortable} from '../modules/sortable.js'; | 7 | import {createSortable} from '../modules/sortable.js'; |
| 8 | import {DELETE, POST} from '../modules/fetch.js'; | ||
| 8 | 9 | ||
| 9 | function initRepoIssueListCheckboxes() { | 10 | function initRepoIssueListCheckboxes() { |
| 10 | const $issueSelectAll = $('.issue-checkbox-all'); | 11 | const $issueSelectAll = $('.issue-checkbox-all'); |
| @@ -146,13 +147,7 @@ function initPinRemoveButton() { · 135 unmodified lines | |||
| 146 | const id = Number(el.getAttribute('data-issue-id')); | 147 | const id = Number(el.getAttribute('data-issue-id')); |
| 147 | 148 | ||
| 148 | // Send the unpin request | 149 | // Send the unpin request |
| 149 | const response = await fetch(el.getAttribute('data-unpin-url'), { | 150 | const response = await DELETE(el.getAttribute('data-unpin-url')); |
| 150 | method: 'delete', | ||
| 151 | headers: { | ||
| 152 | 'X-Csrf-Token': window.config.csrfToken, | ||
| 153 | 'Content-Type': 'application/json', | ||
| 154 | }, | ||
| 155 | }); | ||
| 156 | if (response.ok) { | 151 | if (response.ok) { |
| 157 | // Delete the tooltip | 152 | // Delete the tooltip |
| 158 | el._tippy.destroy(); | 153 | el._tippy.destroy(); |
| @@ -166,14 +161,7 @@ function initPinRemoveButton() { · 7 unmodified lines | |||
| 166 | async function pinMoveEnd(e) { | 161 | async function pinMoveEnd(e) { |
| 167 | const url = e.item.getAttribute('data-move-url'); | 162 | const url = e.item.getAttribute('data-move-url'); |
| 168 | const id = Number(e.item.getAttribute('data-issue-id')); | 163 | const id = Number(e.item.getAttribute('data-issue-id')); |
| 169 | await fetch(url, { | 164 | await POST(url, {data: {id, position: e.newIndex + 1}}); |
| 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 | }); | ||
| 177 | } | 165 | } |
| 178 | 166 | ||
| 179 | async function initIssuePinSort() { | 167 | async function initIssuePinSort() { |
| @@ -1,7 +1,8 @@ | |||
| 1 | import $ from 'jquery'; | 1 | import $ from 'jquery'; |
| 2 | import {hideElem, showElem} from '../utils/dom.js'; | 2 | import {hideElem, showElem} from '../utils/dom.js'; |
| 3 | import {GET, POST} from '../modules/fetch.js'; | ||
| 3 | 4 | ||
| 4 | const {appSubUrl, csrfToken} = window.config; | 5 | const {appSubUrl} = window.config; |
| 5 | 6 | ||
| 6 | export function initRepoMigrationStatusChecker() { | 7 | export function initRepoMigrationStatusChecker() { |
| 7 | const $repoMigrating = $('#repo_migrating'); | 8 | const $repoMigrating = $('#repo_migrating'); |
| @@ -13,7 +14,7 @@ export function initRepoMigrationStatusChecker() { · 5 unmodified lines | |||
| 13 | 14 | ||
| 14 | // returns true if the refresh still need to be called after a while | 15 | // returns true if the refresh still need to be called after a while |
| 15 | const refresh = async () => { | 16 | const refresh = async () => { |
| 16 | const res = await fetch(`${appSubUrl}/user/task/${task}`); | 17 | const res = await GET(`${appSubUrl}/user/task/${task}`); |
| 17 | if (res.status !== 200) return true; // continue to refresh if network error occurs | 18 | if (res.status !== 200) return true; // continue to refresh if network error occurs |
| 18 | 19 | ||
| 19 | const data = await res.json(); | 20 | const data = await res.json(); |
| @@ -58,12 +59,6 @@ export function initRepoMigrationStatusChecker() { · 38 unmodified lines | |||
| 58 | } | 59 | } |
| 59 | 60 | ||
| 60 | async function doMigrationRetry(e) { | 61 | async function doMigrationRetry(e) { |
| 61 | await fetch($(e.target).attr('data-migrating-task-retry-url'), { | 62 | await POST($(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 | }); | ||
| 68 | window.location.reload(); | 63 | window.location.reload(); |
| 69 | } | 64 | } |
| @@ -1,7 +1,8 @@ | |||
| 1 | import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; | 1 | import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; |
| 2 | import {showElem} from '../utils/dom.js'; | 2 | import {showElem} from '../utils/dom.js'; |
| 3 | import {GET, POST} from '../modules/fetch.js'; | ||
| 3 | 4 | ||
| 4 | const {appSubUrl, csrfToken} = window.config; | 5 | const {appSubUrl} = window.config; |
| 5 | 6 | ||
| 6 | export async function initUserAuthWebAuthn() { | 7 | export async function initUserAuthWebAuthn() { |
| 7 | const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); | 8 | const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); |
| @@ -13,7 +14,7 @@ export async function initUserAuthWebAuthn() { · 5 unmodified lines | |||
| 13 | return; | 14 | return; |
| 14 | } | 15 | } |
| 15 | 16 | ||
| 16 | const res = await fetch(`${appSubUrl}/user/webauthn/assertion`); | 17 | const res = await GET(`${appSubUrl}/user/webauthn/assertion`); |
| 17 | if (res.status !== 200) { | 18 | if (res.status !== 200) { |
| 18 | webAuthnError('unknown'); | 19 | webAuthnError('unknown'); |
| 19 | return; | 20 | return; |
| @@ -53,12 +54,8 @@ async function verifyAssertion(assertedCredential) { · 33 unmodified lines | |||
| 53 | const sig = new Uint8Array(assertedCredential.response.signature); | 54 | const sig = new Uint8Array(assertedCredential.response.signature); |
| 54 | const userHandle = new Uint8Array(assertedCredential.response.userHandle); | 55 | const userHandle = new Uint8Array(assertedCredential.response.userHandle); |
| 55 | 56 | ||
| 56 | const res = await fetch(`${appSubUrl}/user/webauthn/assertion`, { | 57 | const res = await POST(`${appSubUrl}/user/webauthn/assertion`, { |
| 57 | method: 'POST', | 58 | data: { |
| 58 | headers: { | ||
| 59 | 'Content-Type': 'application/json; charset=utf-8' | ||
| 60 | }, | ||
| 61 | body: JSON.stringify({ | ||
| 62 | id: assertedCredential.id, | 59 | id: assertedCredential.id, |
| 63 | rawId: encodeURLEncodedBase64(rawId), | 60 | rawId: encodeURLEncodedBase64(rawId), |
| 64 | type: assertedCredential.type, | 61 | type: assertedCredential.type, |
| @@ -69,7 +66,7 @@ async function verifyAssertion(assertedCredential) { · 4 unmodified lines | |||
| 69 | signature: encodeURLEncodedBase64(sig), | 66 | signature: encodeURLEncodedBase64(sig), |
| 70 | userHandle: encodeURLEncodedBase64(userHandle), | 67 | userHandle: encodeURLEncodedBase64(userHandle), |
| 71 | }, | 68 | }, |
| 72 | }), | 69 | }, |
| 73 | }); | 70 | }); |
| 74 | if (res.status === 500) { | 71 | if (res.status === 500) { |
| 75 | webAuthnError('unknown'); | 72 | webAuthnError('unknown'); |
| @@ -88,13 +85,8 @@ async function webauthnRegistered(newCredential) { · 12 unmodified lines | |||
| 88 | const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); | 85 | const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); |
| 89 | const rawId = new Uint8Array(newCredential.rawId); | 86 | const rawId = new Uint8Array(newCredential.rawId); |
| 90 | 87 | ||
| 91 | const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/register`, { | 88 | const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, { |
| 92 | method: 'POST', | 89 | data: { |
| 93 | headers: { | ||
| 94 | 'X-Csrf-Token': csrfToken, | ||
| 95 | 'Content-Type': 'application/json; charset=utf-8', | ||
| 96 | }, | ||
| 97 | body: JSON.stringify({ | ||
| 98 | id: newCredential.id, | 90 | id: newCredential.id, |
| 99 | rawId: encodeURLEncodedBase64(rawId), | 91 | rawId: encodeURLEncodedBase64(rawId), |
| 100 | type: newCredential.type, | 92 | type: newCredential.type, |
| @@ -102,7 +94,7 @@ async function webauthnRegistered(newCredential) { · 1 unmodified line | |||
| 102 | attestationObject: encodeURLEncodedBase64(attestationObject), | 94 | attestationObject: encodeURLEncodedBase64(attestationObject), |
| 103 | clientDataJSON: encodeURLEncodedBase64(clientDataJSON), | 95 | clientDataJSON: encodeURLEncodedBase64(clientDataJSON), |
| 104 | }, | 96 | }, |
| 105 | }), | 97 | }, |
| 106 | }); | 98 | }); |
| 107 | 99 | ||
| 108 | if (res.status === 409) { | 100 | if (res.status === 409) { |
| @@ -165,15 +157,11 @@ export function initUserAuthWebAuthnRegister() { · 56 unmodified lines | |||
| 165 | async function webAuthnRegisterRequest() { | 157 | async function webAuthnRegisterRequest() { |
| 166 | const elNickname = document.getElementById('nickname'); | 158 | const elNickname = document.getElementById('nickname'); |
| 167 | 159 | ||
| 168 | const body = new FormData(); | 160 | const formData = new FormData(); |
| 169 | body.append('name', elNickname.value); | 161 | formData.append('name', elNickname.value); |
| 170 | 162 | ||
| 171 | const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/request_register`, { | 163 | const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, { |
| 172 | method: 'POST', | 164 | data: formData, |
| 173 | headers: { | ||
| 174 | 'X-Csrf-Token': csrfToken, | ||
| 175 | }, | ||
| 176 | body, | ||
| 177 | }); | 165 | }); |
| 178 | 166 | ||
| 179 | if (res.status === 409) { | 167 | if (res.status === 409) { |
| @@ -2,17 +2,18 @@ import {isObject} from '../utils.js'; · 1 unmodified line | |||
| 2 | 2 | ||
| 3 | const {csrfToken} = window.config; | 3 | const {csrfToken} = window.config; |
| 4 | 4 | ||
| 5 | // safe HTTP methods that don't need a csrf token | ||
| 6 | const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']); | ||
| 7 | |||
| 5 | // fetch wrapper, use below method name functions and the `data` option to pass in data | 8 | // 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, | 9 | // which will automatically set an appropriate headers. For json content, only object |
| 7 | // only object and array types are currently supported. | 10 | // and array types are currently supported. |
| 8 | function request(url, {headers, data, body, ...other} = {}) { | 11 | export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) { |
| 9 | let contentType; | 12 | let contentType; |
| 10 | if (!body) { | 13 | if (!body) { |
| 11 | if (data instanceof FormData) { | 14 | if (data instanceof FormData) { |
| 12 | contentType = 'multipart/form-data'; | ||
| 13 | body = data; | 15 | body = data; |
| 14 | } else if (data instanceof URLSearchParams) { | 16 | } else if (data instanceof URLSearchParams) { |
| 15 | contentType = 'application/x-www-form-urlencoded'; | ||
| 16 | body = data; | 17 | body = data; |
| 17 | } else if (isObject(data) || Array.isArray(data)) { | 18 | } else if (isObject(data) || Array.isArray(data)) { |
| 18 | contentType = 'application/json'; | 19 | contentType = 'application/json'; |
| @@ -20,12 +21,18 @@ function request(url, {headers, data, body, ...other} = {}) { · 1 unmodified line | |||
| 20 | } | 21 | } |
| 21 | } | 22 | } |
| 22 | 23 | ||
| 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 | |||
| 23 | return fetch(url, { | 33 | return fetch(url, { |
| 24 | headers: { | 34 | method, |
| 25 | 'x-csrf-token': csrfToken, | 35 | headers: headersMerged, |
| 26 | ...(contentType && {'content-type': contentType}), | ||
| 27 | ...headers, | ||
| 28 | }, | ||
| 29 | ...(body && {body}), | 36 | ...(body && {body}), |
| 30 | ...other, | 37 | ...other, |
| 31 | }); | 38 | }); |