Skip to content

Commit 0b0ff57

Browse files
committed
fix
1 parent 698ae7a commit 0b0ff57

File tree

8 files changed

+101
-89
lines changed

8 files changed

+101
-89
lines changed

modules/markup/sanitizer_default_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ func TestSanitizer(t *testing.T) {
6262
`<a href="javascript:alert('xss')">bad</a>`, `bad`,
6363
`<a href="vbscript:no">bad</a>`, `bad`,
6464
`<a href="data:1234">bad</a>`, `bad`,
65+
66+
// Some classes and attributes are used by the frontend framework and will execute JS code, so make sure they are removed
67+
`<div class="link-action" data-attr-class="foo" data-url="xxx">txt</div>`, `<div data-attr-class="foo">txt</div>`,
68+
`<div class="form-fetch-action" data-markdown-generated-content="bar" data-global-init="a" data-global-click="b">txt</div>`, `<div data-markdown-generated-content="bar">txt</div>`,
6569
}
6670

6771
for i := 0; i < len(testCases); i += 2 {

templates/base/alert.tmpl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
{{if .Flash.ErrorMsg}}
1+
{{- if .Flash.ErrorMsg -}}
22
<div class="ui negative message flash-message flash-error">
33
<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p>
44
</div>
5-
{{end}}
6-
{{if .Flash.SuccessMsg}}
5+
{{- end -}}
6+
{{- if .Flash.SuccessMsg -}}
77
<div class="ui positive message flash-message flash-success">
88
<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
99
</div>
10-
{{end}}
11-
{{if .Flash.InfoMsg}}
10+
{{- end -}}
11+
{{- if .Flash.InfoMsg -}}
1212
<div class="ui info message flash-message flash-info">
1313
<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
1414
</div>
15-
{{end}}
16-
{{if .Flash.WarningMsg}}
15+
{{- end -}}
16+
{{- if .Flash.WarningMsg -}}
1717
<div class="ui warning message flash-message flash-warning">
1818
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
1919
</div>
20-
{{end}}
20+
{{- end -}}

templates/repo/issue/new_form.tmpl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
{{if .Flash}}
21
{{template "base/alert" .}}
3-
{{end}}
42
<form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post">
53
{{.CsrfTokenHtml}}
64
<div class="issue-content-left">
@@ -9,7 +7,10 @@
97
{{ctx.AvatarUtils.Avatar .SignedUser 40}}
108
<div class="ui segment content tw-my-0">
119
<div class="field">
12-
<input name="title" class="js-autofocus-end" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" required maxlength="255" autocomplete="off">
10+
<input name="title" data-global-init="initInputAutoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off"
11+
placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}"
12+
value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}"
13+
>
1314
{{if .PageIsComparePull}}
1415
<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div>
1516
{{end}}

web_src/js/features/autofocus-end.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

web_src/js/features/common-page.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts';
22
import {showGlobalErrorMessage} from '../bootstrap.ts';
33
import {fomanticQuery} from '../modules/fomantic/base.ts';
44
import {queryElems} from '../utils/dom.ts';
5-
import {observeAddedElement} from '../modules/observer.ts';
5+
import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts';
66

77
const {appUrl} = window.config;
88

