mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Merge pull request #1832 from ErisDS/issue-1464
Add update notifications
This commit is contained in:
commit
d5b57a9480
9 changed files with 303 additions and 12 deletions
|
@ -12,6 +12,7 @@ posts = {
|
|||
|
||||
// **takes:** filter / pagination parameters
|
||||
browse: function browse(options) {
|
||||
options = options || {};
|
||||
|
||||
// **returns:** a promise for a page of posts in a json object
|
||||
//return dataProvider.Post.findPage(options);
|
||||
|
|
|
@ -6,6 +6,7 @@ var config = require('../config'),
|
|||
mailer = require('../mail'),
|
||||
errors = require('../errorHandling'),
|
||||
storage = require('../storage'),
|
||||
updateCheck = require('../update-check'),
|
||||
|
||||
adminNavbar,
|
||||
adminControllers,
|
||||
|
@ -278,10 +279,18 @@ adminControllers = {
|
|||
},
|
||||
'index': function (req, res) {
|
||||
/*jslint unparam:true*/
|
||||
res.render('content', {
|
||||
bodyClass: 'manage',
|
||||
adminNav: setSelected(adminNavbar, 'content')
|
||||
});
|
||||
function renderIndex() {
|
||||
res.render('content', {
|
||||
bodyClass: 'manage',
|
||||
adminNav: setSelected(adminNavbar, 'content')
|
||||
});
|
||||
}
|
||||
|
||||
when.join(
|
||||
updateCheck(res),
|
||||
when(renderIndex())
|
||||
// an error here should just get logged
|
||||
).otherwise(errors.logError);
|
||||
},
|
||||
'editor': function (req, res) {
|
||||
if (req.params.id !== undefined) {
|
||||
|
|
|
@ -5,6 +5,12 @@
|
|||
},
|
||||
"dbHash": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"nextUpdateCheck": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"displayUpdateNotification": {
|
||||
"defaultValue": false
|
||||
}
|
||||
},
|
||||
"blog": {
|
||||
|
|
|
@ -4,6 +4,7 @@ var _ = require('underscore'),
|
|||
fs = require('fs'),
|
||||
configPaths = require('./config/paths'),
|
||||
path = require('path'),
|
||||
when = require('when'),
|
||||
errors,
|
||||
|
||||
// Paths for views
|
||||
|
@ -34,6 +35,12 @@ errors = {
|
|||
throw err;
|
||||
},
|
||||
|
||||
// ## Reject Error
|
||||
// Used to pass through promise errors when we want to handle them at a later time
|
||||
rejectError: function (err) {
|
||||
return when.reject(err);
|
||||
},
|
||||
|
||||
logWarn: function (warn, context, help) {
|
||||
if ((process.env.NODE_ENV === 'development' ||
|
||||
process.env.NODE_ENV === 'staging' ||
|
||||
|
|
|
@ -560,6 +560,24 @@ coreHelpers.adminUrl = function (options) {
|
|||
return config.paths.urlFor(context, absolute);
|
||||
};
|
||||
|
||||
coreHelpers.updateNotification = function () {
|
||||
var output = '';
|
||||
|
||||
if (config().updateCheck === false || !this.currentUser) {
|
||||
return when(output);
|
||||
}
|
||||
|
||||
return api.settings.read('displayUpdateNotification').then(function (display) {
|
||||
if (display && display.value && display.value === 'true') {
|
||||
output = '<div class="notification-success">' +
|
||||
'A new version of Ghost is available! Hot damn. ' +
|
||||
'<a href="http://ghost.org/download">Upgrade now</a></div>';
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
};
|
||||
|
||||
// Register an async handlebars helper for a given handlebars instance
|
||||
function registerAsyncHelper(hbs, name, fn) {
|
||||
hbs.registerAsyncHelper(name, function (options, cb) {
|
||||
|
@ -573,7 +591,6 @@ function registerAsyncHelper(hbs, name, fn) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
// Register a handlebars helper for themes
|
||||
function registerThemeHelper(name, fn) {
|
||||
hbs.registerHelper(name, fn);
|
||||
|
@ -651,6 +668,7 @@ registerHelpers = function (adminHbs, assetHash) {
|
|||
|
||||
registerAdminHelper('adminUrl', coreHelpers.adminUrl);
|
||||
|
||||
registerAsyncAdminHelper('updateNotification', coreHelpers.updateNotification);
|
||||
};
|
||||
|
||||
module.exports = coreHelpers;
|
||||
|
|
191
core/server/update-check.js
Normal file
191
core/server/update-check.js
Normal file
|
@ -0,0 +1,191 @@
|
|||
// # Update Checking Service
|
||||
//
|
||||
// Makes a request to Ghost.org to check if there is a new version of Ghost available.
|
||||
// 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 'updateCheck: false' in their config.js
|
||||
//
|
||||
// The data collected is as follows:
|
||||
// - blog id - a hash of the blog hostname, pathname and dbHash, we do not store URL, IP or other identifiable info
|
||||
// - ghost version
|
||||
// - node version
|
||||
// - npm version
|
||||
// - env - production or development
|
||||
// - database type - SQLite, MySQL, pg
|
||||
// - email transport - mail.options.service, or otherwise mail.transport
|
||||
// - created date - the date the database was created
|
||||
// - post count - total number of posts
|
||||
// - user count - total number of users
|
||||
// - theme - name of the currently active theme
|
||||
// - apps - names of any active plugins
|
||||
|
||||
var crypto = require('crypto'),
|
||||
exec = require('child_process').exec,
|
||||
https = require('https'),
|
||||
moment = require('moment'),
|
||||
semver = require('semver'),
|
||||
when = require('when'),
|
||||
nodefn = require('when/node/function'),
|
||||
_ = require('underscore'),
|
||||
url = require('url'),
|
||||
|
||||
api = require('./api'),
|
||||
config = require('./config'),
|
||||
errors = require('./errorHandling'),
|
||||
|
||||
allowedCheckEnvironments = ['development', 'production'],
|
||||
checkEndpoint = 'updates.ghost.org',
|
||||
currentVersion;
|
||||
|
||||
function updateCheckError(error) {
|
||||
errors.logError(
|
||||
error,
|
||||
"Checking for updates failed, your blog will continue to function.",
|
||||
"If you get this error repeatedly, please seek help from http://ghost.org/forum."
|
||||
);
|
||||
}
|
||||
|
||||
function updateCheckData() {
|
||||
var data = {},
|
||||
ops = [],
|
||||
mailConfig = config().mail;
|
||||
|
||||
ops.push(api.settings.read('dbHash').otherwise(errors.rejectError));
|
||||
ops.push(api.settings.read('activeTheme').otherwise(errors.rejectError));
|
||||
ops.push(api.settings.read('activePlugins')
|
||||
.then(function (apps) {
|
||||
try {
|
||||
apps = JSON.parse(apps.value);
|
||||
} catch (e) {
|
||||
return errors.rejectError(e);
|
||||
}
|
||||
|
||||
return _.reduce(apps, function (memo, item) { return memo === '' ? memo + item : memo + ', ' + item; }, '');
|
||||
}).otherwise(errors.rejectError));
|
||||
ops.push(api.posts.browse().otherwise(errors.rejectError));
|
||||
ops.push(api.users.browse().otherwise(errors.rejectError));
|
||||
ops.push(nodefn.call(exec, 'npm -v').otherwise(errors.rejectError));
|
||||
|
||||
data.ghost_version = currentVersion;
|
||||
data.node_version = process.versions.node;
|
||||
data.env = process.env.NODE_ENV;
|
||||
data.database_type = require('./models/base').client;
|
||||
data.email_transport = mailConfig.options && mailConfig.options.service ? mailConfig.options.service : mailConfig.transport;
|
||||
|
||||
return when.settle(ops).then(function (descriptors) {
|
||||
var hash = descriptors[0].value,
|
||||
theme = descriptors[1].value,
|
||||
apps = descriptors[2].value,
|
||||
posts = descriptors[3].value,
|
||||
users = descriptors[4].value,
|
||||
npm = descriptors[5].value,
|
||||
blogUrl = url.parse(config().url),
|
||||
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.total ? posts.total : 0;
|
||||
data.user_count = users && users.length ? users.length : 0;
|
||||
data.blog_created_at = users && users[0] && users[0].created_at ? moment(users[0].created_at).unix() : '';
|
||||
data.npm_version = _.isArray(npm) && npm[0] ? npm[0].toString().replace(/\n/, '') : '';
|
||||
|
||||
return data;
|
||||
}).otherwise(updateCheckError);
|
||||
}
|
||||
|
||||
function updateCheckRequest() {
|
||||
return updateCheckData().then(function (reqData) {
|
||||
var deferred = when.defer(),
|
||||
resData = '',
|
||||
headers,
|
||||
req;
|
||||
|
||||
reqData = JSON.stringify(reqData);
|
||||
|
||||
headers = {
|
||||
'Content-Length': reqData.length
|
||||
};
|
||||
|
||||
req = https.request({
|
||||
hostname: checkEndpoint,
|
||||
method: 'POST',
|
||||
headers: headers
|
||||
}, function (res) {
|
||||
res.on('error', function (error) { deferred.reject(error); });
|
||||
res.on('data', function (chunk) { resData += chunk; });
|
||||
res.on('end', function () {
|
||||
try {
|
||||
resData = JSON.parse(resData);
|
||||
deferred.resolve(resData);
|
||||
} catch (e) {
|
||||
deferred.reject('Unable to decode update response');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.write(reqData);
|
||||
req.end();
|
||||
|
||||
req.on('error', function (error) {
|
||||
deferred.reject(error);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
}
|
||||
|
||||
// ## 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) {
|
||||
var ops = [],
|
||||
displayUpdateNotification = currentVersion && semver.gt(response.version, currentVersion);
|
||||
|
||||
ops.push(api.settings.edit('nextUpdateCheck', response.next_check)
|
||||
.otherwise(errors.rejectError));
|
||||
|
||||
ops.push(api.settings.edit('displayUpdateNotification', displayUpdateNotification)
|
||||
.otherwise(errors.rejectError));
|
||||
|
||||
return when.settle(ops).then(function (descriptors) {
|
||||
descriptors.forEach(function (d) {
|
||||
if (d.state === 'rejected') {
|
||||
errors.rejectError(d.reason);
|
||||
}
|
||||
});
|
||||
return when.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function updateCheck(res) {
|
||||
var deferred = when.defer();
|
||||
|
||||
// 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
|
||||
if (config().updateCheck === false || _.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
|
||||
// No update check
|
||||
deferred.resolve();
|
||||
} else {
|
||||
api.settings.read('nextUpdateCheck').then(function (nextUpdateCheck) {
|
||||
if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
||||
// It's not time to check yet
|
||||
deferred.resolve();
|
||||
} else {
|
||||
// We need to do a check, store the current version
|
||||
currentVersion = res.locals.version;
|
||||
return updateCheckRequest()
|
||||
.then(updateCheckResponse)
|
||||
.otherwise(updateCheckError);
|
||||
}
|
||||
}).otherwise(updateCheckError)
|
||||
.then(deferred.resolve);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
module.exports = updateCheck;
|
|
@ -37,6 +37,8 @@
|
|||
{{/unless}}
|
||||
|
||||
<main role="main" id="main">
|
||||
{{updateNotification}}
|
||||
|
||||
<aside id="notifications">
|
||||
{{> notifications}}
|
||||
</aside>
|
||||
|
|
|
@ -17,15 +17,14 @@ var testUtils = require('../utils'),
|
|||
|
||||
describe('Core Helpers', function () {
|
||||
|
||||
var ghost,
|
||||
sandbox,
|
||||
var sandbox,
|
||||
apiStub;
|
||||
|
||||
beforeEach(function (done) {
|
||||
var adminHbs = hbs.create();
|
||||
helpers = rewire('../../server/helpers');
|
||||
sandbox = sinon.sandbox.create();
|
||||
apiStub = sandbox.stub(api.settings, 'read', function () {
|
||||
apiStub = sandbox.stub(api.settings, 'read', function (arg) {
|
||||
return when({value: 'casper'});
|
||||
});
|
||||
|
||||
|
@ -778,12 +777,14 @@ describe('Core Helpers', function () {
|
|||
|
||||
});
|
||||
|
||||
// ## Admin only helpers
|
||||
describe("ghostScriptTags helper", function () {
|
||||
var rendered,
|
||||
configStub;
|
||||
|
||||
beforeEach(function () {
|
||||
// set the asset hash
|
||||
helpers = rewire('../../server/helpers');
|
||||
helpers.assetHash = 'abc';
|
||||
});
|
||||
|
||||
|
@ -937,4 +938,59 @@ describe('Core Helpers', function () {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('updateNotification', function () {
|
||||
it('outputs a correctly formatted notification when display is set to true', function (done) {
|
||||
var output = '<div class="notification-success">' +
|
||||
'A new version of Ghost is available! Hot damn. ' +
|
||||
'<a href="http://ghost.org/download">Upgrade now</a></div>';
|
||||
|
||||
apiStub.restore();
|
||||
apiStub = sandbox.stub(api.settings, 'read', function () {
|
||||
return when({value: 'true'});
|
||||
});
|
||||
|
||||
helpers.updateNotification.call({currentUser: {name: 'bob'}}).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
|
||||
rendered.should.equal(output);
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('does NOT output a correctly formatted notification when display is not set to true', function (done) {
|
||||
helpers.updateNotification.call({currentUser: {name: 'bob'}}).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.should.equal('');
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('does NOT output a notification if updateCheck is false', function (done) {
|
||||
helpers.__set__('config', function () { return { updateCheck: false}; });
|
||||
|
||||
apiStub.restore();
|
||||
apiStub = sandbox.stub(api.settings, 'read', function () {
|
||||
return when({value: 'true'});
|
||||
});
|
||||
|
||||
helpers.updateNotification.call({currentUser: {name: 'bob'}}).then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.should.equal('');
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
|
||||
it('does NOT output a notification if the user is not logged in', function (done) {
|
||||
apiStub.restore();
|
||||
apiStub = sandbox.stub(api.settings, 'read', function () {
|
||||
return when({value: 'true'});
|
||||
});
|
||||
|
||||
helpers.updateNotification.call().then(function (rendered) {
|
||||
should.exist(rendered);
|
||||
rendered.should.equal('');
|
||||
done();
|
||||
}).then(null, done);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,11 +7,12 @@ var _ = require('underscore'),
|
|||
expectedProperties = {
|
||||
posts: ['posts', 'page', 'limit', 'pages', 'total'],
|
||||
post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
|
||||
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at', 'updated_by',
|
||||
'published_at', 'published_by', 'page', 'author', 'user', 'tags'],
|
||||
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at',
|
||||
'updated_by', 'published_at', 'published_by', 'page', 'author', 'user', 'tags'],
|
||||
// TODO: remove databaseVersion, dbHash
|
||||
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang', "permalinks",
|
||||
'postsPerPage', 'forceI18n', 'activeTheme', 'activePlugins', 'installedPlugins', 'availableThemes'],
|
||||
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang',
|
||||
"permalinks", 'postsPerPage', 'forceI18n', 'activeTheme', 'activePlugins', 'installedPlugins',
|
||||
'availableThemes', 'nextUpdateCheck', 'displayUpdateNotification'],
|
||||
tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent_id',
|
||||
'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
|
||||
user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website',
|
||||
|
|
Loading…
Add table
Reference in a new issue