mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Merge branch '0.3.3-wip'
Conflicts: core/client/views/blog.js core/server/api.js core/server/views/default.hbs package.json
This commit is contained in:
commit
65dcb17117
28 changed files with 248 additions and 173 deletions
|
@ -64,6 +64,9 @@
|
|||
|
||||
$dropzone.find('.js-fileupload').fileupload().fileupload("option", {
|
||||
url: '/ghost/upload/',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
add: function (e, data) {
|
||||
$dropzone.find('.js-fileupload').removeClass('right');
|
||||
$dropzone.find('.js-url, button.centre').remove();
|
||||
|
|
|
@ -23,6 +23,16 @@
|
|||
|
||||
_.extend(Ghost, Backbone.Events);
|
||||
|
||||
Backbone.oldsync = Backbone.sync;
|
||||
// override original sync method to make header request contain csrf token
|
||||
Backbone.sync = function (method, model, options, error) {
|
||||
options.beforeSend = function (xhr) {
|
||||
xhr.setRequestHeader('X-CSRF-Token', $("meta[name='csrf-param']").attr('content'));
|
||||
};
|
||||
/* call the old sync method */
|
||||
return Backbone.oldsync(method, model, options, error);
|
||||
};
|
||||
|
||||
Ghost.init = function () {
|
||||
Ghost.router = new Ghost.Router();
|
||||
|
||||
|
|
|
@ -202,6 +202,9 @@
|
|||
if (self.className.indexOf('notification-persistent') !== -1) {
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
url: '/api/v0.1/notifications/' + $(self).find('.close').data('id')
|
||||
}).done(function (result) {
|
||||
bbSelf.$el.slideUp(250, function () {
|
||||
|
@ -231,6 +234,9 @@
|
|||
bbSelf = this;
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
url: '/api/v0.1/notifications/' + $(self).data('id')
|
||||
}).done(function (result) {
|
||||
var height = bbSelf.$('.js-notification').outerHeight(true),
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
return Backbone.trigger('blog:activeItem', null);
|
||||
}
|
||||
|
||||
var id = this.collection.at(0).id;
|
||||
var id = this.collection.at(0) ? this.collection.at(0).id : false;
|
||||
if (id) {
|
||||
Backbone.trigger('blog:activeItem', id);
|
||||
}
|
||||
|
@ -87,6 +87,8 @@
|
|||
// Load moar posts!
|
||||
this.isLoading = true;
|
||||
this.collection.fetch({
|
||||
update: true,
|
||||
remove: false,
|
||||
data: {
|
||||
status: 'all',
|
||||
page: (self.collection.currentPage + 1),
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
$.ajax({
|
||||
url: '/ghost/signin/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
data: {
|
||||
email: email,
|
||||
password: password,
|
||||
|
@ -87,6 +90,9 @@
|
|||
$.ajax({
|
||||
url: '/ghost/signup/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
data: {
|
||||
name: name,
|
||||
email: email,
|
||||
|
@ -136,6 +142,9 @@
|
|||
$.ajax({
|
||||
url: '/ghost/forgotten/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
data: {
|
||||
email: email
|
||||
},
|
||||
|
|
|
@ -204,8 +204,8 @@
|
|||
var self = this, upload = new Ghost.Models.uploadModal({'key': key, 'src': src, 'id': this.id, 'accept': {
|
||||
func: function () { // The function called on acceptance
|
||||
var data = {};
|
||||
if (this.$('#uploadurl').val()) {
|
||||
data[key] = this.$('#uploadurl').val();
|
||||
if (this.$('.js-upload-url').val()) {
|
||||
data[key] = this.$('.js-upload-url').val();
|
||||
} else {
|
||||
data[key] = this.$('.js-upload-target').attr('src');
|
||||
}
|
||||
|
@ -266,8 +266,8 @@
|
|||
var self = this, upload = new Ghost.Models.uploadModal({'key': key, 'src': src, 'id': this.id, 'accept': {
|
||||
func: function () { // The function called on acceptance
|
||||
var data = {};
|
||||
if (this.$('#uploadurl').val()) {
|
||||
data[key] = this.$('#uploadurl').val();
|
||||
if (this.$('.js-upload-url').val()) {
|
||||
data[key] = this.$('.js-upload-url').val();
|
||||
} else {
|
||||
data[key] = this.$('.js-upload-target').attr('src');
|
||||
}
|
||||
|
@ -379,6 +379,9 @@
|
|||
$.ajax({
|
||||
url: '/ghost/changepw/',
|
||||
type: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': $("meta[name='csrf-param']").attr('content')
|
||||
},
|
||||
data: {
|
||||
password: oldPassword,
|
||||
newpassword: newPassword,
|
||||
|
|
|
@ -111,6 +111,7 @@ function ghostLocals(req, res, next) {
|
|||
res.locals = res.locals || {};
|
||||
res.locals.version = packageInfo.version;
|
||||
res.locals.path = req.path;
|
||||
res.locals.csrfToken = req.session._csrf;
|
||||
|
||||
if (res.isAdmin) {
|
||||
_.extend(res.locals, {
|
||||
|
@ -120,9 +121,9 @@ function ghostLocals(req, res, next) {
|
|||
api.users.read({id: req.session.user}).then(function (currentUser) {
|
||||
_.extend(res.locals, {
|
||||
currentUser: {
|
||||
name: currentUser.attributes.name,
|
||||
email: currentUser.attributes.email,
|
||||
image: currentUser.attributes.image
|
||||
name: currentUser.name,
|
||||
email: currentUser.email,
|
||||
image: currentUser.image
|
||||
}
|
||||
});
|
||||
next();
|
||||
|
@ -277,9 +278,16 @@ when(ghost.init()).then(function () {
|
|||
server.use('/ghost/upload/', express.multipart());
|
||||
server.use('/ghost/upload/', express.multipart({uploadDir: __dirname + '/content/images'}));
|
||||
server.use('/ghost/debug/db/import/', express.multipart());
|
||||
server.use(express.cookieParser(ghost.dbHash));
|
||||
server.use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
|
||||
|
||||
// Session handling
|
||||
// Pro tip: while in development mode cookieSession can be used
|
||||
// to keep you logged in while restarting the server
|
||||
server.use(express.cookieParser(ghost.dbHash));
|
||||
server.use(express.cookieSession({ cookie : { maxAge: 12 * 60 * 60 * 1000 }}));
|
||||
|
||||
|
||||
//enable express csrf protection
|
||||
server.use(express.csrf());
|
||||
// local data
|
||||
server.use(ghostLocals);
|
||||
// So on every request we actually clean out reduntant passive notifications from the server side
|
||||
|
@ -296,10 +304,10 @@ when(ghost.init()).then(function () {
|
|||
|
||||
// ### Error handling
|
||||
// 404 Handler
|
||||
server.use(errors.render404Page);
|
||||
server.use(errors.error404);
|
||||
|
||||
// 500 Handler
|
||||
server.use(errors.render500Page);
|
||||
server.use(errors.error500);
|
||||
|
||||
// ## Routing
|
||||
|
||||
|
@ -349,9 +357,8 @@ when(ghost.init()).then(function () {
|
|||
server.get('/ghost/debug/', auth, admin.debug.index);
|
||||
server.get('/ghost/debug/db/export/', auth, admin.debug['export']);
|
||||
server.post('/ghost/debug/db/import/', auth, admin.debug['import']);
|
||||
server.get('/ghost/debug/db/reset/', auth, admin.debug.reset);
|
||||
// We don't want to register bodyParser globally b/c of security concerns, so use multipart only here
|
||||
server.post('/ghost/upload/', admin.uploader);
|
||||
server.post('/ghost/upload/', auth, admin.uploader);
|
||||
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
|
||||
server.get(/^\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, function (req, res) {
|
||||
res.redirect('/ghost/');
|
||||
|
|
|
@ -18,7 +18,8 @@ var Ghost = require('../ghost'),
|
|||
requestHandler,
|
||||
settingsObject,
|
||||
settingsCollection,
|
||||
settingsFilter;
|
||||
settingsFilter,
|
||||
filteredUserAttributes = ['password', 'created_by', 'updated_by'];
|
||||
|
||||
// ## Posts
|
||||
posts = {
|
||||
|
@ -27,7 +28,17 @@ posts = {
|
|||
// **takes:** filter / pagination parameters
|
||||
browse: function browse(options) {
|
||||
// **returns:** a promise for a page of posts in a json object
|
||||
return dataProvider.Post.findPage(options);
|
||||
//return dataProvider.Post.findPage(options);
|
||||
return dataProvider.Post.findPage(options).then(function (result) {
|
||||
var i = 0,
|
||||
omitted = result;
|
||||
|
||||
for (i = 0; i < omitted.posts.length; i = i + 1) {
|
||||
omitted.posts[i].author = _.omit(omitted.posts[i].author, filteredUserAttributes);
|
||||
omitted.posts[i].user = _.omit(omitted.posts[i].user, filteredUserAttributes);
|
||||
}
|
||||
return omitted;
|
||||
});
|
||||
},
|
||||
|
||||
// #### Read
|
||||
|
@ -35,7 +46,19 @@ posts = {
|
|||
// **takes:** an identifier (id or slug?)
|
||||
read: function read(args) {
|
||||
// **returns:** a promise for a single post in a json object
|
||||
return dataProvider.Post.findOne(args);
|
||||
|
||||
return dataProvider.Post.findOne(args).then(function (result) {
|
||||
var omitted;
|
||||
|
||||
if (result) {
|
||||
omitted = result.toJSON();
|
||||
omitted.author = _.omit(omitted.author, filteredUserAttributes);
|
||||
omitted.user = _.omit(omitted.user, filteredUserAttributes);
|
||||
return omitted;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
// #### Edit
|
||||
|
@ -83,8 +106,8 @@ posts = {
|
|||
return when(posts.read({id : args.id})).then(function (result) {
|
||||
return dataProvider.Post.destroy(args.id).then(function () {
|
||||
var deletedObj = {};
|
||||
deletedObj.id = result.attributes.id;
|
||||
deletedObj.slug = result.attributes.slug;
|
||||
deletedObj.id = result.id;
|
||||
deletedObj.slug = result.slug;
|
||||
return deletedObj;
|
||||
});
|
||||
});
|
||||
|
@ -101,7 +124,21 @@ users = {
|
|||
// **takes:** options object
|
||||
browse: function browse(options) {
|
||||
// **returns:** a promise for a collection of users in a json object
|
||||
return dataProvider.User.browse(options);
|
||||
|
||||
return dataProvider.User.browse(options).then(function (result) {
|
||||
var i = 0,
|
||||
omitted = {};
|
||||
|
||||
if (result) {
|
||||
omitted = result.toJSON();
|
||||
}
|
||||
|
||||
for (i = 0; i < omitted.length; i = i + 1) {
|
||||
omitted[i] = _.omit(omitted[i], filteredUserAttributes);
|
||||
}
|
||||
|
||||
return omitted;
|
||||
});
|
||||
},
|
||||
|
||||
// #### Read
|
||||
|
@ -113,10 +150,13 @@ users = {
|
|||
args = {id: this.user};
|
||||
}
|
||||
|
||||
var filteredAttributes = ['password', 'created_by', 'updated_by'];
|
||||
return dataProvider.User.read(args).then(function (result) {
|
||||
if (result) {
|
||||
var omitted = _.omit(result.toJSON(), filteredUserAttributes);
|
||||
return omitted;
|
||||
}
|
||||
|
||||
return dataProvider.User.read(args).then(function omitAttrs(result) {
|
||||
return _.omit(result, filteredAttributes);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -107,12 +107,14 @@ adminControllers = {
|
|||
});
|
||||
}
|
||||
|
||||
if (type === 'image/jpeg' || type === 'image/png' || type === 'image/gif') {
|
||||
//limit uploads to type && extension
|
||||
if ((type === 'image/jpeg' || type === 'image/png' || type === 'image/gif')
|
||||
&& (ext === '.jpg' || ext === '.jpeg' || ext === '.png' || ext === '.gif')) {
|
||||
getUniqueFileName(dir, basename, ext, null, function (filename) {
|
||||
renameFile(filename);
|
||||
});
|
||||
} else {
|
||||
res.send(404, 'Invalid filetype');
|
||||
res.send(403, 'Invalid file type');
|
||||
}
|
||||
},
|
||||
'login': function (req, res) {
|
||||
|
@ -144,7 +146,6 @@ adminControllers = {
|
|||
} else {
|
||||
res.json(401, {error: 'Slow down, there are way too many login attempts!'});
|
||||
}
|
||||
|
||||
},
|
||||
changepw: function (req, res) {
|
||||
api.users.changePassword({
|
||||
|
@ -229,7 +230,7 @@ adminControllers = {
|
|||
}).otherwise(errors.logAndThrowError);
|
||||
},
|
||||
'logout': function (req, res) {
|
||||
delete req.session.user;
|
||||
req.session = null;
|
||||
var notification = {
|
||||
type: 'success',
|
||||
message: 'You were successfully signed out',
|
||||
|
@ -376,7 +377,7 @@ adminControllers = {
|
|||
};
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
delete req.session.user;
|
||||
req.session = null;
|
||||
res.set({
|
||||
"X-Cache-Invalidate": "/*"
|
||||
});
|
||||
|
@ -392,37 +393,6 @@ adminControllers = {
|
|||
id: 'per-' + (ghost.notifications.length + 1)
|
||||
};
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
res.redirect('/ghost/debug/');
|
||||
});
|
||||
});
|
||||
},
|
||||
'reset': function (req, res) {
|
||||
// Grab the current version so we can get the migration
|
||||
dataProvider.reset()
|
||||
.then(function resetSuccess() {
|
||||
var notification = {
|
||||
type: 'success',
|
||||
message: "Database reset. Create a new user",
|
||||
status: 'persistent',
|
||||
id: 'per-' + (ghost.notifications.length + 1)
|
||||
};
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
delete req.session.user;
|
||||
res.set({
|
||||
"X-Cache-Invalidate": "/*"
|
||||
});
|
||||
res.redirect('/ghost/signup/');
|
||||
});
|
||||
}, function resetFailure(error) {
|
||||
var notification = {
|
||||
type: 'error',
|
||||
message: error.message || error,
|
||||
status: 'persistent',
|
||||
id: 'per-' + (ghost.notifications.length + 1)
|
||||
};
|
||||
|
||||
return api.notifications.add(notification).then(function () {
|
||||
res.redirect('/ghost/debug/');
|
||||
});
|
||||
|
|
|
@ -66,7 +66,7 @@ frontendControllers = {
|
|||
'single': function (req, res, next) {
|
||||
api.posts.read({'slug': req.params.slug}).then(function (post) {
|
||||
if (post) {
|
||||
ghost.doFilter('prePostsRender', post.toJSON(), function (post) {
|
||||
ghost.doFilter('prePostsRender', post, function (post) {
|
||||
res.render('post', {post: post});
|
||||
});
|
||||
} else {
|
||||
|
@ -88,7 +88,7 @@ frontendControllers = {
|
|||
title: ghost.settings('title'),
|
||||
description: ghost.settings('description'),
|
||||
generator: 'Ghost v' + res.locals.version,
|
||||
author: user ? user.attributes.name : null,
|
||||
author: user ? user.name : null,
|
||||
feed_url: url.resolve(siteUrl, '/rss/'),
|
||||
site_url: siteUrl,
|
||||
ttl: '60'
|
||||
|
|
|
@ -2,21 +2,21 @@ var when = require('when'),
|
|||
_ = require('underscore'),
|
||||
migration = require('../migration'),
|
||||
client = require('../../models/base').client,
|
||||
knex = require('../../models/base').Knex,
|
||||
knex = require('../../models/base').knex,
|
||||
|
||||
exporter;
|
||||
|
||||
function getTablesFromSqlite3() {
|
||||
return knex.Raw("select * from sqlite_master where type = 'table'").then(function (response) {
|
||||
return _.reject(_.pluck(response, 'tbl_name'), function (name) {
|
||||
return knex.raw("select * from sqlite_master where type = 'table'").then(function (response) {
|
||||
return _.reject(_.pluck(response[0], 'tbl_name'), function (name) {
|
||||
return name === 'sqlite_sequence';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTablesFromMySQL() {
|
||||
return knex.Raw('show tables').then(function (response) {
|
||||
return _.flatten(_.map(response, function (entry) {
|
||||
return knex.raw("show tables").then(function (response) {
|
||||
return _.flatten(_.map(response[0], function (entry) {
|
||||
return _.values(entry);
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var when = require('when'),
|
||||
knex = require('../../models/base').Knex,
|
||||
knex = require('../../models/base').knex,
|
||||
up,
|
||||
down;
|
||||
|
||||
|
@ -7,7 +7,7 @@ up = function () {
|
|||
|
||||
return when.all([
|
||||
|
||||
knex.Schema.createTable('posts', function (t) {
|
||||
knex.schema.createTable('posts', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('title', 150).notNull();
|
||||
|
@ -30,7 +30,7 @@ up = function () {
|
|||
t.integer('published_by').nullable();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('users', function (t) {
|
||||
knex.schema.createTable('users', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('name', 150).notNull();
|
||||
|
@ -54,7 +54,7 @@ up = function () {
|
|||
t.integer('updated_by').nullable();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('roles', function (t) {
|
||||
knex.schema.createTable('roles', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('name', 150).notNull();
|
||||
|
@ -65,13 +65,13 @@ up = function () {
|
|||
t.integer('updated_by').nullable();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('roles_users', function (t) {
|
||||
knex.schema.createTable('roles_users', function (t) {
|
||||
t.increments().primary();
|
||||
t.integer('role_id').notNull();
|
||||
t.integer('user_id').notNull();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('permissions', function (t) {
|
||||
knex.schema.createTable('permissions', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('name', 150).notNull();
|
||||
|
@ -84,19 +84,19 @@ up = function () {
|
|||
t.integer('updated_by').nullable();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('permissions_users', function (t) {
|
||||
knex.schema.createTable('permissions_users', function (t) {
|
||||
t.increments().primary();
|
||||
t.integer('user_id').notNull();
|
||||
t.integer('permission_id').notNull();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('permissions_roles', function (t) {
|
||||
knex.schema.createTable('permissions_roles', function (t) {
|
||||
t.increments().primary();
|
||||
t.integer('role_id').notNull();
|
||||
t.integer('permission_id').notNull();
|
||||
}),
|
||||
|
||||
knex.Schema.createTable('settings', function (t) {
|
||||
knex.schema.createTable('settings', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('key', 150).notNull().unique();
|
||||
|
@ -107,7 +107,7 @@ up = function () {
|
|||
t.dateTime('updated_at').nullable();
|
||||
t.integer('updated_by').nullable();
|
||||
}),
|
||||
knex.Schema.createTable('tags', function (t) {
|
||||
knex.schema.createTable('tags', function (t) {
|
||||
t.increments().primary();
|
||||
t.string('uuid', 36).notNull();
|
||||
t.string('name', 150).notNull();
|
||||
|
@ -120,30 +120,31 @@ up = function () {
|
|||
t.integer('created_by').notNull();
|
||||
t.dateTime('updated_at').nullable();
|
||||
t.integer('updated_by').nullable();
|
||||
}),
|
||||
knex.Schema.createTable('posts_tags', function (t) {
|
||||
})
|
||||
]).then(function () {
|
||||
return knex.schema.createTable('posts_tags', function (t) {
|
||||
t.increments().primary();
|
||||
t.integer('post_id').notNull().unsigned().references('id').inTable('posts');
|
||||
t.integer('tag_id').notNull().unsigned().references('id').inTable('tags');
|
||||
})
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
down = function () {
|
||||
return when.all([
|
||||
knex.Schema.dropTableIfExists('posts_tags'),
|
||||
knex.Schema.dropTableIfExists('roles_users'),
|
||||
knex.Schema.dropTableIfExists('permissions_users'),
|
||||
knex.Schema.dropTableIfExists('permissions_roles'),
|
||||
knex.Schema.dropTableIfExists('users')
|
||||
knex.schema.dropTableIfExists('posts_tags'),
|
||||
knex.schema.dropTableIfExists('roles_users'),
|
||||
knex.schema.dropTableIfExists('permissions_users'),
|
||||
knex.schema.dropTableIfExists('permissions_roles'),
|
||||
knex.schema.dropTableIfExists('users')
|
||||
|
||||
]).then(function () {
|
||||
return when.all([
|
||||
knex.Schema.dropTableIfExists('roles'),
|
||||
knex.Schema.dropTableIfExists('settings'),
|
||||
knex.Schema.dropTableIfExists('permissions'),
|
||||
knex.Schema.dropTableIfExists('tags'),
|
||||
knex.Schema.dropTableIfExists('posts')
|
||||
knex.schema.dropTableIfExists('roles'),
|
||||
knex.schema.dropTableIfExists('settings'),
|
||||
knex.schema.dropTableIfExists('permissions'),
|
||||
knex.schema.dropTableIfExists('tags'),
|
||||
knex.schema.dropTableIfExists('posts')
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ var _ = require('underscore'),
|
|||
when = require('when'),
|
||||
series = require('when/sequence'),
|
||||
errors = require('../../errorHandling'),
|
||||
knex = require('../../models/base').Knex,
|
||||
knex = require('../../models/base').knex,
|
||||
|
||||
defaultSettings = require('../default-settings'),
|
||||
Settings = require('../../models/settings').Settings,
|
||||
|
@ -30,7 +30,7 @@ function getDefaultDatabaseVersion() {
|
|||
// The migration version number according to the database
|
||||
// This is what the database is currently at and may need to be updated
|
||||
function getDatabaseVersion() {
|
||||
return knex.Schema.hasTable('settings').then(function (exists) {
|
||||
return knex.schema.hasTable('settings').then(function (exists) {
|
||||
// Check for the current version from the settings table
|
||||
if (exists) {
|
||||
// Temporary code to deal with old databases with currentVersion settings
|
||||
|
|
|
@ -29,6 +29,26 @@ errors = {
|
|||
throw err;
|
||||
},
|
||||
|
||||
logWarn: function (warn, context, help) {
|
||||
if ((process.env.NODE_ENV === 'development' ||
|
||||
process.env.NODE_ENV === 'staging' ||
|
||||
process.env.NODE_ENV === 'production')) {
|
||||
|
||||
console.log('\nWarning:'.yellow, warn.yellow);
|
||||
|
||||
if (context) {
|
||||
console.log(context.white);
|
||||
}
|
||||
|
||||
if (help) {
|
||||
console.log(help.green);
|
||||
}
|
||||
|
||||
// add a new line
|
||||
console.log('');
|
||||
}
|
||||
},
|
||||
|
||||
logError: function (err, context, help) {
|
||||
var stack = err ? err.stack : null;
|
||||
err = err.message || err || 'Unknown';
|
||||
|
@ -41,7 +61,7 @@ errors = {
|
|||
console.error('\nERROR:'.red, err.red);
|
||||
|
||||
if (context) {
|
||||
console.error(context);
|
||||
console.error(context.white);
|
||||
}
|
||||
|
||||
if (help) {
|
||||
|
@ -133,7 +153,7 @@ errors = {
|
|||
}
|
||||
|
||||
// Are we admin? If so, don't worry about the user template
|
||||
if (res.isAdmin || userErrorTemplateExists === true) {
|
||||
if ((res.isAdmin && req.session.user) || userErrorTemplateExists === true) {
|
||||
return renderErrorInt();
|
||||
}
|
||||
|
||||
|
@ -150,27 +170,29 @@ errors = {
|
|||
return renderErrorInt();
|
||||
}
|
||||
|
||||
// Message only displays the first time an error is triggered.
|
||||
errors.logError(
|
||||
"Theme error template not found",
|
||||
null,
|
||||
"Add an error.hbs template to the theme for customised errors."
|
||||
);
|
||||
|
||||
renderErrorInt(defaultErrorTemplatePath);
|
||||
});
|
||||
},
|
||||
|
||||
render404Page: function (req, res, next) {
|
||||
var message = res.isAdmin ? "No Ghost Found" : "Page Not Found";
|
||||
this.renderErrorPage(404, message, req, res, next);
|
||||
error404: function (req, res, next) {
|
||||
var message = res.isAdmin && req.session.user ? "No Ghost Found" : "Page Not Found";
|
||||
|
||||
if (req.method === 'GET') {
|
||||
this.renderErrorPage(404, message, req, res, next);
|
||||
} else {
|
||||
res.send(404, message);
|
||||
}
|
||||
},
|
||||
|
||||
render500Page: function (err, req, res, next) {
|
||||
if (!err || !(err instanceof Error)) {
|
||||
next();
|
||||
error500: function (err, req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
if (!err || !(err instanceof Error)) {
|
||||
next();
|
||||
}
|
||||
errors.renderErrorPage(500, err, req, res, next);
|
||||
} else {
|
||||
res.send(500, err);
|
||||
}
|
||||
errors.renderErrorPage(500, err, req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -182,8 +204,8 @@ _.bindAll(
|
|||
'logAndThrowError',
|
||||
'logErrorWithRedirect',
|
||||
'renderErrorPage',
|
||||
'render404Page',
|
||||
'render500Page'
|
||||
'error404',
|
||||
'error500'
|
||||
);
|
||||
|
||||
module.exports = errors;
|
|
@ -1,4 +1,4 @@
|
|||
var GhostBookshelf,
|
||||
var ghostBookshelf,
|
||||
Bookshelf = require('bookshelf'),
|
||||
when = require('when'),
|
||||
moment = require('moment'),
|
||||
|
@ -8,16 +8,15 @@ var GhostBookshelf,
|
|||
Validator = require('validator').Validator,
|
||||
sanitize = require('validator').sanitize;
|
||||
|
||||
// Initializes Bookshelf as its own instance, so we can modify the Models and not mess up
|
||||
// others' if they're using the library outside of ghost.
|
||||
GhostBookshelf = Bookshelf.Initialize('ghost', config[process.env.NODE_ENV || 'development'].database);
|
||||
GhostBookshelf.client = config[process.env.NODE_ENV].database.client;
|
||||
// Initializes a new Bookshelf instance, for reference elsewhere in Ghost.
|
||||
ghostBookshelf = Bookshelf.initialize(config[process.env.NODE_ENV || 'development'].database);
|
||||
ghostBookshelf.client = config[process.env.NODE_ENV].database.client;
|
||||
|
||||
GhostBookshelf.validator = new Validator();
|
||||
ghostBookshelf.validator = new Validator();
|
||||
|
||||
// The Base Model which other Ghost objects will inherit from,
|
||||
// including some convenience functions as static properties on the model.
|
||||
GhostBookshelf.Model = GhostBookshelf.Model.extend({
|
||||
ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
||||
|
||||
hasTimestamps: true,
|
||||
|
||||
|
@ -145,7 +144,7 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
|
|||
*/
|
||||
findAll: function (options) {
|
||||
options = options || {};
|
||||
return GhostBookshelf.Collection.forge([], {model: this}).fetch(options);
|
||||
return ghostBookshelf.Collection.forge([], {model: this}).fetch(options);
|
||||
},
|
||||
|
||||
browse: function () {
|
||||
|
@ -212,4 +211,4 @@ GhostBookshelf.Model = GhostBookshelf.Model.extend({
|
|||
|
||||
});
|
||||
|
||||
module.exports = GhostBookshelf;
|
||||
module.exports = ghostBookshelf;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
var GhostBookshelf = require('./base'),
|
||||
var ghostBookshelf = require('./base'),
|
||||
User = require('./user').User,
|
||||
Role = require('./role').Role,
|
||||
Permission,
|
||||
Permissions;
|
||||
|
||||
Permission = GhostBookshelf.Model.extend({
|
||||
Permission = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'permissions',
|
||||
|
||||
|
@ -14,7 +14,7 @@ Permission = GhostBookshelf.Model.extend({
|
|||
|
||||
validate: function () {
|
||||
// TODO: validate object_type, action_type and object_id
|
||||
GhostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('name'), "Permission name cannot be blank").notEmpty();
|
||||
},
|
||||
|
||||
roles: function () {
|
||||
|
@ -26,7 +26,7 @@ Permission = GhostBookshelf.Model.extend({
|
|||
}
|
||||
});
|
||||
|
||||
Permissions = GhostBookshelf.Collection.extend({
|
||||
Permissions = ghostBookshelf.Collection.extend({
|
||||
model: Permission
|
||||
});
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@ var Post,
|
|||
config = require('../../../config'),
|
||||
Tag = require('./tag').Tag,
|
||||
Tags = require('./tag').Tags,
|
||||
GhostBookshelf = require('./base');
|
||||
ghostBookshelf = require('./base');
|
||||
|
||||
Post = GhostBookshelf.Model.extend({
|
||||
Post = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'posts',
|
||||
|
||||
|
@ -38,7 +38,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
},
|
||||
|
||||
validate: function () {
|
||||
GhostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('title'), "Post title cannot be blank").notEmpty();
|
||||
|
||||
return true;
|
||||
},
|
||||
|
@ -61,7 +61,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
this.set('published_by', 1);
|
||||
}
|
||||
|
||||
GhostBookshelf.Model.prototype.saving.call(this);
|
||||
ghostBookshelf.Model.prototype.saving.call(this);
|
||||
|
||||
if (this.hasChanged('slug')) {
|
||||
// Pass the new slug through the generator to strip illegal characters, detect duplicates
|
||||
|
@ -80,7 +80,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
this.set('author_id', 1);
|
||||
}
|
||||
|
||||
GhostBookshelf.Model.prototype.creating.call(this);
|
||||
ghostBookshelf.Model.prototype.creating.call(this);
|
||||
|
||||
if (!this.get('slug')) {
|
||||
// Generating a slug requires a db call to look for conflicting slugs
|
||||
|
@ -174,7 +174,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
findAll: function (options) {
|
||||
options = options || {};
|
||||
options.withRelated = [ 'author', 'user', 'tags' ];
|
||||
return GhostBookshelf.Model.findAll.call(this, options);
|
||||
return ghostBookshelf.Model.findAll.call(this, options);
|
||||
},
|
||||
|
||||
// #### findOne
|
||||
|
@ -182,7 +182,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
findOne: function (args, options) {
|
||||
options = options || {};
|
||||
options.withRelated = [ 'author', 'user', 'tags' ];
|
||||
return GhostBookshelf.Model.findOne.call(this, args, options);
|
||||
return ghostBookshelf.Model.findOne.call(this, args, options);
|
||||
},
|
||||
|
||||
// #### findPage
|
||||
|
@ -251,7 +251,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
|
||||
// After we're done, we need to figure out what
|
||||
// the limits are for the pagination values.
|
||||
qb = GhostBookshelf.Knex(_.result(collection, 'tableName'));
|
||||
qb = ghostBookshelf.knex(_.result(collection, 'tableName'));
|
||||
|
||||
if (opts.where) {
|
||||
qb.where(opts.where);
|
||||
|
@ -328,7 +328,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
},
|
||||
|
||||
add: function (newPostData, options) {
|
||||
return GhostBookshelf.Model.add.call(this, newPostData, options).tap(function (post) {
|
||||
return ghostBookshelf.Model.add.call(this, newPostData, options).tap(function (post) {
|
||||
// associated models can't be created until the post has an ID, so run this after
|
||||
return post.updateTags(newPostData.tags);
|
||||
});
|
||||
|
@ -349,7 +349,7 @@ Post = GhostBookshelf.Model.extend({
|
|||
|
||||
});
|
||||
|
||||
Posts = GhostBookshelf.Collection.extend({
|
||||
Posts = ghostBookshelf.Collection.extend({
|
||||
|
||||
model: Post
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
var User = require('./user').User,
|
||||
Permission = require('./permission').Permission,
|
||||
GhostBookshelf = require('./base'),
|
||||
ghostBookshelf = require('./base'),
|
||||
Role,
|
||||
Roles;
|
||||
|
||||
Role = GhostBookshelf.Model.extend({
|
||||
Role = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'roles',
|
||||
|
||||
permittedAttributes: ['id', 'uuid', 'name', 'description', 'created_at', 'created_by', 'updated_at', 'updated_by'],
|
||||
|
||||
validate: function () {
|
||||
GhostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
|
||||
GhostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('name'), "Role name cannot be blank").notEmpty();
|
||||
ghostBookshelf.validator.check(this.get('description'), "Role description cannot be blank").notEmpty();
|
||||
},
|
||||
|
||||
users: function () {
|
||||
|
@ -24,7 +24,7 @@ Role = GhostBookshelf.Model.extend({
|
|||
}
|
||||
});
|
||||
|
||||
Roles = GhostBookshelf.Collection.extend({
|
||||
Roles = ghostBookshelf.Collection.extend({
|
||||
model: Role
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
var Settings,
|
||||
GhostBookshelf = require('./base'),
|
||||
validator = GhostBookshelf.validator,
|
||||
ghostBookshelf = require('./base'),
|
||||
validator = ghostBookshelf.validator,
|
||||
uuid = require('node-uuid'),
|
||||
_ = require('underscore'),
|
||||
errors = require('../errorHandling'),
|
||||
|
@ -29,7 +29,7 @@ defaultSettings = parseDefaultSettings();
|
|||
|
||||
// Each setting is saved as a separate row in the database,
|
||||
// but the overlying API treats them as a single key:value mapping
|
||||
Settings = GhostBookshelf.Model.extend({
|
||||
Settings = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'settings',
|
||||
|
||||
|
@ -83,7 +83,7 @@ Settings = GhostBookshelf.Model.extend({
|
|||
this.set('value', this.sanitize('value'));
|
||||
}
|
||||
|
||||
return GhostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
return ghostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
}
|
||||
|
||||
}, {
|
||||
|
@ -92,7 +92,7 @@ Settings = GhostBookshelf.Model.extend({
|
|||
if (!_.isObject(_key)) {
|
||||
_key = { key: _key };
|
||||
}
|
||||
return GhostBookshelf.Model.read.call(this, _key);
|
||||
return ghostBookshelf.Model.read.call(this, _key);
|
||||
},
|
||||
|
||||
edit: function (_data) {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
var Tag,
|
||||
Tags,
|
||||
Posts = require('./post').Posts,
|
||||
GhostBookshelf = require('./base');
|
||||
ghostBookshelf = require('./base');
|
||||
|
||||
Tag = GhostBookshelf.Model.extend({
|
||||
Tag = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'tags',
|
||||
|
||||
|
@ -20,7 +20,7 @@ Tag = GhostBookshelf.Model.extend({
|
|||
creating: function () {
|
||||
var self = this;
|
||||
|
||||
GhostBookshelf.Model.prototype.creating.call(this);
|
||||
ghostBookshelf.Model.prototype.creating.call(this);
|
||||
|
||||
if (!this.get('slug')) {
|
||||
// Generating a slug requires a db call to look for conflicting slugs
|
||||
|
@ -36,7 +36,7 @@ Tag = GhostBookshelf.Model.extend({
|
|||
}
|
||||
});
|
||||
|
||||
Tags = GhostBookshelf.Collection.extend({
|
||||
Tags = ghostBookshelf.Collection.extend({
|
||||
|
||||
model: Tag
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ var User,
|
|||
nodefn = require('when/node/function'),
|
||||
bcrypt = require('bcrypt-nodejs'),
|
||||
Posts = require('./post').Posts,
|
||||
GhostBookshelf = require('./base'),
|
||||
ghostBookshelf = require('./base'),
|
||||
Role = require('./role').Role,
|
||||
Permission = require('./permission').Permission;
|
||||
|
||||
|
||||
function validatePasswordLength(password) {
|
||||
try {
|
||||
GhostBookshelf.validator.check(password, "Your must be at least 8 characters long.").len(8);
|
||||
ghostBookshelf.validator.check(password, "Your must be at least 8 characters long.").len(8);
|
||||
} catch (error) {
|
||||
return when.reject(error);
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ function validatePasswordLength(password) {
|
|||
return when.resolve();
|
||||
}
|
||||
|
||||
User = GhostBookshelf.Model.extend({
|
||||
User = ghostBookshelf.Model.extend({
|
||||
|
||||
tableName: 'users',
|
||||
|
||||
|
@ -33,10 +33,10 @@ User = GhostBookshelf.Model.extend({
|
|||
],
|
||||
|
||||
validate: function () {
|
||||
GhostBookshelf.validator.check(this.get('email'), "Please enter a valid email address. That one looks a bit dodgy.").isEmail();
|
||||
GhostBookshelf.validator.check(this.get('bio'), "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters.").len(0, 200);
|
||||
ghostBookshelf.validator.check(this.get('email'), "Please enter a valid email address. That one looks a bit dodgy.").isEmail();
|
||||
ghostBookshelf.validator.check(this.get('bio'), "We're not writing a novel here! I'm afraid your bio has to stay under 200 characters.").len(0, 200);
|
||||
if (this.get('website') && this.get('website').length > 0) {
|
||||
GhostBookshelf.validator.check(this.get('website'), "Looks like your website is not actually a website. Try again?").isUrl();
|
||||
ghostBookshelf.validator.check(this.get('website'), "Looks like your website is not actually a website. Try again?").isUrl();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
@ -44,7 +44,7 @@ User = GhostBookshelf.Model.extend({
|
|||
creating: function () {
|
||||
var self = this;
|
||||
|
||||
GhostBookshelf.Model.prototype.creating.call(this);
|
||||
ghostBookshelf.Model.prototype.creating.call(this);
|
||||
|
||||
if (!this.get('slug')) {
|
||||
// Generating a slug requires a db call to look for conflicting slugs
|
||||
|
@ -63,7 +63,7 @@ User = GhostBookshelf.Model.extend({
|
|||
this.set('website', this.sanitize('website'));
|
||||
this.set('bio', this.sanitize('bio'));
|
||||
|
||||
return GhostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
return ghostBookshelf.Model.prototype.saving.apply(this, arguments);
|
||||
},
|
||||
|
||||
posts: function () {
|
||||
|
@ -111,7 +111,7 @@ User = GhostBookshelf.Model.extend({
|
|||
// Assign the hashed password
|
||||
userData.password = hash;
|
||||
// Save the user with the hashed password
|
||||
return GhostBookshelf.Model.add.call(self, userData);
|
||||
return ghostBookshelf.Model.add.call(self, userData);
|
||||
}).then(function (addedUser) {
|
||||
// Assign the userData to our created user so we can pass it back
|
||||
userData = addedUser;
|
||||
|
@ -135,8 +135,8 @@ User = GhostBookshelf.Model.extend({
|
|||
// }
|
||||
// return nodefn.call(bcrypt.hash, _user.password, null, null).then(function (hash) {
|
||||
// userData.password = hash;
|
||||
// GhostBookshelf.Model.add.call(UserRole, userRoles);
|
||||
// return GhostBookshelf.Model.add.call(User, userData);
|
||||
// ghostBookshelf.Model.add.call(UserRole, userRoles);
|
||||
// return ghostBookshelf.Model.add.call(User, userData);
|
||||
// }, errors.logAndThrowError);
|
||||
// }, errors.logAndThrowError);
|
||||
|
||||
|
@ -239,7 +239,7 @@ User = GhostBookshelf.Model.extend({
|
|||
|
||||
});
|
||||
|
||||
Users = GhostBookshelf.Collection.extend({
|
||||
Users = ghostBookshelf.Collection.extend({
|
||||
model: User
|
||||
});
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
</header>
|
||||
<section class="content">
|
||||
<form id="settings-export">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label>Export</label>
|
||||
|
@ -25,11 +26,12 @@
|
|||
</fieldset>
|
||||
</form>
|
||||
<form id="settings-import" method="post" action="/ghost/debug/db/import/" enctype="multipart/form-data">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label>Import</label>
|
||||
<input type="file" class="button-add" name="importfile"></input>
|
||||
<input type="submit" class="button-save" value="Import"></input>
|
||||
<input type="file" class="button-add" name="importfile" />
|
||||
<input type="submit" class="button-save" value="Import" />
|
||||
<p>Import from another Ghost installation. If you import a user, this will replace the current user & log you out.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="csrf-param" content="{{csrfToken}}">
|
||||
|
||||
<title>Ghost Admin</title>
|
||||
|
||||
|
|
|
@ -31,26 +31,26 @@ describe('Admin Controller', function() {
|
|||
});
|
||||
|
||||
describe('can not upload invalid file', function() {
|
||||
it('should return 404 for invalid file type', function() {
|
||||
it('should return 403 for invalid file type', function() {
|
||||
res.send = sinon.stub();
|
||||
req.files.uploadimage.name = 'INVALID.FILE';
|
||||
req.files.uploadimage.type = 'application/octet-stream'
|
||||
admin.uploader(req, res);
|
||||
res.send.calledOnce.should.be.true;
|
||||
res.send.args[0][0].should.equal(404);
|
||||
res.send.args[0][1].should.equal('Invalid filetype');
|
||||
res.send.args[0][0].should.equal(403);
|
||||
res.send.args[0][1].should.equal('Invalid file type');
|
||||
});
|
||||
});
|
||||
|
||||
describe('can not upload file with valid extension but invalid type', function() {
|
||||
it('should return 404 for invalid file type', function() {
|
||||
it('should return 403 for invalid file type', function() {
|
||||
res.send = sinon.stub();
|
||||
req.files.uploadimage.name = 'INVALID.jpg';
|
||||
req.files.uploadimage.type = 'application/octet-stream'
|
||||
admin.uploader(req, res);
|
||||
res.send.calledOnce.should.be.true;
|
||||
res.send.args[0][0].should.equal(404);
|
||||
res.send.args[0][1].should.equal('Invalid filetype');
|
||||
res.send.args[0][0].should.equal(403);
|
||||
res.send.args[0][1].should.equal('Invalid file type');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ var testUtils = require('./testUtils'),
|
|||
errors = require('../../server/errorHandling'),
|
||||
|
||||
// Stuff we are testing
|
||||
knex = require("../../server/models/base").Knex,
|
||||
knex = require("../../server/models/base").knex,
|
||||
migration = require('../../server/data/migration'),
|
||||
exporter = require('../../server/data/export'),
|
||||
importer = require('../../server/data/import'),
|
||||
|
|
|
@ -5,7 +5,7 @@ var testUtils = require('./testUtils'),
|
|||
|
||||
// Stuff we are testing
|
||||
Models = require('../../server/models'),
|
||||
knex = require('../../server/models/base').Knex;
|
||||
knex = require('../../server/models/base').knex;
|
||||
|
||||
describe('Settings Model', function () {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var knex = require('../../server/models/base').Knex,
|
||||
var knex = require('../../server/models/base').knex,
|
||||
when = require('when'),
|
||||
migration = require("../../server/data/migration/"),
|
||||
Settings = require('../../server/models/settings').Settings,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name" : "ghost",
|
||||
"version" : "0.3.2",
|
||||
"version" : "0.3.3",
|
||||
"description" : "Just a blogging platform.",
|
||||
"author" : "Ghost Foundation",
|
||||
"homepage" : "http://ghost.org",
|
||||
|
@ -30,8 +30,8 @@
|
|||
"underscore": "1.5.1",
|
||||
"showdown": "0.3.1",
|
||||
"sqlite3": "2.1.16",
|
||||
"bookshelf": "0.3.1",
|
||||
"knex": "0.2.7-alpha",
|
||||
"bookshelf": "0.5.7",
|
||||
"knex": "0.4.11",
|
||||
"when": "2.2.1",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"node-uuid": "1.4.0",
|
||||
|
|
Loading…
Add table
Reference in a new issue