new config: disable_custom_html
Massimo Melina committed
Feb 19, 2025 at 22:20 UTC
6feb1e576a3473d3004b6132961476b040905773
6 files changed
+82
-9
admin/src/CustomHtmlPage.ts
+15
-4
@@ -4,14 +4,16 @@ import { createElement as h, Fragment, useEffect, useMemo, useState } from 'reac
4
import { Field, SelectField } from '@hfs/mui-grid-form'
5
import { apiCall, useApiEx } from './api'
6
import { Alert, Box } from '@mui/material'
7
-import { Dict, HTTP_MESSAGES, isCtrlKey, prefix, md, isNumeric } from './misc';
8
-import { IconBtn, reloadBtn, wikiLink } from './mui';
7
+import { Dict, HTTP_MESSAGES, isCtrlKey, prefix, md, isNumeric, CFG } from './misc'
8
+import { hTooltip, IconBtn, reloadBtn, wikiLink } from './mui'
9
import { Save } from '@mui/icons-material'
10
import _ from 'lodash'
11
import { useDebounce } from 'usehooks-ts'
12
import { TextEditor } from './TextEditor';
13
import { state, useSnapState } from './state'
14
import { PageProps } from './App'
15
+import { switchBtn } from './VerticalSwitch';
16
+import { adminApis } from '../../src/adminApis'
17
18
const names: any = {
19
top: "Top of HTML Body",
@@ -19,11 +21,15 @@ const names: any = {
21
}
22
23
export default function CustomHtmlPage({ setTitleSide }: PageProps) {
22
- const { data, reload } = useApiEx<{ sections: Dict<string> }>('get_custom_html')
24
+ const { data, reload } = useApiEx<typeof adminApis.get_custom_html>('get_custom_html')
25
const { customHtmlSection: section } = useSnapState()
26
const [all, setAll] = useState<Dict<string>>({})
27
const [saved, setSaved] = useState({})
26
- useEffect(() => data && setSaved(data?.sections), [data])
28
+ useEffect(() => {
29
+ if (!data) return
30
+ setSaved(data.sections)
31
+ setEnabled(data.enabled)
32
+ }, [data])
33
useEffect(() => setAll(saved), [saved])
34
const options = useMemo(() => {
35
const keys = _.sortBy(Object.keys(all), isNumeric) // http codes at the bottom
@@ -36,6 +42,7 @@ export default function CustomHtmlPage({ setTitleSide }: PageProps) {
42
}, [useDebounce(all, 500)])
43
const anyChange = useMemo(() => !_.isEqualWith(saved, all, (a,b) => !a && !b || undefined),
44
[saved, all])
45
+ const [enabled, setEnabled] = useState<boolean>()
46
setTitleSide(useMemo(() => h(Box, { sx: { display: { xs: 'none', md: 'block' } } },
47
h(Alert, { severity: 'info' },
48
md("Add HTML code to some parts of the Front-end. It's saved to file `custom.html`, that you can edit directly with your editor of choice. "),
@@ -58,6 +65,10 @@ export default function CustomHtmlPage({ setTitleSide }: PageProps) {
65
modified: anyChange,
66
onClick: save,
67
}),
68
+ hTooltip("Enable / Disable", undefined, switchBtn(enabled, async v => {
69
+ await apiCall('set_config', { values: { [CFG.disable_custom_html]: !v } })
70
+ setEnabled(v)
71
+ })),
72
),
73
h(TextEditor, {
74
lang: section === 'style' ? 'css' : section === 'script' ? 'js' : 'html',
admin/src/VerticalSwitch.ts
new
+59
@@ -0,0 +1,59 @@
1
+import { createElement as h } from 'react'
2
+import { styled, Switch, SwitchProps } from '@mui/material'
3
+
4
+export function switchBtn(value: boolean | undefined, onChange: (v: boolean) => void, props?: SwitchProps) {
5
+ return h(VerticalSwitch, {
6
+ checked: value || false,
7
+ onChange(ev, v) { onChange(v) },
8
+ disabled: value === undefined,
9
+ ...props
10
+ })
11
+}
12
+
13
+export const VerticalSwitch = styled(Switch)(({ theme }) => ({
14
+ height: 48, width: 26,
15
+ padding: 5,
16
+ '& .MuiSwitch-switchBase': {
17
+ margin: 1,
18
+ padding: 0,
19
+ transform: 'translateY(22px)',
20
+ '&.Mui-checked': {
21
+ color: theme.palette.primary.contrastText,
22
+ transform: 'translateY(2px)',
23
+ '& .MuiSwitch-thumb:before': {
24
+ backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
25
+ theme.palette.primary.contrastText
26
+ )}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
27
+ },
28
+ '& + .MuiSwitch-track': {
29
+ backgroundColor: theme.palette.grey[500],
30
+ },
31
+ },
32
+ },
33
+ '& .Mui-checked .MuiSwitch-thumb': {
34
+ backgroundColor: theme.palette.primary.main,
35
+ boxShadow: '0px 2px 1px 1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12)',
36
+ },
37
+ '& .MuiSwitch-thumb': {
38
+ width: 24,
39
+ height: 24,
40
+ backgroundColor: theme.palette.grey[700],
41
+ '&::before': {
42
+ content: "''",
43
+ position: 'absolute',
44
+ width: '85%',
45
+ height: '85%',
46
+ left: 0,
47
+ top: 0,
48
+ backgroundRepeat: 'no-repeat',
49
+ backgroundPosition: 'center',
50
+ backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
51
+ theme.palette.primary.contrastText
52
+ )}" d="M19,13H5V11H19V13Z"/></svg>')`,
53
+ },
54
+ },
55
+ '& .MuiSwitch-track': {
56
+ backgroundColor: theme.palette.grey[500],
57
+ borderRadius: 20 / 2,
58
+ },
59
+}))
\ No newline at end of file
config.md
+1
@@ -124,6 +124,7 @@ Configuration can be done in several ways
124
- `allow_session_ip_change` should requests of the same login session be allowed from different IP addresses. Default is false, to prevent cookie stealing. You can set it `true` to always allow it, or `https` to allow only on https, where stealing the cookie is harder. No UI.
125
- `authorization_header` support Authentication HTTP header. Default is true. No UI.
126
- `cache_control_disk_files` number of seconds after which the browser should bypass the cache and check the server for an updated version of the file. Default is 5. No UI.
127
+- `disable_custom_html` disable the content of `custom_html`. Default is false.
128
- `create-admin` special entry to quickly create an admin account. The value will be set as password. As soon as the account is created, this entry is removed.
129
130
#### Virtual File System (VFS)
src/adminApis.ts
+2
-1
@@ -22,7 +22,7 @@ import Koa from 'koa'
22
import { cloudflareDetected, getProxyDetected } from './middlewares'
23
import { execFile } from 'child_process'
24
import { promisify } from 'util'
25
-import { customHtmlSections, customHtml, saveCustomHtml } from './customHtml'
25
+import { customHtmlSections, customHtml, saveCustomHtml, disableCustomHtml } from './customHtml'
26
import _ from 'lodash'
27
import {
28
autoCheckUpdateResult, getUpdates, getVersions, localUpdateAvailable, update, updateSupported, previousAvailable
@@ -106,6 +106,7 @@ export const adminApis = {
106
107
get_custom_html() {
108
return {
109
+ enabled: !disableCustomHtml.get(),
110
sections: Object.fromEntries([
111
...customHtmlSections.concat(getErrorSections()).map(k => [k,'']), // be sure to output all sections
112
...customHtml.sections // override entries above
src/cross.ts
+1
-1
@@ -30,7 +30,7 @@ export const THEME_OPTIONS = { auto: '', light: 'light', dark: 'dark' }
30
export const CFG = constMap(['geo_enable', 'geo_allow', 'geo_list', 'geo_allow_unknown', 'dynamic_dns_url',
31
'log', 'error_log', 'log_rotation', 'dont_log_net', 'log_gui', 'log_api', 'log_ua', 'log_spam', 'track_ips',
32
'max_downloads', 'max_downloads_per_ip', 'max_downloads_per_account', 'roots', 'force_address', 'split_uploads',
33
- 'allow_session_ip_change', 'force_lang', 'suspend_plugins', 'base_url', 'size_1024'])
33
+ 'allow_session_ip_change', 'force_lang', 'suspend_plugins', 'base_url', 'size_1024', 'disable_custom_html'])
34
export const LIST = { add: '+', remove: '-', update: '=', props: 'props', ready: 'ready', error: 'e' }
35
export type Dict<T=any> = Record<string, T>
36
export type Falsy = false | null | undefined | '' | 0
src/customHtml.ts
+4
-3
@@ -1,6 +1,6 @@
1
-import { prefix } from './misc'
1
+import { CFG, prefix } from './misc'
2
import { watchLoad } from './watchLoad'
3
-import { proxy } from 'valtio/vanilla' // without /vanilla we trigger react dependency
3
+import { defineConfig } from './config'
4
import Dict = NodeJS.Dict
5
import { writeFile } from 'fs/promises'
6
import { mapPlugins } from './plugins'
@@ -12,6 +12,7 @@ export const customHtmlSections: ReadonlyArray<string> = ['style', 'script', 'be
12
'footer', 'top', 'bottom', 'afterEntryName', 'beforeLogin', 'unauthorized', 'htmlHead', 'userPanelAfterInfo']
13
14
export const customHtml = watchLoadCustomHtml()
15
+export const disableCustomHtml = defineConfig(CFG.disable_custom_html, false)
16
17
export function watchLoadCustomHtml(folder='') {
18
const sections = new Map<string, string>()
@@ -33,7 +34,7 @@ export function watchLoadCustomHtml(folder='') {
34
}
35
36
export function getSection(name: string) {
36
- return (customHtml.sections.get(name) || '')
37
+ return (!disableCustomHtml.get() && customHtml.sections.get(name) || '')
38
+ mapPlugins(pl => pl.getData().getCustomHtml()[name]).join('\n')
39
}
40