Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11864,6 +11864,10 @@ const docTemplate = `{
"schema.SiteCustomCssHTMLReq": {
"type": "object",
"properties": {
"custom_banner": {
"type": "string",
"maxLength": 65536
},
"custom_css": {
"type": "string",
"maxLength": 65536
Expand All @@ -11889,6 +11893,10 @@ const docTemplate = `{
"schema.SiteCustomCssHTMLResp": {
"type": "object",
"properties": {
"custom_banner": {
"type": "string",
"maxLength": 65536
},
"custom_css": {
"type": "string",
"maxLength": 65536
Expand Down
8 changes: 8 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -11837,6 +11837,10 @@
"schema.SiteCustomCssHTMLReq": {
"type": "object",
"properties": {
"custom_banner": {
"type": "string",
"maxLength": 65536
},
"custom_css": {
"type": "string",
"maxLength": 65536
Expand All @@ -11862,6 +11866,10 @@
"schema.SiteCustomCssHTMLResp": {
"type": "object",
"properties": {
"custom_banner": {
"type": "string",
"maxLength": 65536
},
"custom_css": {
"type": "string",
"maxLength": 65536
Expand Down
6 changes: 6 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,9 @@ definitions:
type: object
schema.SiteCustomCssHTMLReq:
properties:
custom_banner:
maxLength: 65536
type: string
custom_css:
maxLength: 65536
type: string
Expand All @@ -2371,6 +2374,9 @@ definitions:
type: object
schema.SiteCustomCssHTMLResp:
properties:
custom_banner:
maxLength: 65536
type: string
custom_css:
maxLength: 65536
type: string
Expand Down
3 changes: 3 additions & 0 deletions i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,9 @@ ui:
header:
label: Header
text: This will insert after <body>
banner:
label: Banner
text: This will insert below the site header.
footer:
label: Footer
text: This will insert before </body>.
Expand Down
1 change: 1 addition & 0 deletions internal/schema/siteinfo_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ type SiteCustomCssHTMLReq struct {
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
CustomBanner string `validate:"omitempty,gt=0,lte=65536" json:"custom_banner"`
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"`
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/common/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ export interface AdminSettingsCustom {
custom_css: string;
custom_head: string;
custom_header: string;
custom_banner: string;
custom_footer: string;
custom_sidebar: string;
}
Expand Down
82 changes: 82 additions & 0 deletions ui/src/components/CustomBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { memo, useEffect, useRef } from 'react';

import { customizeStore } from '@/stores';

/**
* Re-create every script node in the container, because the browser runs a
* script only when it is inserted as a node. dangerouslySetInnerHTML parses
* script tags without running them.
*/
const activateScriptNodes = (el: HTMLElement) => {
el.querySelectorAll('script').forEach((so) => {
const script = document.createElement('script');
// Wrap a classic script, to keep its declarations out of the page scope.
// Copy every other script as it is: a module has its own scope, and a data
// block such as application/ld+json must keep its content. The test matches
// the types the browser runs.
const type = so.getAttribute('type')?.trim().toLowerCase();
const isClassic =
!type || /^(text|application)\/(x-)?(java|ecma)script$/.test(type);
// The line breaks matter: a script that ends in a line comment would
// otherwise swallow the closing brace.
script.text = isClassic ? `(() => {\n${so.text}\n})();` : so.text;
for (let i = 0; i < so.attributes.length; i += 1) {
const attr = so.attributes[i];
script.setAttribute(attr.name, attr.value);
}
so.parentNode?.replaceChild(script, so);
});
};

const Index = () => {
const customBanner = customizeStore((state) => state.custom_banner);
const slotRef = useRef<HTMLDivElement>(null);
// The content whose scripts already ran. It stops the scripts from running
// again when React re-runs this effect for the same content.
const activatedBanner = useRef<string | null>(null);

useEffect(() => {
if (!customBanner) {
activatedBanner.current = null;
return;
}
if (!slotRef.current || customBanner === activatedBanner.current) {
return;
}
activatedBanner.current = customBanner;
activateScriptNodes(slotRef.current);
}, [customBanner]);

if (!customBanner) {
return null;
}

return (
<div
id="custom-banner-slot"
ref={slotRef}
dangerouslySetInnerHTML={{ __html: customBanner }}
/>
);
};

export default memo(Index);
2 changes: 2 additions & 0 deletions ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import QuestionList from './QuestionList';
import HotQuestions from './HotQuestions';
import HttpErrorContent from './HttpErrorContent';
import CustomSidebar from './CustomSidebar';
import CustomBanner from './CustomBanner';
import ImgViewer from './ImgViewer';
import SideNav from './SideNav';
import PluginRender from './PluginRender';
Expand Down Expand Up @@ -111,6 +112,7 @@ export {
HotQuestions,
HttpErrorContent,
CustomSidebar,
CustomBanner,
ImgViewer,
SideNav,
PluginRender,
Expand Down
14 changes: 14 additions & 0 deletions ui/src/pages/Admin/CssAndHtml/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const Index: FC = () => {
title: t('header.label'),
description: t('header.text'),
},
custom_banner: {
type: 'string',
title: t('banner.label'),
description: t('banner.text'),
},
custom_sidebar: {
type: 'string',
title: t('sidebar.label'),
Expand Down Expand Up @@ -84,6 +89,13 @@ const Index: FC = () => {
className: ['small', 'font-monospace'],
},
},
custom_banner: {
'ui:widget': 'textarea',
'ui:options': {
rows: 10,
className: ['small', 'font-monospace'],
},
},
custom_sidebar: {
'ui:widget': 'textarea',
'ui:options': {
Expand All @@ -108,6 +120,7 @@ const Index: FC = () => {
custom_css: formData.custom_css.value,
custom_head: formData.custom_head.value,
custom_header: formData.custom_header.value,
custom_banner: formData.custom_banner.value,
custom_sidebar: formData.custom_sidebar.value,
custom_footer: formData.custom_footer.value,
};
Expand Down Expand Up @@ -137,6 +150,7 @@ const Index: FC = () => {
formMeta.custom_css.value = setting.custom_css;
formMeta.custom_head.value = setting.custom_head;
formMeta.custom_header.value = setting.custom_header;
formMeta.custom_banner.value = setting.custom_banner;
formMeta.custom_sidebar.value = setting.custom_sidebar;
formMeta.custom_footer.value = setting.custom_footer;
setFormData(formMeta);
Expand Down
2 changes: 2 additions & 0 deletions ui/src/pages/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '@/stores';
import {
Header,
CustomBanner,
Toast,
Customize,
CustomizeTheme,
Expand Down Expand Up @@ -211,6 +212,7 @@ const Layout: FC = () => {
revalidateOnFocus: false,
}}>
<Header />
<CustomBanner />
<div
className={classnames(
'position-relative page-wrap d-flex flex-column flex-fill',
Expand Down
3 changes: 3 additions & 0 deletions ui/src/stores/customize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ interface IType {
custom_css: string;
custom_head: string;
custom_header: string;
custom_banner: string;
custom_footer: string;
custom_sidebar: string;
update: (params: {
custom_css?: string;
custom_head?: string;
custom_header?: string;
custom_banner?: string;
custom_footer?: string;
custom_sidebar?: string;
}) => void;
Expand All @@ -38,6 +40,7 @@ const loginSetting = create<IType>((set) => ({
custom_css: '',
custom_head: '',
custom_header: '',
custom_banner: '',
custom_footer: '',
custom_sidebar: '',
update: (params) =>
Expand Down