diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index 52273e690..2a27c52c6 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -1508,6 +1508,20 @@ "formatZH": "更新角色 [name]", "formatEN": "update role [name]" }, + "/core/enterprise/settings/footer/reset": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "重置企业版底部链接设置", + "formatEN": "reset Enterprise footer link settings" + }, + "/core/enterprise/settings/footer/update": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "更新企业版底部链接设置", + "formatEN": "update Enterprise footer link settings" + }, "/core/enterprise/skills-hub/delete": { "bodyKeys": [ "id" diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index 52273e690..2a27c52c6 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -1508,6 +1508,20 @@ "formatZH": "更新角色 [name]", "formatEN": "update role [name]" }, + "/core/enterprise/settings/footer/reset": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "重置企业版底部链接设置", + "formatEN": "reset Enterprise footer link settings" + }, + "/core/enterprise/settings/footer/update": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "更新企业版底部链接设置", + "formatEN": "update Enterprise footer link settings" + }, "/core/enterprise/skills-hub/delete": { "bodyKeys": [ "id" diff --git a/frontend/src/components/footer-navigation/event.ts b/frontend/src/components/footer-navigation/event.ts new file mode 100644 index 000000000..2e06310a4 --- /dev/null +++ b/frontend/src/components/footer-navigation/event.ts @@ -0,0 +1,5 @@ +export const FOOTER_NAVIGATION_REFRESH_EVENT = '1panel:footer-navigation-refresh'; + +export const refreshFooterNavigation = () => { + window.dispatchEvent(new Event(FOOTER_NAVIGATION_REFRESH_EVENT)); +}; diff --git a/frontend/src/components/footer-navigation/index.vue b/frontend/src/components/footer-navigation/index.vue new file mode 100644 index 000000000..3686a8e0c --- /dev/null +++ b/frontend/src/components/footer-navigation/index.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/frontend/src/components/footer-navigation/model.ts b/frontend/src/components/footer-navigation/model.ts new file mode 100644 index 000000000..67e1836cc --- /dev/null +++ b/frontend/src/components/footer-navigation/model.ts @@ -0,0 +1,128 @@ +export const footerNavigationKeys = ['learnMore', 'forum', 'documentation', 'project'] as const; + +export type FooterNavigationKey = (typeof footerNavigationKeys)[number]; + +export interface FooterNavigationLinkSetting { + visible: boolean; + url: string; +} + +export type FooterNavigationLinks = Record; + +export interface FooterNavigationSetting { + customized: boolean; + links: FooterNavigationLinks; +} + +export interface FooterNavigationSettingEditor { + validate: () => Promise; + save: () => Promise; + restoreDefaults: () => Promise; + reload: () => Promise; + isDirty: () => boolean; +} + +export const createDefaultFooterNavigationLinks = (isIntl: boolean, docsUrl: string): FooterNavigationLinks => ({ + learnMore: { + visible: true, + url: isIntl ? 'https://1panel.pro/pricing' : 'https://1panel.cn/versions.html', + }, + forum: { + visible: true, + url: isIntl ? 'https://github.com/1Panel-dev/1Panel/discussions' : 'https://bbs.fit2cloud.com/c/1p/7', + }, + documentation: { + visible: true, + url: docsUrl.endsWith('/') ? docsUrl : `${docsUrl}/`, + }, + project: { + visible: true, + url: 'https://github.com/1Panel-dev/1Panel', + }, +}); + +const controlCharacterPattern = /[\u0000-\u001f\u007f-\u009f]/u; +const schemeAuthorityPattern = /^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/i; +const percentBytePattern = /^[\da-f]{2}$/i; + +const containsControlCharacter = (value: string) => controlCharacterPattern.test(value); + +const decodePercentEscapes = (value: string): string | null => { + if (!value.includes('%')) { + return value; + } + + const encoder = new TextEncoder(); + const bytes: number[] = []; + let segmentStart = 0; + for (let index = 0; index < value.length; index += 1) { + if (value[index] !== '%') { + continue; + } + bytes.push(...encoder.encode(value.slice(segmentStart, index))); + const escapedByte = value.slice(index + 1, index + 3); + if (!percentBytePattern.test(escapedByte)) { + return null; + } + bytes.push(Number.parseInt(escapedByte, 16)); + index += 2; + segmentStart = index + 1; + } + bytes.push(...encoder.encode(value.slice(segmentStart))); + return new TextDecoder().decode(Uint8Array.from(bytes)); +}; + +export const isSafeExternalUrl = (value: unknown): value is string => { + if (typeof value !== 'string') { + return false; + } + if (containsControlCharacter(value)) { + return false; + } + + const normalizedURL = value.trim(); + if (normalizedURL.includes('\\')) { + return false; + } + const authority = normalizedURL.match(schemeAuthorityPattern)?.[1]; + if (!authority || authority.endsWith(':') || authority.includes('@') || authority.includes('%')) { + return false; + } + + const decodedURL = decodePercentEscapes(normalizedURL); + if (decodedURL === null || containsControlCharacter(decodedURL)) { + return false; + } + + try { + const url = new URL(normalizedURL); + const port = url.port ? Number(url.port) : null; + return ( + (url.protocol === 'http:' || url.protocol === 'https:') && + Boolean(url.hostname) && + !url.username && + !url.password && + (port === null || (Number.isInteger(port) && port >= 1 && port <= 65535)) + ); + } catch { + return false; + } +}; + +export const mergeFooterNavigationLinks = ( + setting: FooterNavigationSetting | null, + defaults: FooterNavigationLinks, +): FooterNavigationLinks => { + if (!setting?.customized || !setting.links) { + return defaults; + } + + return footerNavigationKeys.reduce((links, key) => { + const customized = setting.links[key]; + links[key] = { + visible: typeof customized?.visible === 'boolean' ? customized.visible : defaults[key].visible, + url: isSafeExternalUrl(customized?.url) ? customized.url : defaults[key].url, + }; + return links; + }, {} as FooterNavigationLinks); +}; diff --git a/frontend/src/components/system-upgrade/index.vue b/frontend/src/components/system-upgrade/index.vue index cb4a6bec7..a92b7b35b 100644 --- a/frontend/src/components/system-upgrade/index.vue +++ b/frontend/src/components/system-upgrade/index.vue @@ -1,53 +1,33 @@