mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
🐛 Fixed theme preview not decoding values properly
- Theme preview was not showing the same behaviour as a real theme because nulls were being encoded and decoded incorrectly causing nulls/empty strings to be treasted as truthy values - Swap from using split to using proper query param parsing so that the code is more robust - this still creates empty strings and the string 'null' so added a small function to decode these back to real nulls - moved to its own file ready to be split out - there needs to be a bigger picture plan for this - added unit tests to cover the known issues + some potential breakages from converting the header string to a query param object
This commit is contained in:
parent
4a939054ba
commit
1c7c246616
3 changed files with 105 additions and 32 deletions
|
@ -7,6 +7,7 @@ const errors = require('@tryghost/errors');
|
||||||
const settingsCache = require('../../../server/services/settings/cache');
|
const settingsCache = require('../../../server/services/settings/cache');
|
||||||
const labs = require('../../../server/services/labs');
|
const labs = require('../../../server/services/labs');
|
||||||
const activeTheme = require('./active');
|
const activeTheme = require('./active');
|
||||||
|
const preview = require('./preview');
|
||||||
|
|
||||||
// ### Ensure Active Theme
|
// ### Ensure Active Theme
|
||||||
// Ensure there's a properly set & mounted active theme before attempting to serve a site request
|
// Ensure there's a properly set & mounted active theme before attempting to serve a site request
|
||||||
|
@ -75,41 +76,11 @@ function haxGetMembersPriceData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The preview header contains a query string with the custom preview data
|
|
||||||
// This is deliberately slightly obscure & means we don't need to add body parsing to the frontend :D
|
|
||||||
// If we start passing in strings like title or description we will probably need to change this
|
|
||||||
const PREVIEW_HEADER_NAME = 'x-ghost-preview';
|
|
||||||
|
|
||||||
function handlePreview(previewHeader, siteData) {
|
|
||||||
// Keep the string shorter with short codes for certain parameters
|
|
||||||
const supportedSettings = {
|
|
||||||
c: 'accent_color',
|
|
||||||
icon: 'icon',
|
|
||||||
logo: 'logo',
|
|
||||||
cover: 'cover_image'
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO: change this to use a proper query string parser - maybe build a fake url and use the url lib
|
|
||||||
let opts = decodeURIComponent(previewHeader).split('&');
|
|
||||||
|
|
||||||
opts.forEach((opt) => {
|
|
||||||
let [key, value] = opt.split('=');
|
|
||||||
if (supportedSettings[key]) {
|
|
||||||
_.set(siteData, supportedSettings[key], value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
siteData._preview = previewHeader;
|
|
||||||
|
|
||||||
return siteData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSiteData(req) {
|
function getSiteData(req) {
|
||||||
let siteData = settingsCache.getPublic();
|
let siteData = settingsCache.getPublic();
|
||||||
|
|
||||||
if (req.header(PREVIEW_HEADER_NAME)) {
|
// @TODO: it would be nicer if this was proper middleware somehow...
|
||||||
siteData = handlePreview(req.header(PREVIEW_HEADER_NAME), siteData);
|
siteData = preview.handle(req, siteData);
|
||||||
}
|
|
||||||
|
|
||||||
return siteData;
|
return siteData;
|
||||||
}
|
}
|
||||||
|
|
47
core/frontend/services/themes/preview.js
Normal file
47
core/frontend/services/themes/preview.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// @TODO: put together a plan for how the frontend should exist as modules and move this out
|
||||||
|
// The preview header contains a query string with the custom preview data
|
||||||
|
// This is deliberately slightly obscure & means we don't need to add body parsing to the frontend :D
|
||||||
|
// If we start passing in strings like title or description we will probably need to change this
|
||||||
|
const PREVIEW_HEADER_NAME = 'x-ghost-preview';
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
function decodeValue(value) {
|
||||||
|
if (value === '' || value === 'null' || value === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviewData(previewHeader, siteData) {
|
||||||
|
// Keep the string shorter with short codes for certain parameters
|
||||||
|
const supportedSettings = {
|
||||||
|
c: 'accent_color',
|
||||||
|
icon: 'icon',
|
||||||
|
logo: 'logo',
|
||||||
|
cover: 'cover_image'
|
||||||
|
};
|
||||||
|
|
||||||
|
let opts = new URLSearchParams(previewHeader);
|
||||||
|
|
||||||
|
opts.forEach((value, key) => {
|
||||||
|
value = decodeValue(value);
|
||||||
|
if (supportedSettings[key]) {
|
||||||
|
_.set(siteData, supportedSettings[key], value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
siteData._preview = previewHeader;
|
||||||
|
|
||||||
|
return siteData;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports._PREVIEW_HEADER_NAME = PREVIEW_HEADER_NAME;
|
||||||
|
module.exports.handle = (req, siteData) => {
|
||||||
|
if (req && req.header(PREVIEW_HEADER_NAME)) {
|
||||||
|
siteData = getPreviewData(req.header(PREVIEW_HEADER_NAME), siteData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return siteData;
|
||||||
|
};
|
55
test/unit/services/themes/preview_spec.js
Normal file
55
test/unit/services/themes/preview_spec.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const should = require('should');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const preview = require('../../../../core/frontend/services/themes/preview');
|
||||||
|
|
||||||
|
describe('Theme Preview', function () {
|
||||||
|
let req, previewString = '';
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
req = {
|
||||||
|
header: () => {
|
||||||
|
return previewString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle empty strings', function () {
|
||||||
|
previewString = 'logo=';
|
||||||
|
|
||||||
|
let siteData = preview.handle(req, {});
|
||||||
|
|
||||||
|
siteData.should.be.an.Object().with.properties('logo');
|
||||||
|
should(siteData.logo).be.null();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle nulls', function () {
|
||||||
|
previewString = 'cover=null';
|
||||||
|
|
||||||
|
let siteData = preview.handle(req, {});
|
||||||
|
|
||||||
|
siteData.should.be.an.Object().with.properties('cover_image');
|
||||||
|
should(siteData.cover_image).be.null();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle URIEncoded accent colors', function () {
|
||||||
|
previewString = 'c=%23f02d2d';
|
||||||
|
|
||||||
|
let siteData = preview.handle(req, {});
|
||||||
|
|
||||||
|
siteData.should.be.an.Object().with.properties('accent_color');
|
||||||
|
should(siteData.accent_color).eql('#f02d2d');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle multiple values', function () {
|
||||||
|
previewString = 'c=%23f02d2d&icon=&logo=&cover=null';
|
||||||
|
|
||||||
|
let siteData = preview.handle(req, {});
|
||||||
|
siteData.should.be.an.Object().with.properties('accent_color', 'icon', 'logo', 'cover_image');
|
||||||
|
|
||||||
|
should(siteData.accent_color).eql('#f02d2d');
|
||||||
|
should(siteData.icon).be.null();
|
||||||
|
should(siteData.logo).be.null();
|
||||||
|
should(siteData.cover_image).be.null();
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue