diff --git a/core/frontend/services/theme-engine/middleware.js b/core/frontend/services/theme-engine/middleware.js deleted file mode 100644 index 5fee718f1f..0000000000 --- a/core/frontend/services/theme-engine/middleware.js +++ /dev/null @@ -1,209 +0,0 @@ -const _ = require('lodash'); -const hbs = require('./engine'); -const urlUtils = require('../../../shared/url-utils'); -const {api} = require('../proxy'); -const errors = require('@tryghost/errors'); -const tpl = require('@tryghost/tpl'); -const settingsCache = require('../../../shared/settings-cache'); -const customThemeSettingsCache = require('../../../shared/custom-theme-settings-cache'); -const labs = require('../../../shared/labs'); -const activeTheme = require('./active'); -const preview = require('./preview'); - -const messages = { - missingTheme: 'The currently active theme "{theme}" is missing.' -}; - -// ### Ensure Active Theme -// Ensure there's a properly set & mounted active theme before attempting to serve a site request -// If there is no active theme, throw an error -// Else, ensure the active theme is mounted -function ensureActiveTheme(req, res, next) { - // CASE: this means that the theme hasn't been loaded yet i.e. there is no active theme - if (!activeTheme.get()) { - // This is the one place we ACTUALLY throw an error for a missing theme as it's a request we cannot serve - return next(new errors.InternalServerError({ - // We use the settingsCache here, because the setting will be set, - // even if the theme itself is not usable because it is invalid or missing. - message: tpl(messages.missingTheme, {theme: settingsCache.get('active_theme')}) - })); - } - - // If the active theme has not yet been mounted, mount it into express - if (!activeTheme.get().mounted) { - activeTheme.get().mount(req.app); - } - - next(); -} - -function calculateLegacyPriceData(products) { - const defaultPrice = { - amount: 0, - currency: 'usd', - interval: 'year', - nickname: '' - }; - - function makePriceObject(price) { - const numberAmount = 0 + price.amount; - const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0; - return { - valueOf() { - return dollarAmount; - }, - amount: numberAmount, - currency: price.currency, - nickname: price.name, - interval: price.interval - }; - } - - const defaultProduct = products[0] || {}; - - const monthlyPrice = makePriceObject(defaultProduct.monthly_price || defaultPrice); - - const yearlyPrice = makePriceObject(defaultProduct.yearly_price || defaultPrice); - - const priceData = { - monthly: monthlyPrice, - yearly: yearlyPrice, - currency: monthlyPrice ? monthlyPrice.currency : defaultPrice.currency - }; - - return priceData; -} - -async function getProductAndPricesData() { - try { - const page = await api.canary.productsPublic.browse({ - include: ['monthly_price', 'yearly_price'], - limit: 'all' - }); - - return page.products; - } catch (err) { - return []; - } -} - -function getSiteData() { - let siteData = settingsCache.getPublic(); - - // theme-only computed property added to @site - if (settingsCache.get('members_signup_access') === 'none') { - const escapedUrl = encodeURIComponent(urlUtils.urlFor({relativeUrl: '/rss/'}, true)); - siteData.signup_url = `https://feedly.com/i/subscription/feed/${escapedUrl}`; - } else { - siteData.signup_url = '#/portal'; - } - - return siteData; -} - -async function updateGlobalTemplateOptions(req, res, next) { - // Static information, same for every request unless the settings change - // @TODO: bind this once and then update based on events? - // @TODO: decouple theme layer from settings cache using the Content API - const siteData = getSiteData(); - const labsData = labs.getAll(); - - const themeData = { - posts_per_page: activeTheme.get().config('posts_per_page'), - image_sizes: activeTheme.get().config('image_sizes') - }; - const themeSettingsData = customThemeSettingsCache.getAll(); - const productData = await getProductAndPricesData(); - const priceData = calculateLegacyPriceData(productData); - - let products = null; - let product = null; - if (productData.length === 1) { - product = productData[0]; - } else { - products = productData; - } - - // @TODO: only do this if something changed? - // @TODO: remove blog in a major where we are happy to break more themes - { - hbs.updateTemplateOptions({ - data: { - blog: siteData, - site: siteData, - labs: labsData, - config: themeData, - price: priceData, - product, - products, - custom: themeSettingsData - } - }); - } - - next(); -} - -function updateLocalTemplateData(req, res, next) { - // Pass 'secure' flag to the view engine - // so that templates can choose to render https or http 'url', see url utility - res.locals.secure = req.secure; - - next(); -} - -function updateLocalTemplateOptions(req, res, next) { - const localTemplateOptions = hbs.getLocalTemplateOptions(res.locals); - - // adjust @site.url for http/https based on the incoming request - const siteData = { - url: urlUtils.urlFor('home', {secure: req.secure, trailingSlash: false}, true) - }; - - // @TODO: it would be nicer if this was proper middleware somehow... - 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); - - const member = req.member ? { - uuid: req.member.uuid, - email: req.member.email, - name: req.member.name, - firstname: req.member.name && req.member.name.split(' ')[0], - avatar_image: req.member.avatar_image, - subscriptions: req.member.subscriptions && req.member.subscriptions.map((sub) => { - return Object.assign({}, sub, { - default_payment_card_last4: sub.default_payment_card_last4 || '****' - }); - }), - paid: req.member.status !== 'free' - } : null; - - hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, { - 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 - } - })); - - next(); -} - -module.exports = [ - ensureActiveTheme, - updateGlobalTemplateOptions, - updateLocalTemplateData, - updateLocalTemplateOptions -]; diff --git a/core/frontend/services/theme-engine/middleware/ensure-active-theme.js b/core/frontend/services/theme-engine/middleware/ensure-active-theme.js new file mode 100644 index 0000000000..998480a5df --- /dev/null +++ b/core/frontend/services/theme-engine/middleware/ensure-active-theme.js @@ -0,0 +1,34 @@ +const errors = require('@tryghost/errors'); +const tpl = require('@tryghost/tpl'); + +const activeTheme = require('../active'); +const settingsCache = require('../../../../shared/settings-cache'); + +const messages = { + missingTheme: 'The currently active theme "{theme}" is missing.' +}; + +// ### Ensure Active Theme +// Ensure there's a properly set & mounted active theme before attempting to serve a site request +// If there is no active theme, throw an error +// Else, ensure the active theme is mounted +function ensureActiveTheme(req, res, next) { + // CASE: this means that the theme hasn't been loaded yet i.e. there is no active theme + if (!activeTheme.get()) { + // This is the one place we ACTUALLY throw an error for a missing theme as it's a request we cannot serve + return next(new errors.InternalServerError({ + // We use the settingsCache here, because the setting will be set, + // even if the theme itself is not usable because it is invalid or missing. + message: tpl(messages.missingTheme, {theme: settingsCache.get('active_theme')}) + })); + } + + // If the active theme has not yet been mounted, mount it into express + if (!activeTheme.get().mounted) { + activeTheme.get().mount(req.app); + } + + next(); +} + +module.exports = ensureActiveTheme; diff --git a/core/frontend/services/theme-engine/middleware/index.js b/core/frontend/services/theme-engine/middleware/index.js new file mode 100644 index 0000000000..b8659c97e9 --- /dev/null +++ b/core/frontend/services/theme-engine/middleware/index.js @@ -0,0 +1,6 @@ +module.exports = [ + require('./ensure-active-theme'), + require('./update-global-template-options'), + require('./update-local-template-data'), + require('./update-local-template-options') +]; diff --git a/core/frontend/services/theme-engine/middleware/update-global-template-options.js b/core/frontend/services/theme-engine/middleware/update-global-template-options.js new file mode 100644 index 0000000000..dc1b62dafd --- /dev/null +++ b/core/frontend/services/theme-engine/middleware/update-global-template-options.js @@ -0,0 +1,116 @@ +const hbs = require('../engine'); +const urlUtils = require('../../../../shared/url-utils'); +const {api} = require('../../proxy'); +const settingsCache = require('../../../../shared/settings-cache'); +const customThemeSettingsCache = require('../../../../shared/custom-theme-settings-cache'); +const labs = require('../../../../shared/labs'); +const activeTheme = require('../active'); + +function calculateLegacyPriceData(products) { + const defaultPrice = { + amount: 0, + currency: 'usd', + interval: 'year', + nickname: '' + }; + + function makePriceObject(price) { + const numberAmount = 0 + price.amount; + const dollarAmount = numberAmount ? Math.round(numberAmount / 100) : 0; + return { + valueOf() { + return dollarAmount; + }, + amount: numberAmount, + currency: price.currency, + nickname: price.name, + interval: price.interval + }; + } + + const defaultProduct = products[0] || {}; + + const monthlyPrice = makePriceObject(defaultProduct.monthly_price || defaultPrice); + + const yearlyPrice = makePriceObject(defaultProduct.yearly_price || defaultPrice); + + const priceData = { + monthly: monthlyPrice, + yearly: yearlyPrice, + currency: monthlyPrice ? monthlyPrice.currency : defaultPrice.currency + }; + + return priceData; +} + +async function getProductAndPricesData() { + try { + const page = await api.canary.productsPublic.browse({ + include: ['monthly_price', 'yearly_price'], + limit: 'all' + }); + + return page.products; + } catch (err) { + return []; + } +} + +function getSiteData() { + let siteData = settingsCache.getPublic(); + + // theme-only computed property added to @site + if (settingsCache.get('members_signup_access') === 'none') { + const escapedUrl = encodeURIComponent(urlUtils.urlFor({relativeUrl: '/rss/'}, true)); + siteData.signup_url = `https://feedly.com/i/subscription/feed/${escapedUrl}`; + } else { + siteData.signup_url = '#/portal'; + } + + return siteData; +} + +async function updateGlobalTemplateOptions(req, res, next) { + // Static information, same for every request unless the settings change + // @TODO: bind this once and then update based on events? + // @TODO: decouple theme layer from settings cache using the Content API + const siteData = getSiteData(); + const labsData = labs.getAll(); + + const themeData = { + posts_per_page: activeTheme.get().config('posts_per_page'), + image_sizes: activeTheme.get().config('image_sizes') + }; + const themeSettingsData = customThemeSettingsCache.getAll(); + const productData = await getProductAndPricesData(); + const priceData = calculateLegacyPriceData(productData); + + let products = null; + let product = null; + if (productData.length === 1) { + product = productData[0]; + } else { + products = productData; + } + + // @TODO: only do this if something changed? + // @TODO: remove blog in a major where we are happy to break more themes + { + hbs.updateTemplateOptions({ + data: { + blog: siteData, + site: siteData, + labs: labsData, + config: themeData, + price: priceData, + product, + products, + custom: themeSettingsData + } + }); + } + + next(); +} + +module.exports = updateGlobalTemplateOptions; diff --git a/core/frontend/services/theme-engine/middleware/update-local-template-data.js b/core/frontend/services/theme-engine/middleware/update-local-template-data.js new file mode 100644 index 0000000000..c392a01a92 --- /dev/null +++ b/core/frontend/services/theme-engine/middleware/update-local-template-data.js @@ -0,0 +1,9 @@ +function updateLocalTemplateData(req, res, next) { + // Pass 'secure' flag to the view engine + // so that templates can choose to render https or http 'url', see url utility + res.locals.secure = req.secure; + + next(); +} + +module.exports = updateLocalTemplateData; diff --git a/core/frontend/services/theme-engine/middleware/update-local-template-options.js b/core/frontend/services/theme-engine/middleware/update-local-template-options.js new file mode 100644 index 0000000000..b01a43834e --- /dev/null +++ b/core/frontend/services/theme-engine/middleware/update-local-template-options.js @@ -0,0 +1,57 @@ +const _ = require('lodash'); +const hbs = require('../engine'); +const urlUtils = require('../../../../shared/url-utils'); +const customThemeSettingsCache = require('../../../../shared/custom-theme-settings-cache'); +const labs = require('../../../../shared/labs'); +const preview = require('../preview'); + +function updateLocalTemplateOptions(req, res, next) { + const localTemplateOptions = hbs.getLocalTemplateOptions(res.locals); + + // adjust @site.url for http/https based on the incoming request + const siteData = { + url: urlUtils.urlFor('home', {secure: req.secure, trailingSlash: false}, true) + }; + + // @TODO: it would be nicer if this was proper middleware somehow... + 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); + + const member = req.member ? { + uuid: req.member.uuid, + email: req.member.email, + name: req.member.name, + firstname: req.member.name && req.member.name.split(' ')[0], + avatar_image: req.member.avatar_image, + subscriptions: req.member.subscriptions && req.member.subscriptions.map((sub) => { + return Object.assign({}, sub, { + default_payment_card_last4: sub.default_payment_card_last4 || '****' + }); + }), + paid: req.member.status !== 'free' + } : null; + + hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, { + 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 + } + })); + + next(); +} + +module.exports = updateLocalTemplateOptions;