Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split RSS module into controller + other #9224

Merged
merged 9 commits into from
Nov 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions core/server/controllers/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,16 @@ var _ = require('lodash'),

module.exports = function channelController(req, res, next) {
// Parse the parameters we need from the URL
var channelOpts = res.locals.channel,
pageParam = req.params.page !== undefined ? req.params.page : 1,
var pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug ? safeString(req.params.slug) : undefined;

// Ensure we at least have an empty object for postOptions
channelOpts.postOptions = channelOpts.postOptions || {};
// @TODO: fix this, we shouldn't change the channel object!
// Set page on postOptions for the query made later
channelOpts.postOptions.page = pageParam;
channelOpts.slugParam = slugParam;
res.locals.channel.postOptions.page = pageParam;
res.locals.channel.slugParam = slugParam;

// Call fetchData to get everything we need from the API
return fetchData(channelOpts).then(function handleResult(result) {
return fetchData(res.locals.channel).then(function handleResult(result) {
// If page is greater than number of pages we have, go straight to 404
if (pageParam > result.meta.pagination.pages) {
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
Expand Down
16 changes: 16 additions & 0 deletions core/server/data/xml/rss/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var crypto = require('crypto'),
generateFeed = require('./generate-feed'),
feedCache = {};

module.exports.getXML = function getFeedXml(path, data) {
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
if (!feedCache[path] || feedCache[path].hash !== dataHash) {
// We need to regenerate
feedCache[path] = {
hash: dataHash,
xml: generateFeed(data)
};
}

return feedCache[path].xml;
};
83 changes: 83 additions & 0 deletions core/server/data/xml/rss/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
var _ = require('lodash'),
url = require('url'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
safeString = require('../../../utils/index').safeString,
settingsCache = require('../../../settings/cache'),

// Really ugly temporary hack for location of things
fetchData = require('../../../controllers/frontend/fetch-data'),
handleError = require('../../../controllers/frontend/error'),

feedCache = require('./cache'),
generate;

// @TODO: is this the right logic? Where should this live?!
function getBaseUrlForRSSReq(originalUrl, pageParam) {
return url.parse(originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/');
}

// @TODO: is this really correct? Should we be using meta data title?
function getTitle(relatedData) {
relatedData = relatedData || {};
var titleStart = _.get(relatedData, 'author[0].name') || _.get(relatedData, 'tag[0].name') || '';

titleStart += titleStart ? ' - ' : '';
return titleStart + settingsCache.get('title');
}

// @TODO: merge this with the rest of the data processing for RSS
// @TODO: swap the fetchData call + duplicate code from channels with something DRY
function getData(channelOpts) {
channelOpts.data = channelOpts.data || {};

return fetchData(channelOpts).then(function formatResult(result) {
var response = {};

response.title = getTitle(result.data);
response.description = settingsCache.get('description');
response.results = {
posts: result.posts,
meta: result.meta
};

return response;
});
}

// @TODO finish refactoring this - it's now a controller
generate = function generate(req, res, next) {
// Parse the parameters we need from the URL
var pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug ? safeString(req.params.slug) : undefined;

// @TODO: fix this, we shouldn't change the channel object!
// Set page on postOptions for the query made later
res.locals.channel.postOptions.page = pageParam;
res.locals.channel.slugParam = slugParam;

return getData(res.locals.channel).then(function handleResult(data) {
// Base URL needs to be the URL for the feed without pagination:
var baseUrl = getBaseUrlForRSSReq(req.originalUrl, pageParam),
maxPage = data.results.meta.pagination.pages;

// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
}

data.version = res.locals.safeVersion;
data.siteUrl = utils.url.urlFor('home', {secure: req.secure}, true);
data.feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.secure = req.secure;

// @TODO this is effectively a renderer
return feedCache.getXML(baseUrl, data).then(function then(feedXml) {
res.set('Content-Type', 'text/xml; charset=UTF-8');
res.send(feedXml);
});
}).catch(handleError(next));
};

module.exports = generate;
87 changes: 87 additions & 0 deletions core/server/data/xml/rss/generate-feed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
var downsize = require('downsize'),
RSS = require('rss'),
utils = require('../../../utils'),
filters = require('../../../filters'),
processUrls = require('../../../utils/make-absolute-urls'),

generateFeed,
generateTags;

generateTags = function generateTags(data) {
if (data.tags) {
return data.tags.reduce(function (tags, tag) {
if (tag.visibility !== 'internal') {
tags.push(tag.name);
}
return tags;
}, []);
}

return [];
};

generateFeed = function generateFeed(data) {
var feed = new RSS({
title: data.title,
description: data.description,
generator: 'Ghost ' + data.version,
feed_url: data.feedUrl,
site_url: data.siteUrl,
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
ttl: '60',
custom_namespaces: {
content: 'http://purl.org/rss/1.0/modules/content/',
media: 'http://search.yahoo.com/mrss/'
}
});

data.results.posts.forEach(function forEach(post) {
var itemUrl = utils.url.urlFor('post', {post: post, secure: data.secure}, true),
htmlContent = processUrls(post.html, data.siteUrl, itemUrl),
item = {
title: post.title,
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
guid: post.id,
url: itemUrl,
date: post.published_at,
categories: generateTags(post),
author: post.author ? post.author.name : null,
custom_elements: []
},
imageUrl;

if (post.feature_image) {
imageUrl = utils.url.urlFor('image', {image: post.feature_image, secure: data.secure}, true);

// Add a media content tag
item.custom_elements.push({
'media:content': {
_attr: {
url: imageUrl,
medium: 'image'
}
}
});

// Also add the image to the content, because not all readers support media:content
htmlContent('p').first().before('<img src="' + imageUrl + '" />');
htmlContent('img').attr('alt', post.title);
}

item.custom_elements.push({
'content:encoded': {
_cdata: htmlContent.html()
}
});

filters.doFilter('rss.item', item, post).then(function then(item) {
feed.item(item);
});
});

return filters.doFilter('rss.feed', feed).then(function then(feed) {
return feed.xml();
});
};

module.exports = generateFeed;
174 changes: 1 addition & 173 deletions core/server/data/xml/rss/index.js
Original file line number Diff line number Diff line change
@@ -1,173 +1 @@
var crypto = require('crypto'),
downsize = require('downsize'),
RSS = require('rss'),
url = require('url'),
utils = require('../../../utils'),
errors = require('../../../errors'),
i18n = require('../../../i18n'),
filters = require('../../../filters'),
processUrls = require('../../../utils/make-absolute-urls'),
settingsCache = require('../../../settings/cache'),

// Really ugly temporary hack for location of things
fetchData = require('../../../controllers/frontend/fetch-data'),

generate,
generateFeed,
generateTags,
getFeedXml,
feedCache = {};

function handleError(next) {
return function handleError(err) {
return next(err);
};
}

function getData(channelOpts, slugParam) {
channelOpts.data = channelOpts.data || {};

return fetchData(channelOpts, slugParam).then(function (result) {
var response = {},
titleStart = '';

if (result.data && result.data.tag) { titleStart = result.data.tag[0].name + ' - ' || ''; }
if (result.data && result.data.author) { titleStart = result.data.author[0].name + ' - ' || ''; }

response.title = titleStart + settingsCache.get('title');
response.description = settingsCache.get('description');
response.results = {
posts: result.posts,
meta: result.meta
};

return response;
});
}

getFeedXml = function getFeedXml(path, data) {
var dataHash = crypto.createHash('md5').update(JSON.stringify(data)).digest('hex');
if (!feedCache[path] || feedCache[path].hash !== dataHash) {
// We need to regenerate
feedCache[path] = {
hash: dataHash,
xml: generateFeed(data)
};
}

return feedCache[path].xml;
};

generateTags = function generateTags(data) {
if (data.tags) {
return data.tags.reduce(function (tags, tag) {
if (tag.visibility !== 'internal') {
tags.push(tag.name);
}
return tags;
}, []);
}

return [];
};

generateFeed = function generateFeed(data) {
var feed = new RSS({
title: data.title,
description: data.description,
generator: 'Ghost ' + data.version,
feed_url: data.feedUrl,
site_url: data.siteUrl,
image_url: utils.url.urlFor({relativeUrl: 'favicon.png'}, true),
ttl: '60',
custom_namespaces: {
content: 'http://purl.org/rss/1.0/modules/content/',
media: 'http://search.yahoo.com/mrss/'
}
});

data.results.posts.forEach(function forEach(post) {
var itemUrl = utils.url.urlFor('post', {post: post, secure: data.secure}, true),
htmlContent = processUrls(post.html, data.siteUrl, itemUrl),
item = {
title: post.title,
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
guid: post.id,
url: itemUrl,
date: post.published_at,
categories: generateTags(post),
author: post.author ? post.author.name : null,
custom_elements: []
},
imageUrl;

if (post.feature_image) {
imageUrl = utils.url.urlFor('image', {image: post.feature_image, secure: data.secure}, true);

// Add a media content tag
item.custom_elements.push({
'media:content': {
_attr: {
url: imageUrl,
medium: 'image'
}
}
});

// Also add the image to the content, because not all readers support media:content
htmlContent('p').first().before('<img src="' + imageUrl + '" />');
htmlContent('img').attr('alt', post.title);
}

item.custom_elements.push({
'content:encoded': {
_cdata: htmlContent.html()
}
});

filters.doFilter('rss.item', item, post).then(function then(item) {
feed.item(item);
});
});

return filters.doFilter('rss.feed', feed).then(function then(feed) {
return feed.xml();
});
};

generate = function generate(req, res, next) {
// Initialize RSS
var pageParam = req.params.page !== undefined ? req.params.page : 1,
slugParam = req.params.slug,
// Base URL needs to be the URL for the feed without pagination:
baseUrl = url.parse(req.originalUrl).pathname.replace(new RegExp('/' + pageParam + '/$'), '/'),
channelConfig = res.locals.channel;

// Ensure we at least have an empty object for postOptions
channelConfig.postOptions = channelConfig.postOptions || {};
// Set page on postOptions for the query made later
channelConfig.postOptions.page = pageParam;

channelConfig.slugParam = slugParam;

return getData(channelConfig).then(function then(data) {
var maxPage = data.results.meta.pagination.pages;

// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return next(new errors.NotFoundError({message: i18n.t('errors.errors.pageNotFound')}));
}

data.version = res.locals.safeVersion;
data.siteUrl = utils.url.urlFor('home', {secure: req.secure}, true);
data.feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.secure = req.secure;

return getFeedXml(baseUrl, data).then(function then(feedXml) {
res.set('Content-Type', 'text/xml; charset=UTF-8');
res.send(feedXml);
});
}).catch(handleError(next));
};

module.exports = generate;
module.exports = require('./controller');
Loading