@samitouri / QOSami-HFS / commits / 6feb1e57

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