mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -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
|
// **takes:** filter / pagination parameters
|
||||||
browse: function browse(options) {
|
browse: function browse(options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
// **returns:** a promise for a page of posts in a json object
|
// **returns:** a promise for a page of posts in a json object
|
||||||
//return dataProvider.Post.findPage(options);
|
//return dataProvider.Post.findPage(options);
|
||||||
|
|
|
@ -6,6 +6,7 @@ var config = require('../config'),
|
||||||
mailer = require('../mail'),
|
mailer = require('../mail'),
|
||||||
errors = require('../errorHandling'),
|
errors = require('../errorHandling'),
|
||||||
storage = require('../storage'),
|
storage = require('../storage'),
|
||||||
|
updateCheck = require('../update-check'),
|
||||||
|
|
||||||
adminNavbar,
|
adminNavbar,
|
||||||
adminControllers,
|
adminControllers,
|
||||||
|
@ -278,10 +279,18 @@ adminControllers = {
|
||||||
},
|
},
|
||||||
'index': function (req, res) {
|
'index': function (req, res) {
|
||||||
/*jslint unparam:true*/
|
/*jslint unparam:true*/
|
||||||
|
function renderIndex() {
|
||||||
res.render('content', {
|
res.render('content', {
|
||||||
bodyClass: 'manage',
|
bodyClass: 'manage',
|
||||||
adminNav: setSelected(adminNavbar, 'content')
|
adminNav: setSelected(adminNavbar, 'content')
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
when.join(
|
||||||
|
updateCheck(res),
|
||||||
|
when(renderIndex())
|
||||||
|
// an error here should just get logged
|
||||||
|
).otherwise(errors.logError);
|
||||||
},
|
},
|
||||||
'editor': function (req, res) {
|
'editor': function (req, res) {
|
||||||
if (req.params.id !== undefined) {
|
if (req.params.id !== undefined) {
|
||||||
|
|
|
@ -5,6 +5,12 @@
|
||||||
},
|
},
|
||||||
"dbHash": {
|
"dbHash": {
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
"nextUpdateCheck": {
|
||||||
|
"defaultValue": null
|
||||||
|
},
|
||||||
|
"displayUpdateNotification": {
|
||||||
|
"defaultValue": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"blog": {
|
"blog": {
|
||||||
|
|
|
@ -4,6 +4,7 @@ var _ = require('underscore'),
|
||||||
fs = require('fs'),
|
fs = require('fs'),
|
||||||
configPaths = require('./config/paths'),
|
configPaths = require('./config/paths'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
|
when = require('when'),
|
||||||
errors,
|
errors,
|
||||||
|
|
||||||
// Paths for views
|
// Paths for views
|
||||||
|
@ -34,6 +35,12 @@ errors = {
|
||||||
throw err;
|
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) {
|
logWarn: function (warn, context, help) {
|
||||||
if ((process.env.NODE_ENV === 'development' ||
|
if ((process.env.NODE_ENV === 'development' ||
|
||||||
process.env.NODE_ENV === 'staging' ||
|
process.env.NODE_ENV === 'staging' ||
|
||||||
|
|
|
@ -560,6 +560,24 @@ coreHelpers.adminUrl = function (options) {
|
||||||
return config.paths.urlFor(context, absolute);
|
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
|
// Register an async handlebars helper for a given handlebars instance
|
||||||
function registerAsyncHelper(hbs, name, fn) {
|
function registerAsyncHelper(hbs, name, fn) {
|
||||||
hbs.registerAsyncHelper(name, function (options, cb) {
|
hbs.registerAsyncHelper(name, function (options, cb) {
|
||||||
|
@ -573,7 +591,6 @@ function registerAsyncHelper(hbs, name, fn) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Register a handlebars helper for themes
|
// Register a handlebars helper for themes
|
||||||
function registerThemeHelper(name, fn) {
|
function registerThemeHelper(name, fn) {
|
||||||
hbs.registerHelper(name, fn);
|
hbs.registerHelper(name, fn);
|
||||||
|
@ -651,6 +668,7 @@ registerHelpers = function (adminHbs, assetHash) {
|
||||||
|
|
||||||
registerAdminHelper('adminUrl', coreHelpers.adminUrl);
|
registerAdminHelper('adminUrl', coreHelpers.adminUrl);
|
||||||
|
|
||||||
|
registerAsyncAdminHelper('updateNotification', coreHelpers.updateNotification);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = coreHelpers;
|
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}}
|
{{/unless}}
|
||||||
|
|
||||||
<main role="main" id="main">
|
<main role="main" id="main">
|
||||||
|
{{updateNotification}}
|
||||||
|
|
||||||
<aside id="notifications">
|
<aside id="notifications">
|
||||||
{{> notifications}}
|
{{> notifications}}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
@ -17,15 +17,14 @@ var testUtils = require('../utils'),
|
||||||
|
|
||||||
describe('Core Helpers', function () {
|
describe('Core Helpers', function () {
|
||||||
|
|
||||||
var ghost,
|
var sandbox,
|
||||||
sandbox,
|
|
||||||
apiStub;
|
apiStub;
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
var adminHbs = hbs.create();
|
var adminHbs = hbs.create();
|
||||||
helpers = rewire('../../server/helpers');
|
helpers = rewire('../../server/helpers');
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
apiStub = sandbox.stub(api.settings, 'read', function () {
|
apiStub = sandbox.stub(api.settings, 'read', function (arg) {
|
||||||
return when({value: 'casper'});
|
return when({value: 'casper'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -778,12 +777,14 @@ describe('Core Helpers', function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ## Admin only helpers
|
||||||
describe("ghostScriptTags helper", function () {
|
describe("ghostScriptTags helper", function () {
|
||||||
var rendered,
|
var rendered,
|
||||||
configStub;
|
configStub;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
// set the asset hash
|
// set the asset hash
|
||||||
|
helpers = rewire('../../server/helpers');
|
||||||
helpers.assetHash = 'abc';
|
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 = {
|
expectedProperties = {
|
||||||
posts: ['posts', 'page', 'limit', 'pages', 'total'],
|
posts: ['posts', 'page', 'limit', 'pages', 'total'],
|
||||||
post: ['id', 'uuid', 'title', 'slug', 'markdown', 'html', 'meta_title', 'meta_description',
|
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',
|
'featured', 'image', 'status', 'language', 'author_id', 'created_at', 'created_by', 'updated_at',
|
||||||
'published_at', 'published_by', 'page', 'author', 'user', 'tags'],
|
'updated_by', 'published_at', 'published_by', 'page', 'author', 'user', 'tags'],
|
||||||
// TODO: remove databaseVersion, dbHash
|
// TODO: remove databaseVersion, dbHash
|
||||||
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang', "permalinks",
|
settings: ['databaseVersion', 'dbHash', 'title', 'description', 'email', 'logo', 'cover', 'defaultLang',
|
||||||
'postsPerPage', 'forceI18n', 'activeTheme', 'activePlugins', 'installedPlugins', 'availableThemes'],
|
"permalinks", 'postsPerPage', 'forceI18n', 'activeTheme', 'activePlugins', 'installedPlugins',
|
||||||
|
'availableThemes', 'nextUpdateCheck', 'displayUpdateNotification'],
|
||||||
tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent_id',
|
tag: ['id', 'uuid', 'name', 'slug', 'description', 'parent_id',
|
||||||
'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
|
'meta_title', 'meta_description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
|
||||||
user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website',
|
user: ['id', 'uuid', 'name', 'slug', 'email', 'image', 'cover', 'bio', 'website',
|
||||||
|
|
Loading…
Add table
Reference in a new issue