From 1ddfa9be1557b72273ca573e921244c0f8db38f5 Mon Sep 17 00:00:00 2001 From: taiyme <53635909+taiyme@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:48:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(tms):=20=E3=82=B5=E3=83=BC=E3=83=90?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0CSS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 12 ++++ locales/ja-JP.yml | 3 + .../1732244400000-tmsServerCustomCss.js | 16 +++++ .../src/core/entities/MetaEntityService.ts | 1 + packages/backend/src/models/Meta.ts | 6 ++ .../backend/src/models/json-schema/meta.ts | 4 ++ .../src/server/api/endpoints/admin/meta.ts | 5 ++ .../server/api/endpoints/admin/update-meta.ts | 5 ++ packages/backend/src/server/web/boot.embed.js | 6 +- packages/backend/src/server/web/boot.js | 26 +++++--- packages/frontend/src/_dev_boot_.ts | 18 ++++-- packages/frontend/src/boot/common.ts | 9 +++ packages/frontend/src/components/MkRange.vue | 8 +-- .../frontend/src/pages/admin/branding.vue | 12 ++++ .../src/scripts/tms/apply-custom-css.ts | 60 +++++++++++++++++++ packages/frontend/src/style.scss | 12 ++++ packages/misskey-js/src/autogen/types.ts | 3 + 17 files changed, 185 insertions(+), 21 deletions(-) create mode 100644 packages/backend/migration/1732244400000-tmsServerCustomCss.js create mode 100644 packages/frontend/src/scripts/tms/apply-custom-css.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index ff9b3bfb5c44..3b7060bb5c13 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10614,6 +10614,10 @@ export interface Locale extends ILocale { * taiymeについて */ readonly "aboutTaiyme": string; + /** + * taiyme限定 + */ + readonly "taiymeOnly": string; /** * taiyme限定機能 */ @@ -10809,6 +10813,14 @@ export interface Locale extends ILocale { }; }; readonly "_admin": { + /** + * サーバーカスタムCSS + */ + readonly "serverCustomCss": string; + /** + * この設定は必ず知識のある方が行ってください。不適切な設定を行うと、管理者を含むすべてのユーザーのクライアントが正常に使用できなくなる恐れがあります。 + */ + readonly "serverCustomCssDescription": string; /** * ソースコードが公開されているリポジトリがある場合、そのURLを記入します。taiymeを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/taiyme/misskey と記入します。 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2925731b40c5..1b7186a2759a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2831,6 +2831,7 @@ _tms: taiy: "taiy" taiyme: "taiyme" aboutTaiyme: "taiymeについて" + taiymeOnly: "taiyme限定" taiymeFeatures: "taiyme限定機能" taiymeFeaturesDescription: "これらの機能はtaiymeで独自実装したものです。" poweredByTaiyme: "{name}は、Misskeyの派生であるtaiymeを使用したサーバーのひとつです。" @@ -2884,6 +2885,8 @@ _tms: label: "長押しによるコンテキストメニューイベントの発行を防ぐ" caption: "長押しを含む操作が中断される問題を解消します。" _admin: + serverCustomCss: "サーバーカスタムCSS" + serverCustomCssDescription: "この設定は必ず知識のある方が行ってください。不適切な設定を行うと、管理者を含むすべてのユーザーのクライアントが正常に使用できなくなる恐れがあります。" repositoryUrlDescription: "ソースコードが公開されているリポジトリがある場合、そのURLを記入します。taiymeを現状のまま(ソースコードにいかなる変更も加えずに)使用している場合は https://github.com/taiyme/misskey と記入します。" _search: searchScopeAll: "全て" diff --git a/packages/backend/migration/1732244400000-tmsServerCustomCss.js b/packages/backend/migration/1732244400000-tmsServerCustomCss.js new file mode 100644 index 000000000000..507e2c1ff87c --- /dev/null +++ b/packages/backend/migration/1732244400000-tmsServerCustomCss.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class TmsServerCustomCss1732244400000 { + name = 'TmsServerCustomCss1732244400000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "tmsServerCustomCss" character varying(8192)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "tmsServerCustomCss"`); + } +} diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 8f138bea17a8..5613f85d4428 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -110,6 +110,7 @@ export class MetaEntityService { maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, defaultLightTheme, defaultDarkTheme, + tmsServerCustomCss: instance.tmsServerCustomCss, ads: ads.map(ad => ({ id: ad.id, url: ad.url, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 9a27c8e7a63a..311e16758393 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -409,6 +409,12 @@ export class MiMeta { }) public defaultDarkTheme: string | null; + @Column('varchar', { + length: 8192, + nullable: true, + }) + public tmsServerCustomCss: string | null; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index c92f099d5ad3..ee9bfcef8a93 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -71,6 +71,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + tmsServerCustomCss: { + type: 'string', + optional: false, nullable: true, + }, disableRegistration: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 595bfb2c050e..e6d845ec8263 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -420,6 +420,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + tmsServerCustomCss: { + type: 'string', + optional: false, nullable: true, + }, description: { type: 'string', optional: false, nullable: true, @@ -591,6 +595,7 @@ export default class extends Endpoint { // eslint- logoImageUrl: instance.logoImageUrl, defaultLightTheme: instance.defaultLightTheme, defaultDarkTheme: instance.defaultDarkTheme, + tmsServerCustomCss: instance.tmsServerCustomCss, enableEmail: instance.enableEmail, enableServiceWorker: instance.enableServiceWorker, translatorAvailable: instance.deeplAuthKey != null, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 38ef0d1de837..c0c013873811 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -67,6 +67,7 @@ export const paramDef = { description: { type: 'string', nullable: true }, defaultLightTheme: { type: 'string', nullable: true }, defaultDarkTheme: { type: 'string', nullable: true }, + tmsServerCustomCss: { type: 'string', nullable: true }, cacheRemoteFiles: { type: 'boolean' }, cacheRemoteSensitiveFiles: { type: 'boolean' }, emailRequiredForSignup: { type: 'boolean' }, @@ -303,6 +304,10 @@ export default class extends Endpoint { // eslint- set.defaultDarkTheme = ps.defaultDarkTheme; } + if (ps.tmsServerCustomCss !== undefined) { + set.tmsServerCustomCss = ps.tmsServerCustomCss; + } + if (ps.cacheRemoteFiles !== undefined) { set.cacheRemoteFiles = ps.cacheRemoteFiles; } diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js index 924ee5d469ef..d30aface103c 100644 --- a/packages/backend/src/server/web/boot.embed.js +++ b/packages/backend/src/server/web/boot.embed.js @@ -106,9 +106,9 @@ //#endregion async function addStyle(styleText) { - let css = document.createElement('style'); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); + const styleTag = document.createElement('style'); + styleTag.textContent = styleText; + document.head.appendChild(styleTag); } async function renderError(code) { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 17f53423e4c4..46367adacd16 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -/* global window, document, localStorage, navigator, console, LANGS, CLIENT_ENTRY */ +/* global window, document, localStorage, navigator, console, URLSearchParams, LANGS, CLIENT_ENTRY */ 'use strict'; @@ -134,17 +134,25 @@ document.documentElement.style.backgroundImage = `url(${wallpaper})`; } - const customCss = localStorage.getItem('customCss'); - if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + const availableCustomCss = (() => { + const params = new URLSearchParams(window.location.search); + return !params.has('disableCustomCss') && !params.has('disablecustomcss'); + })(); + + if (availableCustomCss) { + const customCss = localStorage.getItem('customCss'); + if (customCss && customCss.length > 0) { + const styleTag = document.createElement('style'); + styleTag.id = 'user_custom_css'; + styleTag.textContent = customCss; + document.head.appendChild(styleTag); + } } async function addStyle(styleText) { - let css = document.createElement('style'); - css.appendChild(document.createTextNode(styleText)); - document.head.appendChild(css); + const styleTag = document.createElement('style'); + styleTag.textContent = styleText; + document.head.appendChild(styleTag); } async function renderError(code, details) { diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts index f312765dcf40..6550e22ad346 100644 --- a/packages/frontend/src/_dev_boot_.ts +++ b/packages/frontend/src/_dev_boot_.ts @@ -77,11 +77,19 @@ async function main() { document.documentElement.style.backgroundImage = `url(${wallpaper})`; } - const customCss = localStorage.getItem('customCss'); - if (customCss && customCss.length > 0) { - const style = document.createElement('style'); - style.innerHTML = customCss; - document.head.appendChild(style); + const availableCustomCss = (() => { + const params = new URLSearchParams(window.location.search); + return !params.has('disableCustomCss') && !params.has('disablecustomcss'); + })(); + + if (availableCustomCss) { + const customCss = localStorage.getItem('customCss'); + if (customCss && customCss.length > 0) { + const styleTag = document.createElement('style'); + styleTag.id = 'user_custom_css'; + styleTag.textContent = customCss; + document.head.appendChild(styleTag); + } } } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 9c40d1fbff8b..5718b0a4673e 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -26,6 +26,7 @@ import { setupRouter } from '@/router/main.js'; import { createMainRouter } from '@/router/definition.js'; import { tmsFlaskStore } from '@/tms/flask-store.js'; import { tmsStore } from '@/tms/store.js'; +import { applyCustomCss } from '@/scripts/tms/apply-custom-css.js'; import { preventLongPressContextMenu } from '@/scripts/tms/prevent-longpress-contextmenu.js'; export async function common(createVue: () => App) { @@ -135,6 +136,14 @@ export async function common(createVue: () => App) { miLocalStorage.setItem('v', instance.version); }); + //#region tmsServerCustomCss + fetchInstanceMetaPromise.then(() => { + applyCustomCss({ + serverCustomCss: instance.tmsServerCustomCss, + }); + }); + //#endregion + //#region loginId const params = new URLSearchParams(location.search); const loginId = params.get('loginId'); diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue index ad921c9d3f05..bdf555a9e906 100644 --- a/packages/frontend/src/components/MkRange.vue +++ b/packages/frontend/src/components/MkRange.vue @@ -151,9 +151,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) { closed: () => dispose(), }); - const style = document.createElement('style'); - style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); - document.head.appendChild(style); + const styleTag = document.createElement('style'); + styleTag.textContent = '* { cursor: grabbing !important; } body * { pointer-events: none !important; }'; + document.head.appendChild(styleTag); const thumbWidth = getThumbWidth(); @@ -172,7 +172,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) { let beforeValue = finalValue.value; const onMouseup = () => { - document.head.removeChild(style); + document.head.removeChild(styleTag); tooltipForDragShowing.value = false; window.removeEventListener('mousemove', onDrag); window.removeEventListener('touchmove', onDrag); diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index eb1ce9e9dce5..43eae2e23364 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -88,6 +88,14 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -114,6 +122,7 @@ import { instance, fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; +import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkColorInput from '@/components/MkColorInput.vue'; const iconUrl = ref(null); @@ -124,6 +133,7 @@ const backgroundImageUrl = ref(null); const themeColor = ref(null); const defaultLightTheme = ref(null); const defaultDarkTheme = ref(null); +const tmsServerCustomCss = ref(null); const serverErrorImageUrl = ref(null); const infoImageUrl = ref(null); const notFoundImageUrl = ref(null); @@ -141,6 +151,7 @@ async function init() { themeColor.value = meta.themeColor; defaultLightTheme.value = meta.defaultLightTheme; defaultDarkTheme.value = meta.defaultDarkTheme; + tmsServerCustomCss.value = meta.tmsServerCustomCss; serverErrorImageUrl.value = meta.serverErrorImageUrl; infoImageUrl.value = meta.infoImageUrl; notFoundImageUrl.value = meta.notFoundImageUrl; @@ -159,6 +170,7 @@ function save() { themeColor: themeColor.value === '' ? null : themeColor.value, defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value, defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value, + tmsServerCustomCss: tmsServerCustomCss.value === '' ? null : tmsServerCustomCss.value, infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value, notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value, serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value, diff --git a/packages/frontend/src/scripts/tms/apply-custom-css.ts b/packages/frontend/src/scripts/tms/apply-custom-css.ts new file mode 100644 index 000000000000..b80604a620ce --- /dev/null +++ b/packages/frontend/src/scripts/tms/apply-custom-css.ts @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { url as configUrl } from '@@/js/config.js'; + +const availableCustomCss = (() => { + const params = new URLSearchParams(window.location.search); + return !params.has('disableCustomCss') && !params.has('disablecustomcss'); +})(); + +export function applyCustomCss({ serverCustomCss, userCustomCss }: { + readonly serverCustomCss?: string | null; + readonly userCustomCss?: string | null; +}) { + if (!availableCustomCss) return; + if (serverCustomCss === undefined && userCustomCss === undefined) return; + + let serverStyleTag = document.getElementById('server_custom_css'); + if (serverCustomCss !== undefined) { + if (serverCustomCss === null || serverCustomCss === '') { + serverStyleTag?.remove(); + serverStyleTag = null; + } else { + if (serverStyleTag == null) { + serverStyleTag = document.createElement('style'); + serverStyleTag.id = 'server_custom_css'; + } + serverStyleTag.textContent = serverCustomCss; + } + } + + let userStyleTag = document.getElementById('user_custom_css'); + if (userCustomCss !== undefined) { + if (userCustomCss === null || userCustomCss === '') { + userStyleTag?.remove(); + userStyleTag = null; + } else { + if (userStyleTag == null) { + userStyleTag = document.createElement('style'); + userStyleTag.id = 'user_custom_css'; + } + userStyleTag.textContent = userCustomCss; + } + } + + if (serverStyleTag != null) { + document.head.appendChild(serverStyleTag); + } + if (userStyleTag != null) { + document.head.appendChild(userStyleTag); + } + + if (serverStyleTag != null || userStyleTag != null) { + const url = `${configUrl}/?disableCustomCss`; + console.warn(`Custom CSS has been applied. To temporarily disable it, please visit ${url}.`); + console.warn(`カスタムCSSが適用されました。一時的に無効にするには ${url} にアクセスします。`); + } +} diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 516b75fd7376..ee0a49a2c87f 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -398,6 +398,18 @@ rt { line-height: 1; } +._taiymeOnly { + margin-left: 0.7em; + font-size: 65%; + padding: 3px 4px; + color: #fff; + background-color: #1c3c70; + border-radius: 4px; + vertical-align: top; + display: inline-block; + line-height: 1; +} + ._table { > ._row { display: flex; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index adfb470c272c..c79e0baecd3a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4973,6 +4973,7 @@ export type components = { feedbackUrl: string | null; defaultDarkTheme: string | null; defaultLightTheme: string | null; + tmsServerCustomCss: string | null; disableRegistration: boolean; emailRequiredForSignup: boolean; enableHcaptcha: boolean; @@ -5195,6 +5196,7 @@ export type operations = { deeplIsPro: boolean; defaultDarkTheme: string | null; defaultLightTheme: string | null; + tmsServerCustomCss: string | null; description: string | null; disableRegistration: boolean; impressumUrl: string | null; @@ -9508,6 +9510,7 @@ export type operations = { description?: string | null; defaultLightTheme?: string | null; defaultDarkTheme?: string | null; + tmsServerCustomCss?: string | null; cacheRemoteFiles?: boolean; cacheRemoteSensitiveFiles?: boolean; emailRequiredForSignup?: boolean;