From 7cd4d2ba10106c10602c86f74f55743ded588896 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 9 Jul 2019 11:45:19 +0200 Subject: WIP plugins: add theme support --- server/controllers/api/config.ts | 43 +++++++++++++++-------- server/controllers/api/users/me.ts | 1 + server/helpers/custom-validators/plugins.ts | 6 ++++ server/initializers/checker-before-init.ts | 3 +- server/initializers/config.ts | 3 ++ server/initializers/constants.ts | 5 ++- server/initializers/migrations/0400-user-theme.ts | 25 +++++++++++++ server/lib/plugins/plugin-manager.ts | 34 +++++++++++++++--- server/lib/plugins/theme-utils.ts | 24 +++++++++++++ server/middlewares/validators/config.ts | 5 ++- server/middlewares/validators/plugins.ts | 2 +- server/middlewares/validators/users.ts | 4 +++ server/models/account/user.ts | 11 +++++- server/tests/api/check-params/config.ts | 3 ++ server/tests/api/server/config.ts | 3 ++ 15 files changed, 148 insertions(+), 24 deletions(-) create mode 100644 server/initializers/migrations/0400-user-theme.ts create mode 100644 server/lib/plugins/theme-utils.ts (limited to 'server') diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 8563b7437..088234074 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { snakeCase } from 'lodash' -import { ServerConfig, ServerConfigPlugin, UserRight } from '../../../shared' +import { ServerConfig, UserRight } from '../../../shared' import { About } from '../../../shared/models/server/about.model' import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' @@ -16,7 +16,7 @@ import { isNumeric } from 'validator' import { objectConverter } from '../../helpers/core-utils' import { CONFIG, reloadConfig } from '../../initializers/config' import { PluginManager } from '../../lib/plugins/plugin-manager' -import { PluginType } from '../../../shared/models/plugins/plugin.type' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' const packageJSON = require('../../../../package.json') const configRouter = express.Router() @@ -56,19 +56,23 @@ async function getConfig (req: express.Request, res: express.Response) { .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true) .map(r => parseInt(r, 10)) - const plugins: ServerConfigPlugin[] = [] const registeredPlugins = PluginManager.Instance.getRegisteredPlugins() - for (const pluginName of Object.keys(registeredPlugins)) { - const plugin = registeredPlugins[ pluginName ] - if (plugin.type !== PluginType.PLUGIN) continue - - plugins.push({ - name: plugin.name, - version: plugin.version, - description: plugin.description, - clientScripts: plugin.clientScripts - }) - } + .map(p => ({ + name: p.name, + version: p.version, + description: p.description, + clientScripts: p.clientScripts + })) + + const registeredThemes = PluginManager.Instance.getRegisteredThemes() + .map(t => ({ + name: t.name, + version: t.version, + description: t.description, + clientScripts: t.clientScripts + })) + + const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT) const json: ServerConfig = { instance: { @@ -82,7 +86,13 @@ async function getConfig (req: express.Request, res: express.Response) { css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS } }, - plugins, + plugin: { + registered: registeredPlugins + }, + theme: { + registered: registeredThemes, + default: defaultTheme + }, email: { enabled: Emailer.isEnabled() }, @@ -240,6 +250,9 @@ function customConfig (): CustomConfig { javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT } }, + theme: { + default: CONFIG.THEME.DEFAULT + }, services: { twitter: { username: CONFIG.SERVICES.TWITTER.USERNAME, diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index a078334fe..e7ed3de64 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts @@ -183,6 +183,7 @@ async function updateMe (req: express.Request, res: express.Response) { if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages + if (body.theme !== undefined) user.theme = body.theme if (body.email !== undefined) { if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts index 2fcdc581f..4ab5f9ce8 100644 --- a/server/helpers/custom-validators/plugins.ts +++ b/server/helpers/custom-validators/plugins.ts @@ -4,6 +4,7 @@ import { PluginType } from '../../../shared/models/plugins/plugin.type' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { PluginPackageJson } from '../../../shared/models/plugins/plugin-package-json.model' import { isUrlValid } from './activitypub/misc' +import { isThemeRegistered } from '../../lib/plugins/theme-utils' const PLUGINS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.PLUGINS @@ -61,6 +62,10 @@ function isCSSPathsValid (css: any[]) { return isArray(css) && css.every(c => isSafePath(c)) } +function isThemeValid (name: string) { + return isPluginNameValid(name) && isThemeRegistered(name) +} + function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginType) { return isNpmPluginNameValid(packageJSON.name) && isPluginDescriptionValid(packageJSON.description) && @@ -82,6 +87,7 @@ function isLibraryCodeValid (library: any) { export { isPluginTypeValid, isPackageJSONValid, + isThemeValid, isPluginVersionValid, isPluginNameValid, isPluginDescriptionValid, diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 1f5ec20df..c94bca2f8 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts @@ -29,7 +29,8 @@ function checkMissedConfig () { 'followers.instance.enabled', 'followers.instance.manual_approval', 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces', 'history.videos.max_age', 'views.videos.remote.max_age', - 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max' + 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', + 'theme.default' ] const requiredAlternatives = [ [ // set diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 6737edcd6..dfc4bea21 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -224,6 +224,9 @@ const CONFIG = { get ENABLED () { return config.get('followers.instance.enabled') }, get MANUAL_APPROVAL () { return config.get('followers.instance.manual_approval') } } + }, + THEME: { + get DEFAULT () { return config.get('theme.default') } } } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 8ceefbd0e..9d61ed537 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 395 +const LAST_MIGRATION_VERSION = 400 // --------------------------------------------------------------------------- @@ -585,6 +585,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2 const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) +const DEFAULT_THEME = 'default' + // --------------------------------------------------------------------------- // Special constants for a test instance @@ -667,6 +669,7 @@ export { HLS_STREAMING_PLAYLIST_DIRECTORY, FEEDS, JOB_TTL, + DEFAULT_THEME, NSFW_POLICY_TYPES, STATIC_MAX_AGE, STATIC_PATHS, diff --git a/server/initializers/migrations/0400-user-theme.ts b/server/initializers/migrations/0400-user-theme.ts new file mode 100644 index 000000000..2c1763890 --- /dev/null +++ b/server/initializers/migrations/0400-user-theme.ts @@ -0,0 +1,25 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + const data = { + type: Sequelize.STRING, + allowNull: false, + defaultValue: 'default' + } + + await utils.queryInterface.addColumn('user', 'theme', data) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts index 7cbfa8569..8496979f8 100644 --- a/server/lib/plugins/plugin-manager.ts +++ b/server/lib/plugins/plugin-manager.ts @@ -11,6 +11,7 @@ import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants' import { PluginType } from '../../../shared/models/plugins/plugin.type' import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' import { outputFile } from 'fs-extra' +import { ServerConfigPlugin } from '../../../shared/models/server' export interface RegisteredPlugin { name: string @@ -47,7 +48,7 @@ export class PluginManager { private constructor () { } - async registerPlugins () { + async registerPluginsAndThemes () { await this.resetCSSGlobalFile() const plugins = await PluginModel.listEnabledPluginsAndThemes() @@ -63,12 +64,20 @@ export class PluginManager { this.sortHooksByPriority() } + getRegisteredPluginOrTheme (name: string) { + return this.registeredPlugins[name] + } + getRegisteredPlugin (name: string) { - return this.registeredPlugins[ name ] + const registered = this.getRegisteredPluginOrTheme(name) + + if (!registered || registered.type !== PluginType.PLUGIN) return undefined + + return registered } getRegisteredTheme (name: string) { - const registered = this.getRegisteredPlugin(name) + const registered = this.getRegisteredPluginOrTheme(name) if (!registered || registered.type !== PluginType.THEME) return undefined @@ -76,7 +85,11 @@ export class PluginManager { } getRegisteredPlugins () { - return this.registeredPlugins + return this.getRegisteredPluginsOrThemes(PluginType.PLUGIN) + } + + getRegisteredThemes () { + return this.getRegisteredPluginsOrThemes(PluginType.THEME) } async runHook (hookName: string, param?: any) { @@ -309,6 +322,19 @@ export class PluginManager { } } + private getRegisteredPluginsOrThemes (type: PluginType) { + const plugins: RegisteredPlugin[] = [] + + for (const pluginName of Object.keys(this.registeredPlugins)) { + const plugin = this.registeredPlugins[ pluginName ] + if (plugin.type !== type) continue + + plugins.push(plugin) + } + + return plugins + } + static get Instance () { return this.instance || (this.instance = new this()) } diff --git a/server/lib/plugins/theme-utils.ts b/server/lib/plugins/theme-utils.ts new file mode 100644 index 000000000..066339e65 --- /dev/null +++ b/server/lib/plugins/theme-utils.ts @@ -0,0 +1,24 @@ +import { DEFAULT_THEME } from '../../initializers/constants' +import { PluginManager } from './plugin-manager' +import { CONFIG } from '../../initializers/config' + +function getThemeOrDefault (name: string) { + if (isThemeRegistered(name)) return name + + // Fallback to admin default theme + if (name !== CONFIG.THEME.DEFAULT) return getThemeOrDefault(CONFIG.THEME.DEFAULT) + + return DEFAULT_THEME +} + +function isThemeRegistered (name: string) { + if (name === DEFAULT_THEME) return true + + return !!PluginManager.Instance.getRegisteredThemes() + .find(r => r.name === name) +} + +export { + getThemeOrDefault, + isThemeRegistered +} diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index d015fa6fe..31b131914 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -1,10 +1,11 @@ import * as express from 'express' import { body } from 'express-validator/check' -import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } from '../../helpers/custom-validators/users' +import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' import { logger } from '../../helpers/logger' import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { Emailer } from '../../lib/emailer' import { areValidationErrors } from './utils' +import { isThemeValid } from '../../helpers/custom-validators/plugins' const customConfigUpdateValidator = [ body('instance.name').exists().withMessage('Should have a valid instance name'), @@ -47,6 +48,8 @@ const customConfigUpdateValidator = [ body('followers.instance.enabled').isBoolean().withMessage('Should have a valid followers of instance boolean'), body('followers.instance.manualApproval').isBoolean().withMessage('Should have a valid manual approval boolean'), + body('theme.default').custom(isThemeValid).withMessage('Should have a valid theme'), + async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 672299ee1..fcb461624 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts @@ -16,7 +16,7 @@ const servePluginStaticDirectoryValidator = [ if (areValidationErrors(req, res)) return - const plugin = PluginManager.Instance.getRegisteredPlugin(req.params.pluginName) + const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(req.params.pluginName) if (!plugin || plugin.version !== req.params.pluginVersion) { return res.sendStatus(404) diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 947ed36c3..df7f77b84 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -28,6 +28,7 @@ import { ActorModel } from '../../models/activitypub/actor' import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' import { isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' import { UserRegister } from '../../../shared/models/users/user-register.model' +import { isThemeValid } from '../../helpers/custom-validators/plugins' const usersAddValidator = [ body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), @@ -204,6 +205,9 @@ const usersUpdateMeValidator = [ body('videosHistoryEnabled') .optional() .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), + body('theme') + .optional() + .custom(isThemeValid).withMessage('Should have a valid theme'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 0f425bb82..b8ca1dd5c 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -44,7 +44,7 @@ import { VideoChannelModel } from '../video/video-channel' import { AccountModel } from './account' import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' import { values } from 'lodash' -import { NSFW_POLICY_TYPES } from '../../initializers/constants' +import { DEFAULT_THEME, NSFW_POLICY_TYPES } from '../../initializers/constants' import { clearCacheByUserId } from '../../lib/oauth-model' import { UserNotificationSettingModel } from './user-notification-setting' import { VideoModel } from '../video/video' @@ -52,6 +52,8 @@ import { ActorModel } from '../activitypub/actor' import { ActorFollowModel } from '../activitypub/actor-follow' import { VideoImportModel } from '../video/video-import' import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' +import { isThemeValid } from '../../helpers/custom-validators/plugins' +import { getThemeOrDefault } from '../../lib/plugins/theme-utils' enum ScopeNames { WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' @@ -187,6 +189,12 @@ export class UserModel extends Model { @Column(DataType.BIGINT) videoQuotaDaily: number + @AllowNull(false) + @Default(DEFAULT_THEME) + @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme')) + @Column + theme: string + @CreatedAt createdAt: Date @@ -560,6 +568,7 @@ export class UserModel extends Model { autoPlayVideo: this.autoPlayVideo, videoLanguages: this.videoLanguages, role: this.role, + theme: getThemeOrDefault(this.theme), roleLabel: USER_ROLE_LABELS[ this.role ], videoQuota: this.videoQuota, videoQuotaDaily: this.videoQuotaDaily, diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index a0d9392dc..7773ae1e7 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -27,6 +27,9 @@ describe('Test config API validators', function () { css: 'body { background-color: red; }' } }, + theme: { + default: 'default' + }, services: { twitter: { username: '@MySuperUsername', diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index c39516dee..78fdc9cc0 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -190,6 +190,9 @@ describe('Test config', function () { css: 'body { background-color: red; }' } }, + theme: { + default: 'default' + }, services: { twitter: { username: '@Kuja', -- cgit v1.2.3