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
|
|
|
//
|
2014-06-24 16:59:34 +04:00
|
|
|
// - blog id - a hash of the blog hostname, pathname and dbHash. No identifiable info is stored.
|
2014-01-03 19:50:03 +04:00
|
|
|
// - ghost version
|
|
|
|
// - node version
|
|
|
|
// - npm version
|
|
|
|
// - env - production or development
|
2014-06-24 16:59:34 +04:00
|
|
|
// - database type - SQLite, MySQL, PostgreSQL
|
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
|
|
|
|
|
|
|
var crypto = require('crypto'),
|
|
|
|
exec = require('child_process').exec,
|
|
|
|
https = require('https'),
|
|
|
|
moment = require('moment'),
|
|
|
|
semver = require('semver'),
|
2014-08-17 10:17:23 +04:00
|
|
|
Promise = require('bluebird'),
|
2014-02-05 12:40:30 +04:00
|
|
|
_ = require('lodash'),
|
2014-01-03 19:50:03 +04:00
|
|
|
url = require('url'),
|
|
|
|
|
|
|
|
api = require('./api'),
|
|
|
|
config = require('./config'),
|
2014-05-09 14:11:29 +04:00
|
|
|
errors = require('./errors'),
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2014-07-15 15:03:12 +04:00
|
|
|
internal = {context: {internal: true}},
|
2014-01-03 19:50:03 +04:00
|
|
|
allowedCheckEnvironments = ['development', 'production'],
|
|
|
|
checkEndpoint = 'updates.ghost.org',
|
2014-11-28 20:47:32 +03:00
|
|
|
currentVersion = config.ghostVersion;
|
2014-01-03 19:50:03 +04:00
|
|
|
|
|
|
|
function updateCheckError(error) {
|
2014-09-16 01:08:38 +04:00
|
|
|
api.settings.edit(
|
|
|
|
{settings: [{key: 'nextUpdateCheck', value: Math.round(Date.now() / 1000 + 24 * 3600)}]},
|
|
|
|
internal
|
|
|
|
).catch(errors.rejectError);
|
|
|
|
|
2014-01-03 19:50:03 +04:00
|
|
|
errors.logError(
|
|
|
|
error,
|
2014-07-15 15:03:12 +04:00
|
|
|
'Checking for updates failed, your blog will continue to function.',
|
|
|
|
'If you get this error repeatedly, please seek help from https://ghost.org/forum.'
|
2014-01-03 19:50:03 +04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function updateCheckData() {
|
|
|
|
var data = {},
|
|
|
|
ops = [],
|
2014-07-17 18:33:21 +04:00
|
|
|
mailConfig = config.mail;
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
ops.push(api.settings.read(_.extend(internal, {key: 'dbHash'})).catch(errors.rejectError));
|
|
|
|
ops.push(api.settings.read(_.extend(internal, {key: 'activeTheme'})).catch(errors.rejectError));
|
2014-07-15 15:03:12 +04:00
|
|
|
ops.push(api.settings.read(_.extend(internal, {key: 'activeApps'}))
|
2014-04-28 03:28:50 +04:00
|
|
|
.then(function (response) {
|
|
|
|
var apps = response.settings[0];
|
2014-01-03 19:50:03 +04:00
|
|
|
try {
|
|
|
|
apps = JSON.parse(apps.value);
|
|
|
|
} catch (e) {
|
|
|
|
return errors.rejectError(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
return _.reduce(apps, function (memo, item) { return memo === '' ? memo + item : memo + ', ' + item; }, '');
|
2014-08-17 10:17:23 +04:00
|
|
|
}).catch(errors.rejectError));
|
|
|
|
ops.push(api.posts.browse().catch(errors.rejectError));
|
|
|
|
ops.push(api.users.browse(internal).catch(errors.rejectError));
|
|
|
|
ops.push(Promise.promisify(exec)('npm -v').catch(errors.rejectError));
|
2014-01-03 19:50:03 +04:00
|
|
|
|
|
|
|
data.ghost_version = currentVersion;
|
|
|
|
data.node_version = process.versions.node;
|
|
|
|
data.env = process.env.NODE_ENV;
|
2014-07-17 18:33:21 +04:00
|
|
|
data.database_type = config.database.client;
|
2014-01-08 01:32:43 +04:00
|
|
|
data.email_transport = mailConfig && (mailConfig.options && mailConfig.options.service ? mailConfig.options.service : mailConfig.transport);
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.settle(ops).then(function (descriptors) {
|
|
|
|
var hash = descriptors[0].value().settings[0],
|
|
|
|
theme = descriptors[1].value().settings[0],
|
|
|
|
apps = descriptors[2].value(),
|
|
|
|
posts = descriptors[3].value(),
|
|
|
|
users = descriptors[4].value(),
|
|
|
|
npm = descriptors[5].value(),
|
2014-07-17 18:33:21 +04:00
|
|
|
blogUrl = url.parse(config.url),
|
2014-01-03 19:50:03 +04:00
|
|
|
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 || '';
|
2014-05-04 05:30:30 +04:00
|
|
|
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() : '';
|
2014-01-03 19:50:03 +04:00
|
|
|
data.npm_version = _.isArray(npm) && npm[0] ? npm[0].toString().replace(/\n/, '') : '';
|
|
|
|
|
|
|
|
return data;
|
2014-08-17 10:17:23 +04:00
|
|
|
}).catch(updateCheckError);
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
function updateCheckRequest() {
|
|
|
|
return updateCheckData().then(function (reqData) {
|
2014-08-17 10:17:23 +04:00
|
|
|
var resData = '',
|
2014-01-03 19:50:03 +04:00
|
|
|
headers,
|
|
|
|
req;
|
|
|
|
|
|
|
|
reqData = JSON.stringify(reqData);
|
|
|
|
|
|
|
|
headers = {
|
|
|
|
'Content-Length': reqData.length
|
|
|
|
};
|
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
return new Promise(function (resolve, reject) {
|
|
|
|
req = https.request({
|
|
|
|
hostname: checkEndpoint,
|
|
|
|
method: 'POST',
|
|
|
|
headers: headers
|
|
|
|
}, function (res) {
|
|
|
|
res.on('error', function (error) { reject(error); });
|
|
|
|
res.on('data', function (chunk) { resData += chunk; });
|
|
|
|
res.on('end', function () {
|
|
|
|
try {
|
|
|
|
resData = JSON.parse(resData);
|
|
|
|
resolve(resData);
|
|
|
|
} catch (e) {
|
|
|
|
reject('Unable to decode update response');
|
|
|
|
}
|
|
|
|
});
|
2014-01-03 19:50:03 +04:00
|
|
|
});
|
|
|
|
|
2014-09-16 01:08:38 +04:00
|
|
|
req.on('socket', function (socket) {
|
|
|
|
// Wait a maximum of 10seconds
|
|
|
|
socket.setTimeout(10000);
|
|
|
|
socket.on('timeout', function () {
|
|
|
|
req.abort();
|
|
|
|
});
|
|
|
|
});
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
req.on('error', function (error) {
|
|
|
|
reject(error);
|
|
|
|
});
|
2014-09-16 01:08:38 +04:00
|
|
|
|
|
|
|
req.write(reqData);
|
|
|
|
req.end();
|
2014-01-03 19:50:03 +04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// ## Update Check Response
|
|
|
|
// Handles the response from the update check
|
|
|
|
// Does two 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
|
|
|
|
function updateCheckResponse(response) {
|
2014-07-15 15:03:12 +04:00
|
|
|
var ops = [];
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
|
|
|
|
ops.push(
|
|
|
|
api.settings.edit(
|
|
|
|
{settings: [{key: 'nextUpdateCheck', value: response.next_check}]},
|
2014-07-15 15:03:12 +04:00
|
|
|
internal
|
2014-08-17 10:17:23 +04:00
|
|
|
).catch(errors.rejectError),
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
api.settings.edit(
|
|
|
|
{settings: [{key: 'displayUpdateNotification', value: response.version}]},
|
2014-07-15 15:03:12 +04:00
|
|
|
internal
|
2014-08-17 10:17:23 +04:00
|
|
|
).catch(errors.rejectError)
|
Refactor API arguments
closes #2610, refs #2697
- cleanup API index.js, and add docs
- all API methods take consistent arguments: object & options
- browse, read, destroy take options, edit and add take object and options
- the context is passed as part of options, meaning no more .call
everywhere
- destroy expects an object, rather than an id all the way down to the model layer
- route params such as :id, :slug, and :key are passed as an option & used
to perform reads, updates and deletes where possible - settings / themes
may need work here still
- HTTP posts api can find a post by slug
- Add API utils for checkData
2014-05-08 16:41:19 +04:00
|
|
|
);
|
2014-01-03 19:50:03 +04:00
|
|
|
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.settle(ops).then(function (descriptors) {
|
2014-01-03 19:50:03 +04:00
|
|
|
descriptors.forEach(function (d) {
|
2014-08-17 10:17:23 +04:00
|
|
|
if (d.isRejected()) {
|
|
|
|
errors.rejectError(d.reason());
|
2014-01-03 19:50:03 +04:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2014-01-14 23:46:36 +04:00
|
|
|
function updateCheck() {
|
2014-01-03 19:50:03 +04:00
|
|
|
// The check will not happen if:
|
|
|
|
// 1. updateCheck is defined as false in config.js
|
|
|
|
// 2. we've already done a check this session
|
|
|
|
// 3. we're not in production or development mode
|
2014-09-10 08:06:24 +04:00
|
|
|
// TODO: need to remove config.updateCheck in favor of config.privacy.updateCheck in future version (it is now deprecated)
|
2014-09-03 08:27:37 +04:00
|
|
|
if (config.updateCheck === false || config.isPrivacyDisabled('useUpdateCheck') || _.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
|
2014-01-03 19:50:03 +04:00
|
|
|
// No update check
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.resolve();
|
2014-01-03 19:50:03 +04:00
|
|
|
} else {
|
2014-08-17 10:17:23 +04:00
|
|
|
return api.settings.read(_.extend(internal, {key: 'nextUpdateCheck'})).then(function (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
|
2014-08-17 10:17:23 +04:00
|
|
|
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() {
|
2014-07-15 15:03:12 +04:00
|
|
|
return api.settings.read(_.extend(internal, {key: 'displayUpdateNotification'})).then(function (response) {
|
2014-04-28 03:28:50 +04:00
|
|
|
var display = response.settings[0];
|
|
|
|
|
2014-01-14 23:46:36 +04:00
|
|
|
// Version 0.4 used boolean to indicate the need for an update. This special case is
|
|
|
|
// translated to the version string.
|
|
|
|
// TODO: remove in future version.
|
2014-02-25 23:15:32 +04:00
|
|
|
if (display.value === 'false' || display.value === 'true' || display.value === '1' || display.value === '0') {
|
2014-01-14 23:46:36 +04:00
|
|
|
display.value = '0.4.0';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (display && display.value && currentVersion && semver.gt(display.value, currentVersion)) {
|
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;
|