@@ -30,7 +30,7 @@ export function initFootLanguageMenu() {
3030

3131
export function initGlobalDropdown() {
3232
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
33-
observeAddedElement('.ui.dropdown:not(.custom)', (el) => {
33+
registerGlobalSelectorFunc('.ui.dropdown:not(.custom)', (el) => {
3434
const $dropdown = fomanticQuery(el);
3535
if ($dropdown.data('module-dropdown')) return; // do not re-init if other code has already initialized it.
3636

@@ -80,6 +80,25 @@ export function initGlobalTabularMenu() {
8080
fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false});
8181
}
8282

83+
// for performance considerations, it only uses performant syntax
84+
function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>) {
85+
if (el.type !== 'hidden' &&
86+
el.type !== 'checkbox' &&
87+
el.type !== 'radio' &&
88+
el.type !== 'range' &&
89+
el.type !== 'color') {
90+
el.dir = 'auto';
91+
}
92+
}
93+
94+
export function initGlobalInput() {
95+
registerGlobalSelectorFunc('input, textarea', attachInputDirAuto);
96+
registerGlobalInitFunc('initInputAutoFocusEnd', (el: HTMLInputElement) => {
97+
el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
98+
el.setSelectionRange(el.value.length, el.value.length);
99+
});
100+
}
101+
83102
/**
84103
* Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
85104
* * Cross-origin API request without correct cookie

web_src/js/features/repo-diff.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {POST, GET} from '../modules/fetch.ts';
1010
import {createTippy} from '../modules/tippy.ts';
1111
import {invertFileFolding} from './file-fold.ts';
1212
import {parseDom} from '../utils.ts';
13-
import {observeAddedElement} from '../modules/observer.ts';
13+
import {registerGlobalSelectorFunc} from '../modules/observer.ts';
1414

1515
const {i18n} = window.config;
1616

@@ -254,7 +254,7 @@ export function initRepoDiffView() {
254254
initExpandAndCollapseFilesButton();
255255
initRepoDiffHashChangeListener();
256256

257-
observeAddedElement('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
257+
registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox);
258258
addDelegatedEventListener(document, 'click', '.fold-file', (el) => {
259259
invertFileFolding(el.closest('.file-content'), el);
260260
});

web_src/js/index.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {initImageDiff} from './features/imagediff.ts';
1111
import {initRepoMigration} from './features/repo-migration.ts';
1212
import {initRepoProject} from './features/repo-projects.ts';
1313
import {initTableSort} from './features/tablesort.ts';
14-
import {initAutoFocusEnd} from './features/autofocus-end.ts';
1514
import {initAdminUserListSearchForm} from './features/admin/users.ts';
1615
import {initAdminConfigs} from './features/admin/config.ts';
1716
import {initMarkupAnchors} from './markup/anchors.ts';
@@ -62,31 +61,17 @@ import {initRepoContributors} from './features/contributors.ts';
6261
import {initRepoCodeFrequency} from './features/code-frequency.ts';
6362
import {initRepoRecentCommits} from './features/recent-commits.ts';
6463
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts';
65-
import {initAddedElementObserver} from './modules/observer.ts';
64+
import {initGlobalSelectorObserver} from './modules/observer.ts';
6665
import {initRepositorySearch} from './features/repo-search.ts';
6766
import {initColorPickers} from './features/colorpicker.ts';
6867
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
6968
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
7069
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
71-
import {
72-
initFootLanguageMenu,
73-
initGlobalDropdown,
74-
initGlobalTabularMenu,
75-
initHeadNavbarContentToggle,
76-
} from './features/common-page.ts';
77-
import {
78-
initGlobalButtonClickOnEnter,
79-
initGlobalButtons,
80-
initGlobalDeleteButton,
81-
} from './features/common-button.ts';
82-
import {
83-
initGlobalComboMarkdownEditor,
84-
initGlobalEnterQuickSubmit,
85-
initGlobalFormDirtyLeaveConfirm,
86-
} from './features/common-form.ts';
70+
import {initFootLanguageMenu, initGlobalDropdown, initGlobalInput, initGlobalTabularMenu, initHeadNavbarContentToggle,} from './features/common-page.ts';
71+
import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton,} from './features/common-button.ts';
72+
import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm,} from './features/common-form.ts';
8773

8874
initGiteaFomantic();
89-
initAddedElementObserver();
9075
initSubmitEventPolyfill();
9176

9277
function callInitFunctions(functions: (() => any)[]) {
@@ -129,6 +114,7 @@ onDomReady(() => {
129114
initGlobalFormDirtyLeaveConfirm,
130115
initGlobalComboMarkdownEditor,
131116
initGlobalDeleteButton,
117+
initGlobalInput,
132118

133119
initCommonOrganization,
134120
initCommonIssueListQuickGoto,
@@ -150,7 +136,6 @@ onDomReady(() => {
150136
initSshKeyFormParser,
151137
initStopwatch,
152138
initTableSort,
153-
initAutoFocusEnd,
154139
initFindFileInRepo,
155140
initCopyContent,
156141

@@ -211,5 +196,8 @@ onDomReady(() => {
211196
initColorPickers,
212197

213198
initOAuth2SettingsDisableCheckbox,
199+
200+
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
201+
initGlobalSelectorObserver,
214202
]);
215203
});

web_src/js/modules/observer.ts

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,71 @@
11
import {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
22

3-
type DirElement = HTMLInputElement | HTMLTextAreaElement;
3+
let globalSelectorObserverInited = false;
44

5-
// for performance considerations, it only uses performant syntax
6-
function attachDirAuto(el: Partial<DirElement>) {
7-
if (el.type !== 'hidden' &&
8-
el.type !== 'checkbox' &&
9-
el.type !== 'radio' &&
10-
el.type !== 'range' &&
11-
el.type !== 'color') {
12-
el.dir = 'auto';
13-
}
14-
}
5+
type SelectorHandler = {selector: string, handler: (el: HTMLElement) => void};
6+
const selectorHandlers: SelectorHandler[] = [];
7+
8+
type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => (void | Promise<void>);
9+
const globalEventFuncs: Record<string, GlobalEventFunc<HTMLElement, Event>> = {};
1510

1611
type GlobalInitFunc<T extends HTMLElement> = (el: T) => void | Promise<void>;
1712
const globalInitFuncs: Record<string, GlobalInitFunc<HTMLElement>> = {};
18-
function attachGlobalInit(el: HTMLElement) {
19-
const initFunc = el.getAttribute('data-global-init');
20-
const func = globalInitFuncs[initFunc];
21-
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
22-
func(el);
23-
}
2413

25-
type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => (void | Promise<void>);
26-
const globalEventFuncs: Record<string, GlobalEventFunc<HTMLElement, Event>> = {};
14+
// It handles the global events for all `<div data-global-click="onSomeElemClick"></div>` elements.
2715
export function registerGlobalEventFunc<T extends HTMLElement, E extends Event>(event: string, name: string, func: GlobalEventFunc<T, E>) {
2816
globalEventFuncs[`${event}:${name}`] = func as any;
2917
}
3018

31-
type SelectorHandler = {
32-
selector: string,
33-
handler: (el: HTMLElement) => void,
34-
};
35-
36-
const selectorHandlers: SelectorHandler[] = [
37-
{selector: 'input, textarea', handler: attachDirAuto},
38-
{selector: '[data-global-init]', handler: attachGlobalInit},
39-
];
40-
41-
export function observeAddedElement(selector: string, handler: (el: HTMLElement) => void) {
19+
// It handles the global init functions by a selector, for example:
20+
// > registerGlobalSelectorObserver('.ui.dropdown:not(.custom)', (el) => { initDropdown(el, ...) });
21+
export function registerGlobalSelectorFunc(selector: string, handler: (el: HTMLElement) => void) {
4222
selectorHandlers.push({selector, handler});
43-
const docNodes = document.querySelectorAll<HTMLElement>(selector);
44-
for (const el of docNodes) {
23+
// Then initAddedElementObserver will call this handler for all existing elements after all handlers are added.
24+
// This approach makes the init stage only need to do one "querySelectorAll".
25+
if (!globalSelectorObserverInited) return;
26+
for (const el of document.querySelectorAll<HTMLElement>(selector)) {
4527
handler(el);
4628
}
4729
}
4830

49-
export function initAddedElementObserver(): void {
31+
// It handles the global init functions for all `<div data-global-int="initSomeElem"></div>` elements.
32+
export function registerGlobalInitFunc<T extends HTMLElement>(name: string, handler: GlobalInitFunc<T>) {
33+
globalInitFuncs[name] = handler as any;
34+
// The "global init" functions are managed internally and called by callGlobalInitFunc
35+
// They must be ready before initGlobalSelectorObserver is called.
36+
if (globalSelectorObserverInited) throw new Error('registerGlobalInitFunc() must be called before initGlobalSelectorObserver()');
37+
}
38+
39+
function callGlobalInitFunc(el: HTMLElement) {
40+
const initFunc = el.getAttribute('data-global-init');
41+
const func = globalInitFuncs[initFunc];
42+
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
43+
44+
type GiteaGlobalInitElement = Partial<HTMLElement> & {_giteaGlobalInited: boolean};
45+
if ((el as GiteaGlobalInitElement)._giteaGlobalInited) throw new Error(`Global init function "${initFunc}" already executed`);
46+
(el as GiteaGlobalInitElement)._giteaGlobalInited = true;
47+
func(el);
48+
}
49+
50+
function attachGlobalEvents() {
51+
// add global "[data-global-click]" event handler
52+
document.addEventListener('click', (e) => {
53+
const elem = (e.target as HTMLElement).closest<HTMLElement>('[data-global-click]');
54+
if (!elem) return;
55+
const funcName = elem.getAttribute('data-global-click');
56+
const func = globalEventFuncs[`click:${funcName}`];
57+
if (!func) throw new Error(`Global event function "click:${funcName}" not found`);
58+
func(elem, e);
59+
});
60+
}
61+
62+
export function initGlobalSelectorObserver(): void {
63+
if (globalSelectorObserverInited) throw new Error('initGlobalSelectorObserver() already called');
64+
globalSelectorObserverInited = true;
65+
66+
attachGlobalEvents();
67+
68+
selectorHandlers.push({selector: '[data-global-init]', handler: callGlobalInitFunc});
5069
const observer = new MutationObserver((mutationList) => {
5170
const len = mutationList.length;
5271
for (let i = 0; i < len; i++) {
@@ -60,30 +79,17 @@ export function initAddedElementObserver(): void {
6079
if (addedNode.matches(selector)) {
6180
handler(addedNode);
6281
}
63-
const children = addedNode.querySelectorAll<HTMLElement>(selector);
64-
for (const el of children) {
82+
for (const el of addedNode.querySelectorAll<HTMLElement>(selector)) {
6583
handler(el);
6684
}
6785
}
6886
}
6987
}
7088
});
71-
7289
for (const {selector, handler} of selectorHandlers) {
73-
const docNodes = document.querySelectorAll<HTMLElement>(selector);
74-
for (const el of docNodes) {
90+
for (const el of document.querySelectorAll<HTMLElement>(selector)) {
7591
handler(el);
7692
}
7793
}
78-
7994
observer.observe(document, {subtree: true, childList: true});
80-
81-
document.addEventListener('click', (e) => {
82-
const elem = (e.target as HTMLElement).closest<HTMLElement>('[data-global-click]');
83-
if (!elem) return;
84-
const funcName = elem.getAttribute('data-global-click');
85-
const func = globalEventFuncs[`click:${funcName}`];
86-
if (!func) throw new Error(`Global event function "click:${funcName}" not found`);
87-
func(elem, e);
88-
});
8995
}

0 commit comments

Comments
 (0)