2014-01-03 19:50:03 +04:00
|
|
|
// # Update Checking Service
|
|
|
|
//
|
|
|
|
// Makes a request to Ghost.org to check if there is a new version of Ghost available.
|
2015-05-28 18:16:09 +03:00
|
|
|
// The service is provided in return for users opting in to anonymous usage data collection.
|
|
|
|
//
|
|
|
|
// Blog owners can opt-out of update checks by setting `privacy: { useUpdateCheck: false }` in their config.js
|
2014-01-03 19:50:03 +04:00
|
|
|
//
|
|
|
|
// The data collected is as follows:
|
2015-05-28 18:16:09 +03:00
|
|
|
//
|
2017-04-24 20:41:00 +03:00
|
|
|
// - blog id - a hash of the blog hostname, pathname and db_hash. No identifiable info is stored.
|
2014-01-03 19:50:03 +04:00
|
|
|
// - ghost version
|
|
|
|
// - node version
|
|
|
|
// - npm version
|
|
|
|
// - env - production or development
|
2016-09-16 12:40:23 +03:00
|
|
|
// - database type - SQLite, MySQL
|
2014-01-03 19:50:03 +04:00
|
|
|
// - email transport - mail.options.service, or otherwise mail.transport
|
2014-06-24 16:59:34 +04:00
|
|
|
// - created date - database creation date
|
2014-01-03 19:50:03 +04:00
|
|
|
// - post count - total number of posts
|
|
|
|
// - user count - total number of users
|
|
|
|
// - theme - name of the currently active theme
|
2014-01-21 12:45:27 +04:00
|
|
|
// - apps - names of any active apps
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2017-12-11 21:14:05 +03:00
|
|
|
var crypto = require('crypto'),
|
|
|
|
exec = require('child_process').exec,
|
|
|
|
moment = require('moment'),
|
|
|
|
semver = require('semver'),
|
|
|
|
Promise = require('bluebird'),
|
|
|
|
_ = require('lodash'),
|
|
|
|
url = require('url'),
|
|
|
|
api = require('./api'),
|
|
|
|
config = require('./config'),
|
|
|
|
urlService = require('./services/url'),
|
2017-12-12 00:47:46 +03:00
|
|
|
common = require('./lib/common'),
|
2017-12-14 02:26:11 +03:00
|
|
|
request = require('./lib/request'),
|
2017-12-15 00:14:55 +03:00
|
|
|
currentVersion = require('./lib/ghost-version').full,
|
2014-07-15 15:03:12 +04:00
|
|
|
internal = {context: {internal: true}},
|
2017-08-10 17:01:51 +03:00
|
|
|
checkEndpoint = config.get('updateCheckUrl') || 'https://updates.ghost.org';
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2016-10-06 15:27:35 +03:00
|
|
|
function updateCheckError(err) {
|
2017-12-14 02:26:11 +03:00
|
|
|
if (err.response && err.response.body && typeof err.response.body === 'object') {
|
|
|
|
err = common.errors.utils.deserialize(err.response.body);
|
|
|
|
}
|
2017-02-07 21:51:19 +03:00
|
|
|
|
2014-09-16 01:08:38 +04:00
|
|
|
api.settings.edit(
|
2017-04-24 20:41:00 +03:00
|
|
|
{settings: [{key: 'next_update_check', value: Math.round(Date.now() / 1000 + 24 * 3600)}]},
|
2014-09-16 01:08:38 +04:00
|
|
|
internal
|
2016-03-02 00:17:20 +03:00
|
|
|
);
|
2014-09-16 01:08:38 +04:00
|
|
|
|
2017-12-18 17:47:55 +03:00
|
|
|
err.context = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
|
|
|
|
err.help = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://docs.ghost.org/v1'});
|
2017-12-12 00:47:46 +03:00
|
|
|
common.logging.error(err);
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
|
2016-07-22 16:02:10 +03:00
|
|
|
/**
|
|
|
|
* If the custom message is intended for current version, create and store a custom notification.
|
|
|
|
* @param {Object} message {id: uuid, version: '0.9.x', content: '' }
|
|
|
|
* @return {*|Promise}
|
|
|
|
*/
|
|
|
|
function createCustomNotification(message) {
|
|
|
|
if (!semver.satisfies(currentVersion, message.version)) {
|
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
var notification = {
|
2017-11-01 16:44:54 +03:00
|
|
|
status: 'alert',
|
|
|
|
type: 'info',
|
|
|
|
custom: true,
|
|
|
|
uuid: message.id,
|
|
|
|
dismissible: true,
|
|
|
|
message: message.content
|
|
|
|
},
|
|
|
|
getAllNotifications = api.notifications.browse({context: {internal: true}}),
|
|
|
|
getSeenNotifications = api.settings.read(_.extend({key: 'seen_notifications'}, internal));
|
2016-07-22 16:02:10 +03:00
|
|
|
|
|
|
|
return Promise.join(getAllNotifications, getSeenNotifications, function joined(all, seen) {
|
2017-11-01 16:44:54 +03:00
|
|
|
var isSeen = _.includes(JSON.parse(seen.settings[0].value || []), notification.id),
|
2016-07-22 16:02:10 +03:00
|
|
|
isDuplicate = _.some(all.notifications, {message: notification.message});
|
|
|
|
|
|
|
|
if (!isSeen && !isDuplicate) {
|
|
|
|
return api.notifications.add({notifications: [notification]}, {context: {internal: true}});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-01-03 19:50:03 +04:00
|
|
|
function updateCheckData() {
|
|
|
|
var data = {},
|
2016-09-13 18:41:14 +03:00
|
|
|
mailConfig = config.get('mail');
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2017-12-11 21:14:05 +03:00
|
|
|
data.ghost_version = currentVersion;
|
|
|
|
data.node_version = process.versions.node;
|
|
|
|
data.env = config.get('env');
|
|
|
|
data.database_type = config.get('database').client;
|
2016-01-24 22:30:32 +03:00
|
|
|
data.email_transport = mailConfig &&
|
2017-12-11 21:14:05 +03:00
|
|
|
(mailConfig.options && mailConfig.options.service ?
|
|
|
|
mailConfig.options.service :
|
|
|
|
mailConfig.transport);
|
2016-01-24 22:30:32 +03:00
|
|
|
|
|
|
|
return Promise.props({
|
2017-04-24 20:41:00 +03:00
|
|
|
hash: api.settings.read(_.extend({key: 'db_hash'}, internal)).reflect(),
|
|
|
|
theme: api.settings.read(_.extend({key: 'active_theme'}, internal)).reflect(),
|
|
|
|
apps: api.settings.read(_.extend({key: 'active_apps'}, internal))
|
2016-01-24 22:30:32 +03:00
|
|
|
.then(function (response) {
|
|
|
|
var apps = response.settings[0];
|
2016-03-02 00:17:20 +03:00
|
|
|
|
|
|
|
apps = JSON.parse(apps.value);
|
2016-01-24 22:30:32 +03:00
|
|
|
|
2017-12-11 21:14:05 +03:00
|
|
|
return _.reduce(apps, function (memo, item) {
|
|
|
|
return memo === '' ? memo + item : memo + ', ' + item;
|
|
|
|
}, '');
|
2016-03-02 00:17:20 +03:00
|
|
|
}).reflect(),
|
|
|
|
posts: api.posts.browse().reflect(),
|
|
|
|
users: api.users.browse(internal).reflect(),
|
|
|
|
npm: Promise.promisify(exec)('npm -v').reflect()
|
2016-01-24 22:30:32 +03:00
|
|
|
}).then(function (descriptors) {
|
2017-12-11 21:14:05 +03:00
|
|
|
var hash = descriptors.hash.value().settings[0],
|
|
|
|
theme = descriptors.theme.value().settings[0],
|
|
|
|
apps = descriptors.apps.value(),
|
|
|
|
posts = descriptors.posts.value(),
|
|
|
|
users = descriptors.users.value(),
|
|
|
|
npm = descriptors.npm.value(),
|
|
|
|
blogUrl = url.parse(urlService.utils.urlFor('home', true)),
|
|
|
|
blogId = blogUrl.hostname + blogUrl.pathname.replace(/\//, '') + hash.value;
|
|
|
|
|
|
|
|
data.blog_id = crypto.createHash('md5').update(blogId).digest('hex');
|
|
|
|
data.theme = theme ? theme.value : '';
|
|
|
|
data.apps = apps || '';
|
|
|
|
data.post_count = posts && posts.meta && posts.meta.pagination ? posts.meta.pagination.total : 0;
|
|
|
|
data.user_count = users && users.users && users.users.length ? users.users.length : 0;
|
2014-04-28 03:28:50 +04:00
|
|
|
data.blog_created_at = users && users.users && users.users[0] && users.users[0].created_at ? moment(users.users[0].created_at).unix() : '';
|
2017-12-11 21:14:05 +03:00
|
|
|
data.npm_version = npm.trim();
|
|
|
|
data.lts = false;
|
2014-01-03 19:50:03 +04:00
|
|
|
|
|
|
|
return data;
|
2014-08-17 10:17:23 +04:00
|
|
|
}).catch(updateCheckError);
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
function updateCheckRequest() {
|
2017-12-14 02:26:11 +03:00
|
|
|
return updateCheckData()
|
|
|
|
.then(function then(reqData) {
|
|
|
|
return request(checkEndpoint, {
|
|
|
|
json: true,
|
|
|
|
body: reqData,
|
|
|
|
headers: {
|
|
|
|
'Content-Length': Buffer.byteLength(JSON.stringify(reqData))
|
|
|
|
},
|
|
|
|
timeout: 1000
|
|
|
|
}).then(function (response) {
|
|
|
|
return response.body;
|
2014-09-16 01:08:38 +04:00
|
|
|
});
|
2014-01-03 19:50:03 +04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-07-22 16:02:10 +03:00
|
|
|
/**
|
|
|
|
* Handles the response from the update check
|
|
|
|
* Does three things with the information received:
|
|
|
|
* 1. Updates the time we can next make a check
|
|
|
|
* 2. Checks if the version in the response is new, and updates the notification setting
|
|
|
|
* 3. Create custom notifications is response from UpdateCheck as "messages" array which has the following structure:
|
|
|
|
*
|
|
|
|
* "messages": [{
|
|
|
|
* "id": ed9dc38c-73e5-4d72-a741-22b11f6e151a,
|
|
|
|
* "version": "0.5.x",
|
|
|
|
* "content": "<p>Hey there! 0.6 is available, visit <a href=\"https://ghost.org/download\">Ghost.org</a> to grab your copy now<!/p>"
|
|
|
|
* ]}
|
|
|
|
*
|
|
|
|
* @param {Object} response
|
|
|
|
* @return {Promise}
|
|
|
|
*/
|
2014-01-03 19:50:03 +04:00
|
|
|
function updateCheckResponse(response) {
|
2016-07-22 16:02:10 +03:00
|
|
|
return Promise.all([
|
2017-04-24 20:41:00 +03:00
|
|
|
api.settings.edit({settings: [{key: 'next_update_check', value: response.next_check}]}, internal),
|
|
|
|
api.settings.edit({settings: [{key: 'display_update_notification', value: response.version}]}, internal)
|
2016-07-22 16:02:10 +03:00
|
|
|
]).then(function () {
|
|
|
|
var messages = response.messages || [];
|
2017-02-07 21:51:19 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* by default the update check service returns messages: []
|
2017-04-24 20:41:00 +03:00
|
|
|
* but the latest release version get's stored anyway, because we adding the `display_update_notification` ^
|
2017-02-07 21:51:19 +03:00
|
|
|
*/
|
2016-07-22 16:02:10 +03:00
|
|
|
return Promise.map(messages, createCustomNotification);
|
|
|
|
});
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
|
2014-01-14 23:46:36 +04:00
|
|
|
function updateCheck() {
|
2017-02-03 21:25:39 +03:00
|
|
|
if (config.isPrivacyDisabled('useUpdateCheck')) {
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.resolve();
|
2014-01-03 19:50:03 +04:00
|
|
|
} else {
|
2017-04-24 20:41:00 +03:00
|
|
|
return api.settings.read(_.extend({key: 'next_update_check'}, internal)).then(function then(result) {
|
2014-04-28 03:28:50 +04:00
|
|
|
var nextUpdateCheck = result.settings[0];
|
|
|
|
|
2014-01-03 19:50:03 +04:00
|
|
|
if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
|
|
|
// It's not time to check yet
|
2017-11-01 16:44:54 +03:00
|
|
|
return; // eslint-disable-line no-useless-return
|
2014-01-03 19:50:03 +04:00
|
|
|
} else {
|
2014-01-14 23:46:36 +04:00
|
|
|
// We need to do a check
|
2014-01-03 19:50:03 +04:00
|
|
|
return updateCheckRequest()
|
|
|
|
.then(updateCheckResponse)
|
2014-08-17 10:17:23 +04:00
|
|
|
.catch(updateCheckError);
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
2014-08-17 10:17:23 +04:00
|
|
|
}).catch(updateCheckError);
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-01-14 23:46:36 +04:00
|
|
|
function showUpdateNotification() {
|
2017-04-24 20:41:00 +03:00
|
|
|
return api.settings.read(_.extend({key: 'display_update_notification'}, internal)).then(function then(response) {
|
2014-04-28 03:28:50 +04:00
|
|
|
var display = response.settings[0];
|
|
|
|
|
2017-09-13 14:12:41 +03:00
|
|
|
// @TODO: We only show minor/major releases. This is a temporary fix. #5071 is coming soon.
|
2017-09-25 14:22:56 +03:00
|
|
|
if (display && display.value && currentVersion && semver.gt(display.value, currentVersion) && semver.patch(display.value) === 0) {
|
2014-08-17 10:17:23 +04:00
|
|
|
return display.value;
|
2014-01-14 23:46:36 +04:00
|
|
|
}
|
2014-08-17 10:17:23 +04:00
|
|
|
|
|
|
|
return false;
|
2014-01-14 23:46:36 +04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-01-08 01:32:43 +04:00
|
|
|
module.exports = updateCheck;
|
2014-01-14 23:46:36 +04:00
|
|
|
module.exports.showUpdateNotification = showUpdateNotification;
|