diff --git a/core/frontend/services/theme-engine/middleware.js b/core/frontend/services/theme-engine/middleware.js index cd7fd5ce2e..5fee718f1f 100644 --- a/core/frontend/services/theme-engine/middleware.js +++ b/core/frontend/services/theme-engine/middleware.js @@ -161,7 +161,15 @@ function updateLocalTemplateOptions(req, res, next) { }; // @TODO: it would be nicer if this was proper middleware somehow... - const previewData = preview.handle(req); + const previewData = preview.handle(req, Object.keys(customThemeSettingsCache.getAll())); + + // strip custom off of preview data so it doesn't get merged into @site + const customThemeSettingsPreviewData = previewData.custom; + delete previewData.custom; + let customData = {}; + if (labs.isSet('customThemeSettings')) { + customData = customThemeSettingsPreviewData; + } // update site data with any preview values from the request Object.assign(siteData, previewData); @@ -184,6 +192,7 @@ function updateLocalTemplateOptions(req, res, next) { data: { member: member, site: siteData, + custom: customData, // @deprecated: a gscan warning for @blog was added before 3.0 which replaced it with @site blog: siteData } diff --git a/core/frontend/services/theme-engine/preview.js b/core/frontend/services/theme-engine/preview.js index 76c8d06332..178d732b9e 100644 --- a/core/frontend/services/theme-engine/preview.js +++ b/core/frontend/services/theme-engine/preview.js @@ -14,13 +14,14 @@ function decodeValue(value) { return value; } -function getPreviewData(previewHeader) { +function getPreviewData(previewHeader, customThemeSettingKeys = []) { // Keep the string shorter with short codes for certain parameters const supportedSettings = { c: 'accent_color', icon: 'icon', logo: 'logo', - cover: 'cover_image' + cover: 'cover_image', + custom: 'custom' }; let opts = new URLSearchParams(previewHeader); @@ -34,17 +35,34 @@ function getPreviewData(previewHeader) { } }); + if (previewData.custom) { + try { + const custom = {}; + const previewCustom = JSON.parse(previewData.custom); + + if (typeof previewCustom === 'object') { + customThemeSettingKeys.forEach((key) => { + custom[key] = previewCustom[key]; + }); + } + + previewData.custom = custom; + } catch (e) { + previewData.custom = {}; + } + } + previewData._preview = previewHeader; return previewData; } module.exports._PREVIEW_HEADER_NAME = PREVIEW_HEADER_NAME; -module.exports.handle = (req) => { +module.exports.handle = (req, customThemeSettingKeys) => { let previewData = {}; if (req && req.header(PREVIEW_HEADER_NAME)) { - previewData = getPreviewData(req.header(PREVIEW_HEADER_NAME)); + previewData = getPreviewData(req.header(PREVIEW_HEADER_NAME), customThemeSettingKeys); } return previewData; diff --git a/test/unit/services/theme-engine/middleware.test.js b/test/unit/services/theme-engine/middleware.test.js index a239183c40..e4961f1577 100644 --- a/test/unit/services/theme-engine/middleware.test.js +++ b/test/unit/services/theme-engine/middleware.test.js @@ -5,6 +5,7 @@ const middleware = require('../../../../core/frontend/services/theme-engine').mi // is only exposed via themeEngine.getActive() const activeTheme = require('../../../../core/frontend/services/theme-engine/active'); const settingsCache = require('../../../../core/shared/settings-cache'); +const customThemeSettingsCache = require('../../../../core/shared/custom-theme-settings-cache'); const sandbox = sinon.createSandbox(); @@ -34,6 +35,7 @@ describe('Themes middleware', function () { let fakeActiveThemeName; let fakeSiteData; let fakeLabsData; + let fakeCustomThemeSettingsData; beforeEach(function () { req = {app: {}, header: () => {}}; @@ -52,7 +54,12 @@ describe('Themes middleware', function () { // labs data is deep cloned, // if we want to compare it // we will need some unique content - members: true + members: true, + customThemeSettings: true + }; + + fakeCustomThemeSettingsData = { + header_typography: 'Sans-Serif' }; sandbox.stub(activeTheme, 'get') @@ -65,6 +72,9 @@ describe('Themes middleware', function () { sandbox.stub(settingsCache, 'getPublic') .returns(fakeSiteData); + sandbox.stub(customThemeSettingsCache, 'getAll') + .returns(fakeCustomThemeSettingsData); + sandbox.stub(hbs, 'updateTemplateOptions'); sandbox.stub(hbs, 'updateLocalTemplateOptions'); }); @@ -151,7 +161,7 @@ describe('Themes middleware', function () { const templateOptions = hbs.updateTemplateOptions.firstCall.args[0]; const data = templateOptions.data; - data.should.be.an.Object().with.properties('site', 'labs', 'config'); + data.should.be.an.Object().with.properties('site', 'labs', 'config', 'custom'); // Check Theme Config data.config.should.be.an.Object() @@ -167,6 +177,8 @@ describe('Themes middleware', function () { should.exist(data.site.signup_url); data.site.signup_url.should.equal('#/portal'); + should.deepEqual(data.custom, fakeCustomThemeSettingsData); + done(); } catch (error) { done(error); @@ -249,5 +261,113 @@ describe('Themes middleware', function () { } }); }); + + it('calls updateLocalTemplateOptions with correct custom theme settings data', function (done) { + const customPreviewObject = {header_typography: 'Serif'}; + const previewString = `custom=${encodeURIComponent(JSON.stringify(customPreviewObject))}`; + req.header = () => { + return previewString; + }; + + executeMiddleware(middleware, req, res, function next(err) { + try { + should.not.exist(err); + + hbs.updateLocalTemplateOptions.calledOnce.should.be.true(); + const templateOptions = hbs.updateLocalTemplateOptions.firstCall.args[1]; + const data = templateOptions.data; + + data.should.be.an.Object().with.properties('site', 'custom'); + + data.custom.should.be.an.Object().with.properties('header_typography'); + data.custom.header_typography.should.eql('Serif'); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it('calls updateLocalTemplateOptions with correct without unknown custom theme settings', function (done) { + const customPreviewObject = {header_typography: 'Serif', unknown_setting: true}; + const previewString = `custom=${encodeURIComponent(JSON.stringify(customPreviewObject))}`; + req.header = () => { + return previewString; + }; + + executeMiddleware(middleware, req, res, function next(err) { + try { + should.not.exist(err); + + hbs.updateLocalTemplateOptions.calledOnce.should.be.true(); + const templateOptions = hbs.updateLocalTemplateOptions.firstCall.args[1]; + const data = templateOptions.data; + + data.should.be.an.Object().with.properties('site', 'custom'); + + data.custom.should.be.an.Object().with.properties('header_typography'); + data.custom.header_typography.should.eql('Serif'); + + data.custom.should.not.have.property('unknown_setting'); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it('calls updateLocalTemplateOptions with no custom data when custom param is not parseable JSON', function (done) { + const previewString = `custom=${encodeURIComponent('')}`; + req.header = () => { + return previewString; + }; + + executeMiddleware(middleware, req, res, function next(err) { + try { + should.not.exist(err); + + hbs.updateLocalTemplateOptions.calledOnce.should.be.true(); + const templateOptions = hbs.updateLocalTemplateOptions.firstCall.args[1]; + const data = templateOptions.data; + + data.should.be.an.Object().with.properties('site', 'custom'); + + data.custom.should.be.an.Object(); + data.custom.should.be.empty(); + + done(); + } catch (error) { + done(error); + } + }); + }); + + it('calls updateLocalTemplateOptions with no custom data when custom param is not an object', function (done) { + const previewString = `custom=${encodeURIComponent('"header_typography"')}`; + req.header = () => { + return previewString; + }; + + executeMiddleware(middleware, req, res, function next(err) { + try { + should.not.exist(err); + + hbs.updateLocalTemplateOptions.calledOnce.should.be.true(); + const templateOptions = hbs.updateLocalTemplateOptions.firstCall.args[1]; + const data = templateOptions.data; + + data.should.be.an.Object().with.properties('site', 'custom'); + + data.custom.should.be.an.Object(); + data.custom.should.be.empty(); + + done(); + } catch (error) { + done(error); + } + }); + }); }); });