From cf6324cf367a8967d199b81289084007e66482f6 Mon Sep 17 00:00:00 2001 From: interpreterwork Date: Sat, 21 Mar 2026 01:34:29 +0000 Subject: [PATCH 1/3] Fix office theme persistence from host theme query --- editors/desktop-stub.js | 41 ++++++++++++++++++++++-------- editors/offline-loader-proper.html | 10 +++++++- server.js | 4 +++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/editors/desktop-stub.js b/editors/desktop-stub.js index d039ecc..f2b7b05 100644 --- a/editors/desktop-stub.js +++ b/editors/desktop-stub.js @@ -117,6 +117,35 @@ return ''; } + function detectThemeConfig() { + var params = new URLSearchParams(window.location.search || ''); + var theme = (params.get('theme') || 'system').toLowerCase(); + + if (theme === 'dark') { + return { + id: 'theme-night', + type: 'dark', + system: 'dark' + }; + } + + if (theme === 'light') { + return { + id: 'theme-classic-light', + type: 'light', + system: 'light' + }; + } + + return { + id: 'theme-system', + type: 'light', + system: 'light' + }; + } + + var initialThemeConfig = detectThemeConfig(); + function shouldUseEastAsiaVariant() { var lang = detectLanguageCode(); if (!lang && window.Common && window.Common.Locale && typeof window.Common.Locale.getCurrentLanguage === 'function') { @@ -264,11 +293,7 @@ }, // Theme support - theme: { - id: 'theme-classic-light', - type: 'light', - system: 'light' - }, + theme: { ...initialThemeConfig }, // Crypto plugin support (stub) CryptoMode: 0, @@ -1657,11 +1682,7 @@ // Create RendererProcessVariable stub for theme and RTL support window.RendererProcessVariable = { - theme: { - id: 'theme-classic-light', - type: 'light', - system: 'light' - }, + theme: { ...initialThemeConfig }, localthemes: {}, rtl: false }; diff --git a/editors/offline-loader-proper.html b/editors/offline-loader-proper.html index 991c6ae..bf2fed7 100644 --- a/editors/offline-loader-proper.html +++ b/editors/offline-loader-proper.html @@ -412,8 +412,16 @@ return docparams; } + function resolveUiTheme(urlParams) { + var themeParam = (urlParams['theme'] || 'system').toLowerCase(); + if (themeParam === 'dark') return 'theme-night'; + if (themeParam === 'light') return 'theme-classic-light'; + return 'theme-system'; + } + function getEditorConfig(urlParams) { var headerLogoUrl = window.location.origin + '/web-apps/apps/common/main/resources/img/header/header-logo_s.svg'; + var uiTheme = resolveUiTheme(urlParams); return { customization : { goback: { url: "onlyoffice.com" }, @@ -423,7 +431,7 @@ hideRightMenu: false, showReviewChanges: false, toolbarNoTabs: false, - uiTheme: 'theme-classic-light', // Prevent theme switching dialogs + uiTheme: uiTheme, autosave: false, // Disable autosave completely logo: { visible: true, diff --git a/server.js b/server.js index 7222773..6fea37f 100644 --- a/server.js +++ b/server.js @@ -864,6 +864,7 @@ app.get('/open', (req, res) => { const filepath = req.query.filepath; // NOTE(victor): OnlyOffice expects ISO 639-1 codes (e.g. "es", "fr"), not full names like "spanish" const lang = typeof req.query.lang === 'string' ? req.query.lang : ''; + const theme = typeof req.query.theme === 'string' ? req.query.theme : ''; if (!filepath) { return res.status(400).json({ error: 'filepath query parameter is required' }); @@ -896,6 +897,9 @@ app.get('/open', (req, res) => { if (lang) { redirectParams.set('lang', lang); } + if (theme) { + redirectParams.set('theme', theme); + } res.redirect(`/offline-loader-proper.html?${redirectParams.toString()}`); }); From 875ccc91349bdcb4351a1b2f23689d756e13e615 Mon Sep 17 00:00:00 2001 From: "Victor A." <52110451+cs50victor@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:36:37 -0700 Subject: [PATCH 2/3] fix: remove theme-system fallback --- __tests__/desktop-stub-utils.test.js | 64 ++++++++++++++++++++++++++++ editors/desktop-stub-utils.js | 55 ++++++++++++++++++++++++ editors/desktop-stub.js | 38 +++++++---------- editors/offline-loader-proper.html | 14 ++++-- 4 files changed, 145 insertions(+), 26 deletions(-) diff --git a/__tests__/desktop-stub-utils.test.js b/__tests__/desktop-stub-utils.test.js index a6a01ea..768aa78 100644 --- a/__tests__/desktop-stub-utils.test.js +++ b/__tests__/desktop-stub-utils.test.js @@ -2,6 +2,9 @@ import { describe, test, expect } from 'bun:test'; import { parseSpriteScale, detectLanguageCode, + normalizeThemeChoice, + resolveUiThemeId, + buildThemeConfig, shouldUseEastAsiaVariant, getSpriteOptions, collectFontNames, @@ -72,6 +75,67 @@ describe('detectLanguageCode', () => { }); }); +describe('normalizeThemeChoice', () => { + test('defaults to light for empty or unknown values', () => { + expect(normalizeThemeChoice(undefined)).toBe('light'); + expect(normalizeThemeChoice(null)).toBe('light'); + expect(normalizeThemeChoice('')).toBe('light'); + expect(normalizeThemeChoice('system')).toBe('light'); + expect(normalizeThemeChoice('theme-system')).toBe('light'); + expect(normalizeThemeChoice('unknown')).toBe('light'); + }); + + test('accepts light aliases', () => { + expect(normalizeThemeChoice('light')).toBe('light'); + expect(normalizeThemeChoice('theme-classic-light')).toBe('light'); + expect(normalizeThemeChoice('theme-white')).toBe('light'); + }); + + test('accepts dark aliases', () => { + expect(normalizeThemeChoice('dark')).toBe('dark'); + expect(normalizeThemeChoice('theme-night')).toBe('dark'); + expect(normalizeThemeChoice('theme-dark')).toBe('dark'); + }); +}); + +describe('resolveUiThemeId', () => { + test('returns explicit ONLYOFFICE theme ids', () => { + expect(resolveUiThemeId('dark')).toBe('theme-night'); + expect(resolveUiThemeId('theme-night')).toBe('theme-night'); + expect(resolveUiThemeId('light')).toBe('theme-classic-light'); + expect(resolveUiThemeId('theme-system')).toBe('theme-classic-light'); + expect(resolveUiThemeId(undefined)).toBe('theme-classic-light'); + }); +}); + +describe('buildThemeConfig', () => { + test('returns explicit light theme config by default', () => { + expect(buildThemeConfig(undefined)).toEqual({ + id: 'theme-classic-light', + type: 'light', + system: 'light' + }); + expect(buildThemeConfig('theme-system')).toEqual({ + id: 'theme-classic-light', + type: 'light', + system: 'light' + }); + }); + + test('returns explicit dark theme config for dark values', () => { + expect(buildThemeConfig('dark')).toEqual({ + id: 'theme-night', + type: 'dark', + system: 'dark' + }); + expect(buildThemeConfig('theme-night')).toEqual({ + id: 'theme-night', + type: 'dark', + system: 'dark' + }); + }); +}); + describe('shouldUseEastAsiaVariant', () => { test('returns false for empty/null lang', () => { expect(shouldUseEastAsiaVariant('')).toBe(false); diff --git a/editors/desktop-stub-utils.js b/editors/desktop-stub-utils.js index ea5df80..bb16959 100644 --- a/editors/desktop-stub-utils.js +++ b/editors/desktop-stub-utils.js @@ -45,6 +45,58 @@ function detectLanguageCode(location) { return ''; } +/** + * Normalize incoming theme values to the two supported modes. + * Accepts either host-friendly names ("dark"/"light") or ONLYOFFICE theme ids. + * Any unknown value falls back to light. + * @param {string} theme - Theme query parameter or theme id + * @returns {"dark"|"light"} Normalized theme choice + */ +function normalizeThemeChoice(theme) { + var normalized = typeof theme === 'string' ? theme.toLowerCase() : ''; + if ( + normalized === 'dark' || + normalized === 'theme-night' || + normalized === 'theme-dark' + ) { + return 'dark'; + } + return 'light'; +} + +/** + * Resolve the ONLYOFFICE uiTheme id for an incoming theme value. + * @param {string} theme - Theme query parameter or theme id + * @returns {string} ONLYOFFICE theme id + */ +function resolveUiThemeId(theme) { + return normalizeThemeChoice(theme) === 'dark' + ? 'theme-night' + : 'theme-classic-light'; +} + +/** + * Build the desktop stub theme object expected by the web apps. + * @param {string} theme - Theme query parameter or theme id + * @returns {{id: string, type: string, system: string}} Theme config + */ +function buildThemeConfig(theme) { + var normalized = normalizeThemeChoice(theme); + if (normalized === 'dark') { + return { + id: 'theme-night', + type: 'dark', + system: 'dark' + }; + } + + return { + id: 'theme-classic-light', + type: 'light', + system: 'light' + }; +} + /** * Check if East Asian font variant should be used * @param {string} lang - Language code @@ -168,6 +220,9 @@ function extractBlobUrl(path) { FONT_SPRITE_ROW_HEIGHT: FONT_SPRITE_ROW_HEIGHT, parseSpriteScale: parseSpriteScale, detectLanguageCode: detectLanguageCode, + normalizeThemeChoice: normalizeThemeChoice, + resolveUiThemeId: resolveUiThemeId, + buildThemeConfig: buildThemeConfig, shouldUseEastAsiaVariant: shouldUseEastAsiaVariant, getSpriteOptions: getSpriteOptions, collectFontNames: collectFontNames, diff --git a/editors/desktop-stub.js b/editors/desktop-stub.js index f2b7b05..67ebd14 100644 --- a/editors/desktop-stub.js +++ b/editors/desktop-stub.js @@ -109,19 +109,9 @@ var extractMediaFilename = utils.extractMediaFilename || function(p) { return p; }; var buildMediaUrl = utils.buildMediaUrl || function(base, hash, name) { return hash ? base + '/api/media/' + hash + '/' + name : null; }; var extractBlobUrl = utils.extractBlobUrl || function(p) { return null; }; - - function detectLanguageCode() { - if (utils.detectLanguageCode) { - return utils.detectLanguageCode(window.location); - } - return ''; - } - - function detectThemeConfig() { - var params = new URLSearchParams(window.location.search || ''); - var theme = (params.get('theme') || 'system').toLowerCase(); - - if (theme === 'dark') { + var buildThemeConfig = utils.buildThemeConfig || function(theme) { + var normalized = typeof theme === 'string' ? theme.toLowerCase() : ''; + if (normalized === 'dark' || normalized === 'theme-night' || normalized === 'theme-dark') { return { id: 'theme-night', type: 'dark', @@ -129,19 +119,23 @@ }; } - if (theme === 'light') { - return { - id: 'theme-classic-light', - type: 'light', - system: 'light' - }; - } - return { - id: 'theme-system', + id: 'theme-classic-light', type: 'light', system: 'light' }; + }; + + function detectLanguageCode() { + if (utils.detectLanguageCode) { + return utils.detectLanguageCode(window.location); + } + return ''; + } + + function detectThemeConfig() { + var params = new URLSearchParams(window.location.search || ''); + return buildThemeConfig(params.get('theme')); } var initialThemeConfig = detectThemeConfig(); diff --git a/editors/offline-loader-proper.html b/editors/offline-loader-proper.html index bf2fed7..3be8eff 100644 --- a/editors/offline-loader-proper.html +++ b/editors/offline-loader-proper.html @@ -413,10 +413,16 @@ } function resolveUiTheme(urlParams) { - var themeParam = (urlParams['theme'] || 'system').toLowerCase(); - if (themeParam === 'dark') return 'theme-night'; - if (themeParam === 'light') return 'theme-classic-light'; - return 'theme-system'; + if (window.DesktopStubUtils && typeof window.DesktopStubUtils.resolveUiThemeId === 'function') { + return window.DesktopStubUtils.resolveUiThemeId(urlParams['theme']); + } + + var themeParam = typeof urlParams['theme'] === 'string' + ? urlParams['theme'].toLowerCase() + : ''; + return (themeParam === 'dark' || themeParam === 'theme-night' || themeParam === 'theme-dark') + ? 'theme-night' + : 'theme-classic-light'; } function getEditorConfig(urlParams) { From faebe7a62e6d7b51309c12f82d55b23cbd65775b Mon Sep 17 00:00:00 2001 From: "Victor A." <52110451+cs50victor@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:15:38 -0700 Subject: [PATCH 3/3] fix: inherit theme query in editor iframe --- __tests__/desktop-stub-utils.test.js | 31 +++++++++++++++++++ editors/desktop-stub-utils.js | 39 +++++++++++++++++++++++ editors/desktop-stub.js | 46 ++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/__tests__/desktop-stub-utils.test.js b/__tests__/desktop-stub-utils.test.js index 768aa78..9d51bca 100644 --- a/__tests__/desktop-stub-utils.test.js +++ b/__tests__/desktop-stub-utils.test.js @@ -2,6 +2,8 @@ import { describe, test, expect } from 'bun:test'; import { parseSpriteScale, detectLanguageCode, + extractThemeQueryValue, + findThemeChoice, normalizeThemeChoice, resolveUiThemeId, buildThemeConfig, @@ -75,6 +77,35 @@ describe('detectLanguageCode', () => { }); }); +describe('extractThemeQueryValue', () => { + test('returns empty string for empty or invalid input', () => { + expect(extractThemeQueryValue(undefined)).toBe(''); + expect(extractThemeQueryValue(null)).toBe(''); + expect(extractThemeQueryValue('')).toBe(''); + }); + + test('extracts theme from query string', () => { + expect(extractThemeQueryValue('?theme=dark')).toBe('dark'); + expect(extractThemeQueryValue('?foo=bar&theme=theme-night')).toBe('theme-night'); + }); +}); + +describe('findThemeChoice', () => { + test('returns first matching theme from location chain', () => { + expect(findThemeChoice([ + '', + '?foo=bar', + '?theme=dark', + '?theme=light' + ])).toBe('dark'); + }); + + test('returns empty string when no theme exists', () => { + expect(findThemeChoice(['', '?foo=bar'])).toBe(''); + expect(findThemeChoice(null)).toBe(''); + }); +}); + describe('normalizeThemeChoice', () => { test('defaults to light for empty or unknown values', () => { expect(normalizeThemeChoice(undefined)).toBe('light'); diff --git a/editors/desktop-stub-utils.js b/editors/desktop-stub-utils.js index bb16959..d7edc05 100644 --- a/editors/desktop-stub-utils.js +++ b/editors/desktop-stub-utils.js @@ -45,6 +45,43 @@ function detectLanguageCode(location) { return ''; } +/** + * Extract theme query parameter from a search string. + * @param {string} search - URL search string + * @returns {string} Theme query value or empty string + */ +function extractThemeQueryValue(search) { + try { + var searchValue = typeof search === 'string' ? search : ''; + if (!searchValue) { + return ''; + } + return new URLSearchParams(searchValue).get('theme') || ''; + } catch (err) { + return ''; + } +} + +/** + * Find the first theme value from a list of search strings. + * @param {Array} searches - Ordered search strings to inspect + * @returns {string} First non-empty theme query value or empty string + */ +function findThemeChoice(searches) { + if (!Array.isArray(searches)) { + return ''; + } + + for (var i = 0; i < searches.length; i++) { + var theme = extractThemeQueryValue(searches[i]); + if (theme) { + return theme; + } + } + + return ''; +} + /** * Normalize incoming theme values to the two supported modes. * Accepts either host-friendly names ("dark"/"light") or ONLYOFFICE theme ids. @@ -220,6 +257,8 @@ function extractBlobUrl(path) { FONT_SPRITE_ROW_HEIGHT: FONT_SPRITE_ROW_HEIGHT, parseSpriteScale: parseSpriteScale, detectLanguageCode: detectLanguageCode, + extractThemeQueryValue: extractThemeQueryValue, + findThemeChoice: findThemeChoice, normalizeThemeChoice: normalizeThemeChoice, resolveUiThemeId: resolveUiThemeId, buildThemeConfig: buildThemeConfig, diff --git a/editors/desktop-stub.js b/editors/desktop-stub.js index 67ebd14..a0242c8 100644 --- a/editors/desktop-stub.js +++ b/editors/desktop-stub.js @@ -109,6 +109,27 @@ var extractMediaFilename = utils.extractMediaFilename || function(p) { return p; }; var buildMediaUrl = utils.buildMediaUrl || function(base, hash, name) { return hash ? base + '/api/media/' + hash + '/' + name : null; }; var extractBlobUrl = utils.extractBlobUrl || function(p) { return null; }; + var findThemeChoice = utils.findThemeChoice || function(searches) { + if (!Array.isArray(searches)) { + return ''; + } + + for (var i = 0; i < searches.length; i++) { + var searchValue = typeof searches[i] === 'string' ? searches[i] : ''; + if (!searchValue) { + continue; + } + + try { + var theme = new URLSearchParams(searchValue).get('theme'); + if (theme) { + return theme; + } + } catch (err) {} + } + + return ''; + }; var buildThemeConfig = utils.buildThemeConfig || function(theme) { var normalized = typeof theme === 'string' ? theme.toLowerCase() : ''; if (normalized === 'dark' || normalized === 'theme-night' || normalized === 'theme-dark') { @@ -133,9 +154,30 @@ return ''; } + function collectLocationSearches() { + var searches = []; + var currentWindow = window; + var visitedWindows = []; + + while (currentWindow && visitedWindows.indexOf(currentWindow) === -1) { + visitedWindows.push(currentWindow); + + try { + searches.push(currentWindow.location && currentWindow.location.search ? currentWindow.location.search : ''); + if (!currentWindow.parent || currentWindow.parent === currentWindow) { + break; + } + currentWindow = currentWindow.parent; + } catch (err) { + break; + } + } + + return searches; + } + function detectThemeConfig() { - var params = new URLSearchParams(window.location.search || ''); - return buildThemeConfig(params.get('theme')); + return buildThemeConfig(findThemeChoice(collectLocationSearches())); } var initialThemeConfig = detectThemeConfig();