Merge pull request #1627 from sebgie/issue#755

remove ghost.settings and ghost.notifications
This commit is contained in:
Hannah Wolfe 2013-12-07 03:36:08 -08:00
commit 7a46c36045
24 changed files with 963 additions and 875 deletions

View File

@ -11,6 +11,7 @@ var config = require('./server/config'),
models = require('./server/models'),
permissions = require('./server/permissions'),
uuid = require('node-uuid'),
api = require('./server/api'),
// Variables
Ghost,
@ -27,39 +28,36 @@ Ghost = function () {
if (!instance) {
instance = this;
// Holds the persistent notifications
instance.notifications = [];
instance.globals = {};
// Holds the dbhash (mainly used for cookie secret)
instance.dbHash = undefined;
_.extend(instance, {
// there's no management here to be sure this has loaded
settings: function (key) {
if (key) {
return instance.settingsCache[key].value;
}
return instance.settingsCache;
},
dataProvider: models,
blogGlobals: function () {
var localPath = url.parse(config().url).path;
// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}
/* this is a bit of a hack until we have a better way to combine settings and config
* this data is what becomes globally available to themes */
return {
url: config().url.replace(/\/$/, ''),
path: localPath,
title: instance.settings('title'),
description: instance.settings('description'),
logo: instance.settings('logo'),
cover: instance.settings('cover')
};
return instance.globals;
},
getGlobals: function () {
return when.all([
api.settings.read('title'),
api.settings.read('description'),
api.settings.read('logo'),
api.settings.read('cover')
]).then(function (globals) {
instance.globals.path = config.paths().path;
instance.globals.url = config().url;
instance.globals.title = globals[0].value;
instance.globals.description = globals[1].value;
instance.globals.logo = globals[2] ? globals[2].value : '';
instance.globals.cover = globals[3] ? globals[3].value : '';
return;
});
}
});
}
@ -82,13 +80,12 @@ Ghost.prototype.init = function () {
'See <a href="http://docs.ghost.org/">http://docs.ghost.org</a> for instructions.'
];
self.notifications.push({
return api.notifications.add({
type: 'info',
message: firstRunMessage.join(' '),
status: 'persistent',
id: 'ghost-first-run'
});
return when.resolve();
}
function initDbHashAndFirstRun() {
@ -112,16 +109,17 @@ Ghost.prototype.init = function () {
// Initialise the models
self.dataProvider.init(),
// Calculate paths
config.paths.updatePaths()
config.paths.updatePaths(config().url)
).then(function () {
// Populate any missing default settings
return models.Settings.populateDefaults();
}).then(function () {
// Initialize the settings cache
return self.updateSettingsCache();
return api.init();
}).then(function () {
return self.getGlobals();
}).then(function () {
// Update path to activeTheme
config.paths.setActiveTheme(self);
return when.join(
// Check for or initialise a dbHash.
initDbHashAndFirstRun(),
@ -131,60 +129,4 @@ Ghost.prototype.init = function () {
}).otherwise(errors.logAndThrowError);
};
// Maintain the internal cache of the settings object
Ghost.prototype.updateSettingsCache = function (settings) {
var self = this;
settings = settings || {};
if (!_.isEmpty(settings)) {
_.map(settings, function (setting, key) {
self.settingsCache[key].value = setting.value;
});
} else {
// TODO: this should use api.browse
return when(models.Settings.findAll()).then(function (result) {
return when(self.readSettingsResult(result)).then(function (s) {
self.settingsCache = s;
});
});
}
};
Ghost.prototype.readSettingsResult = function (result) {
var settings = {};
return when(_.map(result.models, function (member) {
if (!settings.hasOwnProperty(member.attributes.key)) {
var val = {};
val.value = member.attributes.value;
val.type = member.attributes.type;
settings[member.attributes.key] = val;
}
})).then(function () {
return when(config.paths().availableThemes).then(function (themes) {
var themeKeys = Object.keys(themes),
res = [],
i,
item;
for (i = 0; i < themeKeys.length; i += 1) {
//do not include hidden files
if (themeKeys[i].indexOf('.') !== 0) {
item = {};
item.name = themeKeys[i];
//data about files currently not used
//item.details = themes[themeKeys[i]];
if (themeKeys[i] === settings.activeTheme.value) {
item.active = true;
}
res.push(item);
}
}
settings.availableThemes = {};
settings.availableThemes.value = res;
settings.availableThemes.type = 'theme';
return settings;
});
});
};
module.exports = Ghost;

View File

@ -1,5 +1,4 @@
var Ghost = require('../../ghost'),
dataExport = require('../data/export'),
var dataExport = require('../data/export'),
dataImport = require('../data/import'),
api = require('../api'),
fs = require('fs-extra'),
@ -8,8 +7,8 @@ var Ghost = require('../../ghost'),
nodefn = require('when/node/function'),
_ = require('underscore'),
schema = require('../data/schema'),
config = require('../config'),
ghost = new Ghost(),
db;
db = {
@ -27,15 +26,16 @@ db = {
res.download(exportedFilePath, 'GhostData.json');
}).otherwise(function (error) {
// Notify of an error if it occurs
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/');
return api.notification.browse().then(function (notifications) {
var notification = {
type: 'error',
message: error.message || error,
status: 'persistent',
id: 'per-' + (notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect(config.paths().webroot + '/ghost/debug/');
});
});
});
},
@ -51,15 +51,16 @@ db = {
* - If the size is 0
* - If the name doesn't have json in it
*/
notification = {
type: 'error',
message: "Must select a .json file to import",
status: 'persistent',
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect('/ghost/debug/');
return api.notification.browse().then(function (notifications) {
notification = {
type: 'error',
message: "Must select a .json file to import",
status: 'persistent',
id: 'per-' + (notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect(config.paths().webroot + '/ghost/debug/');
});
});
}
@ -125,32 +126,35 @@ db = {
});
})
.then(function importSuccess() {
notification = {
type: 'success',
message: "Data imported. Log in with the user details you imported",
status: 'persistent',
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notification.browse().then(function (notifications) {
notification = {
type: 'success',
message: "Data imported. Log in with the user details you imported",
status: 'persistent',
id: 'per-' + (notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
req.session.destroy();
res.set({
"X-Cache-Invalidate": "/*"
return api.notifications.add(notification).then(function () {
req.session.destroy();
res.set({
"X-Cache-Invalidate": "/*"
});
res.redirect(config.paths().webroot + '/ghost/signin/');
});
res.redirect('/ghost/signin/');
});
}, function importFailure(error) {
// Notify of an error if it occurs
notification = {
type: 'error',
message: error.message || error,
status: 'persistent',
id: 'per-' + (ghost.notifications.length + 1)
};
return api.notification.browse().then(function (notifications) {
// Notify of an error if it occurs
notification = {
type: 'error',
message: error.message || error,
status: 'persistent',
id: 'per-' + (notifications.length + 1)
};
return api.notifications.add(notification).then(function () {
res.redirect('/ghost/debug/');
return api.notifications.add(notification).then(function () {
res.redirect(config.paths().webroot + '/ghost/debug/');
});
});
});
}

View File

@ -1,397 +1,19 @@
// # Ghost Data API
// Provides access to the data model
var Ghost = require('../../ghost'),
_ = require('underscore'),
when = require('when'),
errors = require('../errorHandling'),
permissions = require('../permissions'),
db = require('./db'),
canThis = permissions.canThis,
ghost = new Ghost(),
dataProvider = ghost.dataProvider,
posts,
users,
tags,
notifications,
settings,
var _ = require('underscore'),
when = require('when'),
errors = require('../errorHandling'),
db = require('./db'),
settings = require('./settings'),
notifications = require('./notifications'),
dataProvider = require('../models'),
config = require('../config'),
posts = require('./posts'),
users = require('./users'),
tags = require('./tags'),
requestHandler,
settingsObject,
settingsCollection,
settingsFilter,
filteredUserAttributes = ['password', 'created_by', 'updated_by', 'last_login'],
ONE_DAY = 86400000;
// ## Posts
posts = {
// #### Browse
// **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).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
// **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).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 when.reject({errorCode: 404, message: 'Post not found'});
});
},
// #### Edit
// **takes:** a json object with all the properties which should be updated
edit: function edit(postData) {
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'});
}
var self = this;
return canThis(self.user).edit.post(postData.id).then(function () {
return dataProvider.Post.edit(postData).then(function (result) {
if (result) {
var omitted = result.toJSON();
omitted.author = _.omit(omitted.author, filteredUserAttributes);
omitted.user = _.omit(omitted.user, filteredUserAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'Post not found'});
}).otherwise(function (error) {
return dataProvider.Post.findOne({id: postData.id, status: 'all'}).then(function (result) {
if (!result) {
return when.reject({errorCode: 404, message: 'Post not found'});
}
return when.reject({message: error.message});
});
});
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'});
});
},
// #### Add
// **takes:** a json object representing a post,
add: function add(postData) {
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'});
}
return canThis(this.user).create.post().then(function () {
return dataProvider.Post.add(postData);
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'});
});
},
// #### Destroy
// **takes:** an identifier (id or slug?)
destroy: function destroy(args) {
// **returns:** a promise for a json response with the id of the deleted post
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'});
}
return canThis(this.user).remove.post(args.id).then(function () {
return when(posts.read({id : args.id, status: 'all'})).then(function (result) {
return dataProvider.Post.destroy(args.id).then(function () {
var deletedObj = {};
deletedObj.id = result.id;
deletedObj.slug = result.slug;
return deletedObj;
});
});
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'});
});
}
};
// ## Users
users = {
// #### Browse
// **takes:** options object
browse: function browse(options) {
// **returns:** a promise for a collection of users in a json object
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
// **takes:** an identifier (id or slug?)
read: function read(args) {
// **returns:** a promise for a single user in a json object
if (args.id === 'me') {
args = {id: this.user};
}
return dataProvider.User.read(args).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredUserAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'User not found'});
});
},
// #### Edit
// **takes:** a json object representing a user
edit: function edit(userData) {
// **returns:** a promise for the resulting user in a json object
userData.id = this.user;
return dataProvider.User.edit(userData).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredUserAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'User not found'});
});
},
// #### Add
// **takes:** a json object representing a user
add: function add(userData) {
// **returns:** a promise for the resulting user in a json object
return dataProvider.User.add(userData);
},
// #### Check
// Checks a password matches the given email address
// **takes:** a json object representing a user
check: function check(userData) {
// **returns:** on success, returns a promise for the resulting user in a json object
return dataProvider.User.check(userData);
},
// #### Change Password
// **takes:** a json object representing a user
changePassword: function changePassword(userData) {
// **returns:** on success, returns a promise for the resulting user in a json object
return dataProvider.User.changePassword(userData);
},
generateResetToken: function generateResetToken(email) {
// TODO: Do we want to be able to pass this in?
var expires = Date.now() + ONE_DAY;
return dataProvider.User.generateResetToken(email, expires, ghost.dbHash);
},
validateToken: function validateToken(token) {
return dataProvider.User.validateToken(token, ghost.dbHash);
},
resetPassword: function resetPassword(token, newPassword, ne2Password) {
return dataProvider.User.resetPassword(token, newPassword, ne2Password, ghost.dbHash);
}
};
tags = {
// #### All
// **takes:** Nothing yet
all: function browse() {
// **returns:** a promise for all tags which have previously been used in a json object
return dataProvider.Tag.findAll();
}
};
// ## Notifications
notifications = {
// #### Destroy
// **takes:** an identifier (id)
destroy: function destroy(i) {
ghost.notifications = _.reject(ghost.notifications, function (element) {
return element.id === i.id;
});
// **returns:** a promise for remaining notifications as a json object
return when(ghost.notifications);
},
// #### Add
// **takes:** a notification object of the form
// ```
// msg = {
// type: 'error', // this can be 'error', 'success', 'warn' and 'info'
// message: 'This is an error', // A string. Should fit in one line.
// status: 'persistent', // or 'passive'
// id: 'auniqueid' // A unique ID
// };
// ```
add: function add(notification) {
// **returns:** a promise for all notifications as a json object
return when(ghost.notifications.push(notification));
}
};
// ## Settings
// ### Helpers
// Turn a settings collection into a single object/hashmap
settingsObject = function (settings) {
if (_.isObject(settings)) {
return _.reduce(settings, function (res, item, key) {
if (_.isArray(item)) {
res[key] = item;
} else {
res[key] = item.value;
}
return res;
}, {});
}
return (settings.toJSON ? settings.toJSON() : settings).reduce(function (res, item) {
if (item.toJSON) { item = item.toJSON(); }
if (item.key) { res[item.key] = item.value; }
return res;
}, {});
};
// Turn an object into a collection
settingsCollection = function (settings) {
return _.map(settings, function (value, key) {
return { key: key, value: value };
});
};
// Filters an object based on a given filter object
settingsFilter = function (settings, filter) {
return _.object(_.filter(_.pairs(settings), function (setting) {
if (filter) {
return _.some(filter.split(','), function (f) {
return setting[1].type === f;
});
}
return true;
}));
};
settings = {
// #### Browse
// **takes:** options object
browse: function browse(options) {
// **returns:** a promise for a settings json object
if (ghost.settings()) {
return when(ghost.settings()).then(function (settings) {
//TODO: omit where type==core
return settingsObject(settingsFilter(settings, options.type));
}, errors.logAndThrowError);
}
},
// #### Read
// **takes:** either a json object containing a key, or a single key string
read: function read(options) {
if (_.isString(options)) {
options = { key: options };
}
if (ghost.settings()) {
return when(ghost.settings()[options.key]).then(function (setting) {
if (!setting) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + options.key});
}
var res = {};
res.key = options.key;
res.value = setting.value;
return res;
}, errors.logAndThrowError);
}
},
// #### Edit
// **takes:** either a json object representing a collection of settings, or a key and value pair
edit: function edit(key, value) {
// Check for passing a collection of settings first
if (_.isObject(key)) {
//clean data
var type = key.type;
delete key.type;
delete key.availableThemes;
key = settingsCollection(key);
return dataProvider.Settings.edit(key).then(function (result) {
result.models = result;
return when(ghost.readSettingsResult(result)).then(function (settings) {
ghost.updateSettingsCache(settings);
return settingsObject(settingsFilter(ghost.settings(), type));
});
}).otherwise(function (error) {
return dataProvider.Settings.read(key.key).then(function (result) {
if (!result) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key});
}
return when.reject({message: error.message});
});
});
}
return dataProvider.Settings.read(key).then(function (setting) {
if (!setting) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key});
}
if (!_.isString(value)) {
value = JSON.stringify(value);
}
setting.set('value', value);
return dataProvider.Settings.edit(setting).then(function (result) {
ghost.settings()[_.first(result).attributes.key].value = _.first(result).attributes.value;
return settingsObject(ghost.settings());
}, errors.logAndThrowError);
});
}
};
init;
// ## Request Handlers
@ -429,41 +51,49 @@ requestHandler = function (apiMethod) {
apiContext = {
user: req.session && req.session.user
},
root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
postRouteIndex,
i;
// If permalinks have changed, find old post route
if (req.body.permalinks && req.body.permalinks !== ghost.settings('permalinks')) {
for (i = 0; i < req.app.routes.get.length; i += 1) {
if (req.app.routes.get[i].path === root + ghost.settings('permalinks')) {
postRouteIndex = i;
break;
settings.read('permalinks').then(function (permalinks) {
// If permalinks have changed, find old post route
if (req.body.permalinks && req.body.permalinks !== permalinks) {
for (i = 0; i < req.app.routes.get.length; i += 1) {
if (req.app.routes.get[i].path === config.paths().webroot + permalinks) {
postRouteIndex = i;
break;
}
}
}
}
return apiMethod.call(apiContext, options).then(function (result) {
// Reload post route
if (postRouteIndex) {
req.app.get(ghost.settings('permalinks'), req.app.routes.get.splice(postRouteIndex, 1)[0].callbacks);
}
return apiMethod.call(apiContext, options).then(function (result) {
// Reload post route
if (postRouteIndex) {
req.app.get(permalinks, req.app.routes.get.splice(postRouteIndex, 1)[0].callbacks);
}
invalidateCache(req, res, result);
res.json(result || {});
}, function (error) {
var errorCode = error.errorCode || 500,
errorMsg = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')};
res.json(errorCode, errorMsg);
invalidateCache(req, res, result);
res.json(result || {});
}, function (error) {
var errorCode = error.errorCode || 500,
errorMsg = {error: _.isString(error) ? error : (_.isObject(error) ? error.message : 'Unknown API Error')};
res.json(errorCode, errorMsg);
});
});
};
};
init = function () {
return settings.updateSettingsCache();
};
// Public API
module.exports.posts = posts;
module.exports.users = users;
module.exports.tags = tags;
module.exports.notifications = notifications;
module.exports.settings = settings;
module.exports.db = db;
module.exports.requestHandler = requestHandler;
module.exports = {
posts: posts,
users: users,
tags: tags,
notifications: notifications,
settings: settings,
db: db,
requestHandler: requestHandler,
init: init
};

View File

@ -0,0 +1,47 @@
var when = require('when'),
_ = require('underscore'),
// Holds the persistent notifications
notificationsStore = [],
notifications;
// ## Notifications
notifications = {
browse: function browse() {
return when(notificationsStore);
},
// #### Destroy
// **takes:** an identifier object ({id: id})
destroy: function destroy(i) {
notificationsStore = _.reject(notificationsStore, function (element) {
return element.id === i.id;
});
// **returns:** a promise for remaining notifications as a json object
return when(notificationsStore);
},
destroyAll: function destroyAll() {
notificationsStore = [];
return when(notificationsStore);
},
// #### Add
// **takes:** a notification object of the form
// ```
// msg = {
// type: 'error', // this can be 'error', 'success', 'warn' and 'info'
// message: 'This is an error', // A string. Should fit in one line.
// status: 'persistent', // or 'passive'
// id: 'auniqueid' // A unique ID
// };
// ```
add: function add(notification) {
// **returns:** a promise for all notifications as a json object
return when(notificationsStore.push(notification));
}
};
module.exports = notifications;

121
core/server/api/posts.js Normal file
View File

@ -0,0 +1,121 @@
var when = require('when'),
_ = require('underscore'),
dataProvider = require('../models'),
permissions = require('../permissions'),
canThis = permissions.canThis,
filteredUserAttributes = require('./users').filteredAttributes,
posts;
// ## Posts
posts = {
// #### Browse
// **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).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
// **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).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 when.reject({errorCode: 404, message: 'Post not found'});
});
},
// #### Edit
// **takes:** a json object with all the properties which should be updated
edit: function edit(postData) {
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'});
}
var self = this;
return canThis(self.user).edit.post(postData.id).then(function () {
return dataProvider.Post.edit(postData).then(function (result) {
if (result) {
var omitted = result.toJSON();
omitted.author = _.omit(omitted.author, filteredUserAttributes);
omitted.user = _.omit(omitted.user, filteredUserAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'Post not found'});
}).otherwise(function (error) {
return dataProvider.Post.findOne({id: postData.id, status: 'all'}).then(function (result) {
if (!result) {
return when.reject({errorCode: 404, message: 'Post not found'});
}
return when.reject({message: error.message});
});
});
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to edit this post.'});
});
},
// #### Add
// **takes:** a json object representing a post,
add: function add(postData) {
// **returns:** a promise for the resulting post in a json object
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'});
}
return canThis(this.user).create.post().then(function () {
return dataProvider.Post.add(postData);
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to add posts.'});
});
},
// #### Destroy
// **takes:** an identifier (id or slug?)
destroy: function destroy(args) {
// **returns:** a promise for a json response with the id of the deleted post
if (!this.user) {
return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'});
}
return canThis(this.user).remove.post(args.id).then(function () {
return when(posts.read({id : args.id, status: 'all'})).then(function (result) {
return dataProvider.Post.destroy(args.id).then(function () {
var deletedObj = {};
deletedObj.id = result.id;
deletedObj.slug = result.slug;
return deletedObj;
});
});
}, function () {
return when.reject({errorCode: 403, message: 'You do not have permission to remove posts.'});
});
}
};
module.exports = posts;

185
core/server/api/settings.js Normal file
View File

@ -0,0 +1,185 @@
var _ = require('underscore'),
dataProvider = require('../models'),
when = require('when'),
errors = require('../errorHandling'),
config = require('../config'),
settings,
settingsObject,
settingsCollection,
settingsFilter,
updateSettingsCache,
readSettingsResult,
// Holds cached settings
settingsCache = {};
// ### Helpers
// Turn a settings collection into a single object/hashmap
settingsObject = function (settings) {
if (_.isObject(settings)) {
return _.reduce(settings, function (res, item, key) {
if (_.isArray(item)) {
res[key] = item;
} else {
res[key] = item.value;
}
return res;
}, {});
}
return (settings.toJSON ? settings.toJSON() : settings).reduce(function (res, item) {
if (item.toJSON) { item = item.toJSON(); }
if (item.key) { res[item.key] = item.value; }
return res;
}, {});
};
// Turn an object into a collection
settingsCollection = function (settings) {
return _.map(settings, function (value, key) {
return { key: key, value: value };
});
};
// Filters an object based on a given filter object
settingsFilter = function (settings, filter) {
return _.object(_.filter(_.pairs(settings), function (setting) {
if (filter) {
return _.some(filter.split(','), function (f) {
return setting[1].type === f;
});
}
return true;
}));
};
// Maintain the internal cache of the settings object
updateSettingsCache = function (settings) {
settings = settings || {};
if (!_.isEmpty(settings)) {
_.map(settings, function (setting, key) {
settingsCache[key].value = setting.value;
});
} else {
return when(dataProvider.Settings.findAll()).then(function (result) {
return when(readSettingsResult(result)).then(function (s) {
settingsCache = s;
});
});
}
};
readSettingsResult = function (result) {
var settings = {};
return when(_.map(result.models, function (member) {
if (!settings.hasOwnProperty(member.attributes.key)) {
var val = {};
val.value = member.attributes.value;
val.type = member.attributes.type;
settings[member.attributes.key] = val;
}
})).then(function () {
return when(config.paths().availableThemes).then(function (themes) {
var themeKeys = Object.keys(themes),
res = [],
i,
item;
for (i = 0; i < themeKeys.length; i += 1) {
//do not include hidden files
if (themeKeys[i].indexOf('.') !== 0) {
item = {};
item.name = themeKeys[i];
//data about files currently not used
//item.details = themes[themeKeys[i]];
if (themeKeys[i] === settings.activeTheme.value) {
item.active = true;
}
res.push(item);
}
}
settings.availableThemes = {};
settings.availableThemes.value = res;
settings.availableThemes.type = 'theme';
return settings;
});
});
};
settings = {
// #### Browse
// **takes:** options object
browse: function browse(options) {
// **returns:** a promise for a settings json object
if (settingsCache) {
return when(settingsCache).then(function (settings) {
//TODO: omit where type==core
return settingsObject(settingsFilter(settings, options.type));
}, errors.logAndThrowError);
}
},
// #### Read
// **takes:** either a json object containing a key, or a single key string
read: function read(options) {
if (_.isString(options)) {
options = { key: options };
}
if (settingsCache) {
return when(settingsCache[options.key]).then(function (setting) {
if (!setting) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + options.key});
}
var res = {};
res.key = options.key;
res.value = setting.value;
return res;
}, errors.logAndThrowError);
}
},
// #### Edit
// **takes:** either a json object representing a collection of settings, or a key and value pair
edit: function edit(key, value) {
// Check for passing a collection of settings first
if (_.isObject(key)) {
//clean data
var type = key.type;
delete key.type;
delete key.availableThemes;
key = settingsCollection(key);
return dataProvider.Settings.edit(key).then(function (result) {
result.models = result;
return when(readSettingsResult(result)).then(function (settings) {
updateSettingsCache(settings);
return settingsObject(settingsFilter(settingsCache, type));
});
}).otherwise(function (error) {
return dataProvider.Settings.read(key.key).then(function (result) {
if (!result) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key});
}
return when.reject({message: error.message});
});
});
}
return dataProvider.Settings.read(key).then(function (setting) {
if (!setting) {
return when.reject({errorCode: 404, message: 'Unable to find setting: ' + key});
}
if (!_.isString(value)) {
value = JSON.stringify(value);
}
setting.set('value', value);
return dataProvider.Settings.edit(setting).then(function (result) {
settingsCache[_.first(result).attributes.key].value = _.first(result).attributes.value;
return settingsObject(settingsCache);
}, errors.logAndThrowError);
});
}
};
module.exports = settings;
module.exports.updateSettingsCache = updateSettingsCache;

15
core/server/api/tags.js Normal file
View File

@ -0,0 +1,15 @@
var dataProvider = require('../models'),
tags;
tags = {
// #### All
// **takes:** Nothing yet
all: function browse() {
// **returns:** a promise for all tags which have previously been used in a json object
return dataProvider.Tag.findAll();
}
};
module.exports = tags;

115
core/server/api/users.js Normal file
View File

@ -0,0 +1,115 @@
var when = require('when'),
_ = require('underscore'),
dataProvider = require('../models'),
settings = require('./settings'),
ONE_DAY = 86400000,
filteredAttributes = ['password', 'created_by', 'updated_by', 'last_login'],
users;
// ## Users
users = {
// #### Browse
// **takes:** options object
browse: function browse(options) {
// **returns:** a promise for a collection of users in a json object
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], filteredAttributes);
}
return omitted;
});
},
// #### Read
// **takes:** an identifier (id or slug?)
read: function read(args) {
// **returns:** a promise for a single user in a json object
if (args.id === 'me') {
args = {id: this.user};
}
return dataProvider.User.read(args).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'User not found'});
});
},
// #### Edit
// **takes:** a json object representing a user
edit: function edit(userData) {
// **returns:** a promise for the resulting user in a json object
userData.id = this.user;
return dataProvider.User.edit(userData).then(function (result) {
if (result) {
var omitted = _.omit(result.toJSON(), filteredAttributes);
return omitted;
}
return when.reject({errorCode: 404, message: 'User not found'});
});
},
// #### Add
// **takes:** a json object representing a user
add: function add(userData) {
// **returns:** a promise for the resulting user in a json object
return dataProvider.User.add(userData);
},
// #### Check
// Checks a password matches the given email address
// **takes:** a json object representing a user
check: function check(userData) {
// **returns:** on success, returns a promise for the resulting user in a json object
return dataProvider.User.check(userData);
},
// #### Change Password
// **takes:** a json object representing a user
changePassword: function changePassword(userData) {
// **returns:** on success, returns a promise for the resulting user in a json object
return dataProvider.User.changePassword(userData);
},
generateResetToken: function generateResetToken(email) {
// TODO: Do we want to be able to pass this in?
var expires = Date.now() + ONE_DAY;
return settings.read('dbHash').then(function (dbHash) {
return dataProvider.User.generateResetToken(email, expires, dbHash);
});
},
validateToken: function validateToken(token) {
return settings.read('dbHash').then(function (dbHash) {
return dataProvider.User.validateToken(token, dbHash);
});
},
resetPassword: function resetPassword(token, newPassword, ne2Password) {
return settings.read('dbHash').then(function (dbHash) {
return dataProvider.User.resetPassword(token, newPassword, ne2Password, dbHash);
});
}
};
module.exports = users;
module.exports.filteredAttributes = filteredAttributes;

View File

@ -2,25 +2,28 @@
var path = require('path'),
when = require('when'),
url = require('url'),
requireTree = require('../require-tree'),
appRoot = path.resolve(__dirname, '../../../'),
themePath = path.resolve(appRoot + '/content/themes'),
pluginPath = path.resolve(appRoot + '/content/plugins'),
themeDirectories = requireTree(themePath),
pluginDirectories = requireTree(pluginPath),
activeTheme = '',
localPath = '',
availableThemes,
availablePlugins;
function getPaths() {
return {
'appRoot': appRoot,
'path': localPath,
'webroot': localPath === '/' ? '' : localPath,
'config': path.join(appRoot, 'config.js'),
'configExample': path.join(appRoot, 'config.example.js'),
'themePath': themePath,
'pluginPath': pluginPath,
'activeTheme': path.join(themePath, activeTheme),
'adminViews': path.join(appRoot, '/core/server/views/'),
'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'),
'lang': path.join(appRoot, '/core/shared/lang/'),
@ -29,8 +32,16 @@ function getPaths() {
};
}
// TODO: remove configURL and give direct access to config object?
// TODO: not called when executing tests
function updatePaths(configURL) {
localPath = url.parse(configURL).path;
// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}
function updatePaths() {
return when.all([themeDirectories, pluginDirectories]).then(function (paths) {
availableThemes = paths[0];
availablePlugins = paths[1];
@ -38,14 +49,6 @@ function updatePaths() {
});
}
function setActiveTheme(ghost) {
if (ghost && ghost.settingsCache) {
activeTheme = ghost.settingsCache.activeTheme.value;
}
}
module.exports = getPaths;
module.exports.updatePaths = updatePaths;
module.exports.setActiveTheme = setActiveTheme;

View File

@ -1,5 +1,4 @@
var Ghost = require('../../ghost'),
config = require('../config'),
var config = require('../config'),
_ = require('underscore'),
path = require('path'),
when = require('when'),
@ -8,8 +7,6 @@ var Ghost = require('../../ghost'),
errors = require('../errorHandling'),
storage = require('../storage'),
ghost = new Ghost(),
dataProvider = ghost.dataProvider,
adminNavbar,
adminControllers,
loginSecurity = [];
@ -74,8 +71,7 @@ adminControllers = {
});
},
'auth': function (req, res) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
currentTime = process.hrtime()[0],
var currentTime = process.hrtime()[0],
denied = '';
loginSecurity = _.filter(loginSecurity, function (ipTime) {
return (ipTime.time + 2 > currentTime);
@ -90,7 +86,7 @@ adminControllers = {
req.session.regenerate(function (err) {
if (!err) {
req.session.user = user.id;
var redirect = root + '/ghost/';
var redirect = config.paths().webroot + '/ghost/';
if (req.body.redirect) {
redirect += decodeURIComponent(req.body.redirect);
}
@ -126,8 +122,7 @@ adminControllers = {
});
},
'doRegister': function (req, res) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
name = req.body.name,
var name = req.body.name,
email = req.body.email,
password = req.body.password;
@ -142,7 +137,7 @@ adminControllers = {
if (req.session.user === undefined) {
req.session.user = user.id;
}
res.json(200, {redirect: root + '/ghost/'});
res.json(200, {redirect: config.paths().webroot + '/ghost/'});
}
});
});
@ -159,8 +154,7 @@ adminControllers = {
});
},
'generateResetToken': function (req, res) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
email = req.body.email;
var email = req.body.email;
api.users.generateResetToken(email).then(function (token) {
var siteLink = '<a href="' + config().url + '">' + config().url + '</a>',
@ -185,7 +179,7 @@ adminControllers = {
};
return api.notifications.add(notification).then(function () {
res.json(200, {redirect: root + '/ghost/signin/'});
res.json(200, {redirect: config.paths().webroot + '/ghost/signin/'});
});
}, function failure(error) {
@ -199,9 +193,8 @@ adminControllers = {
});
},
'reset': function (req, res) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
// Validate the request token
token = req.params.token;
// Validate the request token
var token = req.params.token;
api.users.validateToken(token).then(function () {
// Render the reset form
@ -222,13 +215,12 @@ adminControllers = {
errors.logError(err, 'admin.js', "Please check the provided token for validity and expiration.");
return api.notifications.add(notification).then(function () {
res.redirect(root + '/ghost/forgotten');
res.redirect(config.paths().webroot + '/ghost/forgotten');
});
});
},
'resetPassword': function (req, res) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
token = req.params.token,
var token = req.params.token,
newPassword = req.param('newpassword'),
ne2Password = req.param('ne2password');
@ -241,7 +233,7 @@ adminControllers = {
};
return api.notifications.add(notification).then(function () {
res.json(200, {redirect: root + '/ghost/signin/'});
res.json(200, {redirect: config.paths().webroot + '/ghost/signin/'});
});
}).otherwise(function (err) {
// TODO: Better error message if we can tell whether the passwords didn't match or something
@ -251,8 +243,7 @@ adminControllers = {
'logout': function (req, res) {
req.session.destroy();
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
notification = {
var notification = {
type: 'success',
message: 'You were successfully signed out',
status: 'passive',
@ -260,7 +251,7 @@ adminControllers = {
};
return api.notifications.add(notification).then(function () {
res.redirect(root + '/ghost/signin/');
res.redirect(config.paths().webroot + '/ghost/signin/');
});
},
'index': function (req, res) {

View File

@ -4,8 +4,7 @@
/*global require, module */
var Ghost = require('../../ghost'),
config = require('../config'),
var config = require('../config'),
api = require('../api'),
RSS = require('rss'),
_ = require('underscore'),
@ -14,35 +13,37 @@ var Ghost = require('../../ghost'),
url = require('url'),
filters = require('../../server/filters'),
ghost = new Ghost(),
frontendControllers;
frontendControllers = {
'homepage': function (req, res, next) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
// Parse the page number
pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
postsPerPage = parseInt(ghost.settings('postsPerPage'), 10),
// Parse the page number
var pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
postsPerPage,
options = {};
// No negative pages
if (isNaN(pageParam) || pageParam < 1) {
//redirect to 404 page?
return res.redirect(root + '/');
}
options.page = pageParam;
api.settings.read('postsPerPage').then(function (postPP) {
postsPerPage = parseInt(postPP.value, 10);
// No negative pages
if (isNaN(pageParam) || pageParam < 1) {
//redirect to 404 page?
return res.redirect('/');
}
options.page = pageParam;
// Redirect '/page/1/' to '/' for all teh good SEO
if (pageParam === 1 && req.route.path === '/page/:page/') {
return res.redirect(root + '/');
}
// Redirect '/page/1/' to '/' for all teh good SEO
if (pageParam === 1 && req.route.path === '/page/:page/') {
return res.redirect(config.paths().webroot + '/');
}
// No negative posts per page, must be number
if (!isNaN(postsPerPage) && postsPerPage > 0) {
options.limit = postsPerPage;
}
api.posts.browse(options).then(function (page) {
// No negative posts per page, must be number
if (!isNaN(postsPerPage) && postsPerPage > 0) {
options.limit = postsPerPage;
}
return;
}).then(function () {
return api.posts.browse(options);
}).then(function (page) {
var maxPage = page.pages;
// A bit of a hack for situations with no content.
@ -53,7 +54,7 @@ frontendControllers = {
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return res.redirect(maxPage === 1 ? root + '/' : (root + '/page/' + maxPage + '/'));
return res.redirect(maxPage === 1 ? config.paths().webroot + '/' : (config.paths().webroot + '/page/' + maxPage + '/'));
}
// Render the page of posts
@ -70,12 +71,14 @@ frontendControllers = {
api.posts.read(_.pick(req.params, ['id', 'slug'])).then(function (post) {
if (post) {
filters.doFilter('prePostsRender', post).then(function (post) {
var paths = config.paths().availableThemes[ghost.settings('activeTheme')];
if (post.page && paths.hasOwnProperty('page')) {
res.render('page', {post: post});
} else {
res.render('post', {post: post});
}
api.settings.read('activeTheme').then(function (activeTheme) {
var paths = config.paths().availableThemes[activeTheme];
if (post.page && paths.hasOwnProperty('page')) {
res.render('page', {post: post});
} else {
res.render('post', {post: post});
}
});
});
} else {
next();
@ -90,14 +93,20 @@ frontendControllers = {
'rss': function (req, res, next) {
// Initialize RSS
var siteUrl = config().url,
root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
pageParam = req.params.page !== undefined ? parseInt(req.params.page, 10) : 1,
feed;
//needs refact for multi user to not use first user as default
api.users.read({id : 1}).then(function (user) {
when.all([
api.users.read({id : 1}),
api.settings.read('title'),
api.settings.read('description')
]).then(function (values) {
var user = values[0],
title = values[1].value,
description = values[2].value;
feed = new RSS({
title: ghost.settings('title'),
description: ghost.settings('description'),
title: title,
description: description,
generator: 'Ghost v' + res.locals.version,
author: user ? user.name : null,
feed_url: url.resolve(siteUrl, '/rss/'),
@ -107,11 +116,11 @@ frontendControllers = {
// No negative pages
if (isNaN(pageParam) || pageParam < 1) {
return res.redirect(root + '/rss/');
return res.redirect(config.paths().webroot + '/rss/');
}
if (pageParam === 1 && req.route.path === root + '/rss/:page/') {
return res.redirect(root + '/rss/');
if (pageParam === 1 && req.route.path === config.paths().webroot + '/rss/:page/') {
return res.redirect(config.paths().webroot + '/rss/');
}
api.posts.browse({page: pageParam}).then(function (page) {
@ -125,7 +134,7 @@ frontendControllers = {
// If page is greater than number of pages we have, redirect to last page
if (pageParam > maxPage) {
return res.redirect(root + '/rss/' + maxPage + '/');
return res.redirect(config.paths().webroot + '/rss/' + maxPage + '/');
}
filters.doFilter('prePostsRender', page.posts).then(function (posts) {

View File

@ -13,6 +13,7 @@ var _ = require('underscore'),
version = packageInfo.version,
scriptTemplate = _.template("<script src='<%= source %>?v=<%= version %>'></script>"),
isProduction = process.env.NODE_ENV === 'production',
api = require('../api'),
coreHelpers = {},
registerHelpers;
@ -95,25 +96,23 @@ coreHelpers.url = function (options) {
},
blog = coreHelpers.ghost.blogGlobals(),
isAbsolute = options && options.hash.absolute;
if (isAbsolute) {
output += blog.url;
}
if (blog.path && blog.path !== '/') {
output += blog.path;
}
if (models.isPost(this)) {
output += coreHelpers.ghost.settings('permalinks');
output = output.replace(/(:[a-z]+)/g, function (match) {
if (_.has(tags, match.substr(1))) {
return tags[match.substr(1)]();
}
});
}
return output;
return api.settings.read('permalinks').then(function (permalinks) {
if (isAbsolute) {
output += blog.url;
}
if (blog.path && blog.path !== '/') {
output += blog.path;
}
if (models.isPost(self)) {
output += permalinks.value;
output = output.replace(/(:[a-z]+)/g, function (match) {
if (_.has(tags, match.substr(1))) {
return tags[match.substr(1)]();
}
});
}
return output;
});
};
// ### Asset helper
@ -429,14 +428,19 @@ coreHelpers.meta_description = function (options) {
*/
coreHelpers.e = function (key, defaultString, options) {
var output;
if (coreHelpers.ghost.settings('defaultLang') === 'en' && _.isEmpty(options.hash) && !coreHelpers.ghost.settings('forceI18n')) {
output = defaultString;
} else {
output = polyglot().t(key, options.hash);
}
return output;
when.all([
api.settings.read('defaultLang'),
api.settings.read('forceI18n')
]).then(function (values) {
if (values[0].value === 'en'
&& _.isEmpty(options.hash)
&& _.isEmpty(values[1].value)) {
output = defaultString;
} else {
output = polyglot().t(key, options.hash);
}
return output;
});
};
coreHelpers.json = function (object, options) {
@ -605,8 +609,6 @@ registerHelpers = function (ghost, config) {
registerThemeHelper('date', coreHelpers.date);
registerThemeHelper('e', coreHelpers.e);
registerThemeHelper('encode', coreHelpers.encode);
registerThemeHelper('excerpt', coreHelpers.excerpt);
@ -619,18 +621,16 @@ registerHelpers = function (ghost, config) {
registerThemeHelper('has_tag', coreHelpers.has_tag);
registerThemeHelper('helperMissing', coreHelpers.helperMissing);
registerThemeHelper('json', coreHelpers.json);
registerThemeHelper('pageUrl', coreHelpers.pageUrl);
registerThemeHelper('tags', coreHelpers.tags);
registerThemeHelper('url', coreHelpers.url);
registerAsyncThemeHelper('body_class', coreHelpers.body_class);
registerAsyncThemeHelper('e', coreHelpers.e);
registerAsyncThemeHelper('ghost_foot', coreHelpers.ghost_foot);
registerAsyncThemeHelper('ghost_head', coreHelpers.ghost_head);
@ -641,6 +641,8 @@ registerHelpers = function (ghost, config) {
registerAsyncThemeHelper('post_class', coreHelpers.post_class);
registerAsyncThemeHelper('url', coreHelpers.url);
paginationHelper = template.loadTemplate('pagination').then(function (templateFn) {
coreHelpers.paginationTemplate = templateFn;

View File

@ -5,7 +5,8 @@ var templates = {},
errors = require('../errorHandling'),
path = require('path'),
when = require('when'),
config = require('../config');
config = require('../config'),
api = require('../api');
// ## Template utils
@ -19,22 +20,23 @@ templates.compileTemplate = function (templatePath) {
// Load a template for a handlebars helper
templates.loadTemplate = function (name) {
var templateFileName = name + '.hbs',
// Check for theme specific version first
templatePath = path.join(config.paths().activeTheme, 'partials', templateFileName),
deferred = when.defer();
// Check for theme specific version first
return api.settings.read('activeTheme').then(function (activeTheme) {
var templatePath = path.join(config.paths().themePath, activeTheme.value, 'partials', templateFileName);
// Can't use nodefn here because exists just returns one parameter, true or false
// Can't use nodefn here because exists just returns one parameter, true or false
fs.exists(templatePath, function (exists) {
if (!exists) {
// Fall back to helpers templates location
templatePath = path.join(config.paths().helperTemplates, templateFileName);
}
fs.exists(templatePath, function (exists) {
if (!exists) {
// Fall back to helpers templates location
templatePath = path.join(config.paths().helperTemplates, templateFileName);
}
templates.compileTemplate(templatePath).then(deferred.resolve, deferred.reject);
});
templates.compileTemplate(templatePath).then(deferred.resolve, deferred.reject);
return deferred.promise;
});
return deferred.promise;
};
module.exports = templates;

View File

@ -46,7 +46,7 @@ function setup(server) {
// Initialise mail after first run,
// passing in config module to prevent
// circular dependencies.
mailer.init(ghost, config),
mailer.init(),
helpers.loadCoreHelpers(ghost, config)
);
}).then(function () {

View File

@ -3,7 +3,9 @@ var cp = require('child_process'),
_ = require('underscore'),
when = require('when'),
nodefn = require('when/node/function'),
nodemailer = require('nodemailer');
nodemailer = require('nodemailer'),
api = require('./api'),
config = require('./config');
function GhostMailer(opts) {
opts = opts || {};
@ -12,19 +14,10 @@ function GhostMailer(opts) {
// ## E-mail transport setup
// *This promise should always resolve to avoid halting Ghost::init*.
GhostMailer.prototype.init = function (ghost, configModule) {
this.ghost = ghost;
// TODO: fix circular reference ghost -> mail -> api -> ghost, remove this late require
this.api = require('./api');
// We currently pass in the config module to avoid
// circular references, similar to above.
this.config = configModule;
var self = this,
config = this.config();
if (config.mail && config.mail.transport && config.mail.options) {
this.createTransport(config);
GhostMailer.prototype.init = function () {
var self = this;
if (config().mail && config().mail.transport && config().mail.options) {
this.createTransport(config());
return when.resolve();
}
@ -64,7 +57,7 @@ GhostMailer.prototype.createTransport = function (config) {
};
GhostMailer.prototype.usingSendmail = function () {
this.api.notifications.add({
api.notifications.add({
type: 'info',
message: [
"Ghost is attempting to use your server's <b>sendmail</b> to send e-mail.",
@ -77,7 +70,7 @@ GhostMailer.prototype.usingSendmail = function () {
};
GhostMailer.prototype.emailDisabled = function () {
this.api.notifications.add({
api.notifications.add({
type: 'warn',
message: [
"Ghost is currently unable to send e-mail.",
@ -97,18 +90,19 @@ GhostMailer.prototype.send = function (message) {
if (!(message && message.subject && message.html)) {
return when.reject(new Error('Email Error: Incomplete message data.'));
}
api.settings.read('email').then(function (email) {
var from = config().mail.fromaddress || email.value,
to = message.to || email.value;
var from = this.config().mail.fromaddress || this.ghost.settings('email'),
to = message.to || this.ghost.settings('email'),
sendMail = nodefn.lift(this.transport.sendMail.bind(this.transport));
message = _.extend(message, {
from: from,
to: to,
generateTextFromHTML: true
});
return sendMail(message).otherwise(function (error) {
message = _.extend(message, {
from: from,
to: to,
generateTextFromHTML: true
});
}).then(function () {
var sendMail = nodefn.lift(this.transport.sendMail.bind(this.transport));
return sendMail(message);
}).otherwise(function (error) {
// Proxy the error message so we can add 'Email Error:' to the beginning to make it clearer.
error = _.isString(error) ? 'Email Error:' + error : (_.isObject(error) ? 'Email Error: ' + error.message : 'Email Error: Unknown Email Error');
return when.reject(new Error(error));

View File

@ -5,6 +5,7 @@
var middleware = require('./middleware'),
express = require('express'),
_ = require('underscore'),
when = require('when'),
slashes = require('connect-slashes'),
errors = require('../errorHandling'),
api = require('../api'),
@ -33,24 +34,32 @@ function ghostLocals(req, res, next) {
if (res.isAdmin) {
res.locals.csrfToken = req.csrfToken();
api.users.read({id: req.session.user}).then(function (currentUser) {
when.all([
api.users.read({id: req.session.user}),
api.notifications.browse()
]).then(function (values) {
var currentUser = values[0],
notifications = values[1];
_.extend(res.locals, {
currentUser: {
name: currentUser.name,
email: currentUser.email,
image: currentUser.image
},
messages: ghost.notifications
messages: notifications
});
next();
}).otherwise(function () {
// Only show passive notifications
_.extend(res.locals, {
messages: _.reject(ghost.notifications, function (notification) {
return notification.status !== 'passive';
})
api.notifications.browse().then(function (notifications) {
_.extend(res.locals, {
messages: _.reject(notifications, function (notification) {
return notification.status !== 'passive';
})
});
next();
});
next();
});
} else {
next();
@ -66,14 +75,15 @@ function initViews(req, res, next) {
if (!res.isAdmin) {
// self.globals is a hack til we have a better way of getting combined settings & config
hbsOptions = {templateOptions: {data: {blog: ghost.blogGlobals()}}};
api.settings.read('activeTheme').then(function (activeTheme) {
if (config.paths().availableThemes[activeTheme.value].hasOwnProperty('partials')) {
// Check that the theme has a partials directory before trying to use it
hbsOptions.partialsDir = path.join(config.paths().themePath, activeTheme.value, 'partials');
}
if (config.paths().availableThemes[ghost.settings('activeTheme')].hasOwnProperty('partials')) {
// Check that the theme has a partials directory before trying to use it
hbsOptions.partialsDir = path.join(config.paths().activeTheme, 'partials');
}
ghost.server.engine('hbs', hbs.express3(hbsOptions));
ghost.server.set('views', config.paths().activeTheme);
ghost.server.engine('hbs', hbs.express3(hbsOptions));
ghost.server.set('views', path.join(config.paths().themePath, activeTheme.value));
});
} else {
ghost.server.engine('hbs', hbs.express3({partialsDir: config.paths().adminViews + 'partials'}));
ghost.server.set('views', config.paths().adminViews);
@ -84,25 +94,22 @@ function initViews(req, res, next) {
// ### Activate Theme
// Helper for manageAdminAndTheme
function activateTheme() {
function activateTheme(activeTheme) {
var stackLocation = _.indexOf(ghost.server.stack, _.find(ghost.server.stack, function (stackItem) {
return stackItem.route === '' && stackItem.handle.name === 'settingEnabled';
}));
// Tell the paths to update
config.paths.setActiveTheme(ghost);
// clear the view cache
ghost.server.cache = {};
ghost.server.disable(ghost.server.get('activeTheme'));
ghost.server.set('activeTheme', ghost.settings('activeTheme'));
ghost.server.set('activeTheme', activeTheme);
ghost.server.enable(ghost.server.get('activeTheme'));
if (stackLocation) {
ghost.server.stack[stackLocation].handle = middleware.whenEnabled(ghost.server.get('activeTheme'), middleware.staticTheme());
}
// Update user error template
errors.updateActiveTheme(ghost.settings('activeTheme'));
errors.updateActiveTheme(activeTheme);
}
// ### ManageAdminAndTheme Middleware
@ -123,26 +130,26 @@ function manageAdminAndTheme(req, res, next) {
ghost.server.enable(ghost.server.get('activeTheme'));
ghost.server.disable('admin');
}
// Check if the theme changed
if (ghost.settings('activeTheme') !== ghost.server.get('activeTheme')) {
// Change theme
if (!config.paths().availableThemes.hasOwnProperty(ghost.settings('activeTheme'))) {
if (!res.isAdmin) {
// Throw an error if the theme is not available, but not on the admin UI
errors.logAndThrowError('The currently active theme ' + ghost.settings('activeTheme') + ' is missing.');
api.settings.read('activeTheme').then(function (activeTheme) {
// Check if the theme changed
if (activeTheme.value !== ghost.server.get('activeTheme')) {
// Change theme
if (!config.paths().availableThemes.hasOwnProperty(activeTheme.value)) {
if (!res.isAdmin) {
// Throw an error if the theme is not available, but not on the admin UI
errors.logAndThrowError('The currently active theme ' + activeTheme.value + ' is missing.');
}
} else {
activateTheme(activeTheme.value);
}
} else {
activateTheme();
}
}
next();
next();
});
}
module.exports = function (server) {
var oneYear = 31536000000,
root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
root = config.paths().webroot,
corePath = path.join(config.paths().appRoot, 'core');
// Logging configuration

View File

@ -7,6 +7,7 @@ var _ = require('underscore'),
Ghost = require('../../ghost'),
config = require('../config'),
path = require('path'),
api = require('../api'),
ghost = new Ghost();
function isBlackListedFileType(file) {
@ -23,26 +24,26 @@ var middleware = {
auth: function (req, res, next) {
if (!req.session.user) {
var reqPath = req.path.replace(/^\/ghost\/?/gi, ''),
root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path,
redirect = '',
msg;
if (reqPath !== '') {
msg = {
type: 'error',
message: 'Please Sign In',
status: 'passive',
id: 'failedauth'
};
// let's only add the notification once
if (!_.contains(_.pluck(ghost.notifications, 'id'), 'failedauth')) {
ghost.notifications.push(msg);
return api.notifications.browse().then(function (notifications) {
if (reqPath !== '') {
msg = {
type: 'error',
message: 'Please Sign In',
status: 'passive',
id: 'failedauth'
};
// let's only add the notification once
if (!_.contains(_.pluck(notifications, 'id'), 'failedauth')) {
api.notifications.add(msg);
}
redirect = '?r=' + encodeURIComponent(reqPath);
}
redirect = '?r=' + encodeURIComponent(reqPath);
}
return res.redirect(root + '/ghost/signin/' + redirect);
return res.redirect(config.paths().webroot + '/ghost/signin/' + redirect);
});
}
next();
},
@ -61,10 +62,8 @@ var middleware = {
// Check if we're logged in, and if so, redirect people back to dashboard
// Login and signup forms in particular
redirectToDashboard: function (req, res, next) {
var root = ghost.blogGlobals().path === '/' ? '' : ghost.blogGlobals().path;
if (req.session.user) {
return res.redirect(root + '/ghost/');
return res.redirect(config.paths().webroot + '/ghost/');
}
next();
@ -76,10 +75,14 @@ var middleware = {
// otherwise they'd appear one too many times
cleanNotifications: function (req, res, next) {
/*jslint unparam:true*/
ghost.notifications = _.reject(ghost.notifications, function (notification) {
return notification.status === 'passive';
api.notifications.browse().then(function (notifications) {
_.each(notifications, function (notification) {
if (notification.status === 'passive') {
api.notifications.destroy(notification);
}
});
next();
});
next();
},
// ### DisableCachedResult Middleware
@ -119,7 +122,9 @@ var middleware = {
// to allow unit testing
forwardToExpressStatic: function (req, res, next) {
return express['static'](config.paths().activeTheme)(req, res, next);
api.settings.read('activeTheme').then(function (activeTheme) {
express['static'](path.join(config.paths().themePath, activeTheme.value))(req, res, next);
});
},
conditionalCSRF: function (req, res, next) {

View File

@ -2,19 +2,14 @@
var _ = require('underscore'),
when = require('when'),
errors = require('../errorHandling'),
ghostApi,
api = require('../api'),
loader = require('./loader'),
availablePlugins;
// Holds the available plugins
availablePlugins = {};
// Holds the available plugins
availablePlugins = {};
function getInstalledPlugins() {
if (!ghostApi) {
ghostApi = require('../api');
}
return ghostApi.settings.read('installedPlugins').then(function (installed) {
return api.settings.read('installedPlugins').then(function (installed) {
installed.value = installed.value || '[]';
try {
@ -31,7 +26,7 @@ function saveInstalledPlugins(installedPlugins) {
return getInstalledPlugins().then(function (currentInstalledPlugins) {
var updatedPluginsInstalled = _.uniq(installedPlugins.concat(currentInstalledPlugins));
return ghostApi.settings.edit('installedPlugins', updatedPluginsInstalled);
return api.settings.edit('installedPlugins', updatedPluginsInstalled);
});
}
@ -41,7 +36,9 @@ module.exports = {
try {
// We have to parse the value because it's a string
pluginsToLoad = JSON.parse(ghost.settings('activePlugins')) || [];
api.settings.read('activePlugins').then(function (aPlugins) {
pluginsToLoad = JSON.parse(aPlugins.value) || [];
});
} catch (e) {
errors.logError(
'Failed to parse activePlugins setting value: ' + e.message,

View File

@ -1,14 +1,14 @@
var frontend = require('../controllers/frontend'),
Ghost = require('../../ghost'),
ghost = new Ghost();
api = require('../api');
module.exports = function (server) {
// ### Frontend routes
/* TODO: dynamic routing, homepage generator, filters ETC ETC */
server.get('/rss/', frontend.rss);
server.get('/rss/:page/', frontend.rss);
server.get('/page/:page/', frontend.homepage);
server.get(ghost.settings('permalinks'), frontend.single);
server.get('/', frontend.homepage);
api.settings.read('permalinks').then(function (permalinks) {
server.get(permalinks.value, frontend.single);
});
};

View File

@ -44,22 +44,4 @@ describe("Ghost API", function () {
should.strictEqual(ghost, ghost2);
});
it("uses init() to initialize", function (done) {
var dataProviderInitMock = sandbox.stub(ghost.dataProvider, "init", function () {
return when.resolve();
});
should.not.exist(ghost.settings());
ghost.init().then(function () {
should.exist(ghost.settings());
dataProviderInitMock.called.should.equal(true);
done();
}, done);
});
});

View File

@ -1,19 +1,19 @@
/*globals describe, beforeEach, it*/
var testUtils = require('../utils'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
_ = require('underscore'),
path = require('path'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
_ = require('underscore'),
path = require('path'),
// Stuff we are testing
config = require('../../server/config'),
api = require('../../server/api'),
template = require('../../server/helpers/template');
describe('Helpers Template', function () {
var testTemplatePath = 'core/test/utils/fixtures/',
themeTemplatePath = 'core/test/utils/fixtures/theme',
sandbox;
beforeEach(function () {
@ -48,26 +48,27 @@ describe('Helpers Template', function () {
pathsStub = sandbox.stub(config, "paths", function () {
return {
// Forcing the theme path to be the same
activeTheme: path.join(process.cwd(), testTemplatePath),
themePath: path.join(process.cwd(), testTemplatePath),
helperTemplates: path.join(process.cwd(), testTemplatePath)
};
});
apiStub = sandbox.stub(api.settings , 'read', function () {
return when({value: 'casper'});
});
template.loadTemplate('test').then(function (templateFn) {
compileSpy.restore();
pathsStub.restore();
// test that compileTemplate was called with the expected path
compileSpy.calledOnce.should.equal(true);
compileSpy.calledWith(path.join(process.cwd(), testTemplatePath, 'test.hbs')).should.equal(true);
should.exist(templateFn);
_.isFunction(templateFn).should.equal(true);
templateFn().should.equal('<h1>HelloWorld</h1>');
done();
}).then(null, done);
}).otherwise(done);
});
it("loads templates from themes first", function (done) {
@ -79,15 +80,19 @@ describe('Helpers Template', function () {
// In order for the test to work, need to replace the path to the template
pathsStub = sandbox.stub(config, "paths", function () {
return {
activeTheme: path.join(process.cwd(), themeTemplatePath),
// Forcing the theme path to be the same
themePath: path.join(process.cwd(), testTemplatePath),
helperTemplates: path.join(process.cwd(), testTemplatePath)
};
});
apiStub = sandbox.stub(api.settings , 'read', function () {
return when({value: 'theme'});
});
template.loadTemplate('test').then(function (templateFn) {
// test that compileTemplate was called with the expected path
compileSpy.calledOnce.should.equal(true);
compileSpy.calledWith(path.join(process.cwd(), themeTemplatePath, 'partials', 'test.hbs')).should.equal(true);
compileSpy.calledWith(path.join(process.cwd(), testTemplatePath, 'theme', 'partials', 'test.hbs')).should.equal(true);
should.exist(templateFn);
_.isFunction(templateFn).should.equal(true);

View File

@ -8,7 +8,6 @@ var testUtils = require('../utils'),
cp = require('child_process'),
// Stuff we are testing
Ghost = require('../../ghost'),
defaultConfig = require('../../../config'),
mailer = require('../../server/mail'),
SMTP,
@ -17,7 +16,6 @@ var testUtils = require('../utils'),
fakeSettings,
fakeSendmail,
sandbox = sinon.sandbox.create(),
ghost,
config;
// Mock SMTP config
@ -51,14 +49,8 @@ describe("Mail", function () {
};
fakeSendmail = '/fake/bin/sendmail';
ghost = new Ghost();
config = sinon.stub().returns(fakeConfig);
sandbox.stub(ghost, "settings", function () {
return fakeSettings;
});
sandbox.stub(mailer, "isWindows", function () {
return false;
});
@ -80,8 +72,8 @@ describe("Mail", function () {
});
it('should setup SMTP transport on initialization', function (done) {
fakeConfig.mail = SMTP;
mailer.init(ghost, config).then(function () {
fakeConfig[process.env.NODE_ENV].mail = SMTP;
mailer.init().then(function () {
mailer.should.have.property('transport');
mailer.transport.transportType.should.eql('SMTP');
mailer.transport.sendMail.should.be.a.function;
@ -90,8 +82,8 @@ describe("Mail", function () {
});
it('should setup sendmail transport on initialization', function (done) {
fakeConfig.mail = SENDMAIL;
mailer.init(ghost, config).then(function () {
fakeConfig[process.env.NODE_ENV].mail = SENDMAIL;
mailer.init().then(function () {
mailer.should.have.property('transport');
mailer.transport.transportType.should.eql('SENDMAIL');
mailer.transport.sendMail.should.be.a.function;
@ -100,8 +92,8 @@ describe("Mail", function () {
});
it('should fallback to sendmail if no config set', function (done) {
fakeConfig.mail = null;
mailer.init(ghost, config).then(function () {
fakeConfig[process.env.NODE_ENV].mail = null;
mailer.init().then(function () {
mailer.should.have.property('transport');
mailer.transport.transportType.should.eql('SENDMAIL');
mailer.transport.options.path.should.eql(fakeSendmail);
@ -110,8 +102,8 @@ describe("Mail", function () {
});
it('should fallback to sendmail if config is empty', function (done) {
fakeConfig.mail = {};
mailer.init(ghost, config).then(function () {
fakeConfig[process.env.NODE_ENV].mail = {};
mailer.init().then(function () {
mailer.should.have.property('transport');
mailer.transport.transportType.should.eql('SENDMAIL');
mailer.transport.options.path.should.eql(fakeSendmail);
@ -120,23 +112,23 @@ describe("Mail", function () {
});
it('should disable transport if config is empty & sendmail not found', function (done) {
fakeConfig.mail = {};
fakeConfig[process.env.NODE_ENV].mail = {};
mailer.detectSendmail.restore();
sandbox.stub(mailer, "detectSendmail", when.reject);
mailer.init(ghost, config).then(function () {
mailer.init().then(function () {
should.not.exist(mailer.transport);
done();
}).then(null, done);
});
it('should disable transport if config is empty & platform is win32', function (done) {
fakeConfig.mail = {};
fakeConfig[process.env.NODE_ENV].mail = {};
mailer.detectSendmail.restore();
mailer.isWindows.restore();
sandbox.stub(mailer, 'isWindows', function () {
return true;
});
mailer.init(ghost, config).then(function () {
mailer.init().then(function () {
should.not.exist(mailer.transport);
done();
}).then(null, done);
@ -145,7 +137,7 @@ describe("Mail", function () {
it('should fail to send messages when no transport is set', function (done) {
mailer.detectSendmail.restore();
sandbox.stub(mailer, "detectSendmail", when.reject);
mailer.init(ghost, config).then(function () {
mailer.init().then(function () {
mailer.send().then(function () {
should.fail();
done();

View File

@ -6,6 +6,7 @@ var assert = require('assert'),
_ = require('underscore'),
express = require('express'),
Ghost = require('../../ghost'),
api = require('../../server/api');
middleware = require('../../server/middleware').middleware;
describe('Middleware', function () {
@ -13,7 +14,7 @@ describe('Middleware', function () {
describe('auth', function () {
var req, res, ghost = new Ghost();
beforeEach(function () {
beforeEach(function (done) {
req = {
session: {}
};
@ -22,42 +23,51 @@ describe('Middleware', function () {
redirect: sinon.spy()
};
ghost.notifications = [];
api.notifications.destroyAll().then(function () {
return done();
});
});
it('should redirect to signin path', function (done) {
req.path = '';
middleware.auth(req, res, null);
assert(res.redirect.calledWithMatch('/ghost/signin/'));
return done();
middleware.auth(req, res, null).then(function () {
assert(res.redirect.calledWithMatch('/ghost/signin/'));
return done();
});
});
it('should redirect to signin path with redirect paramater stripped of /ghost/', function(done) {
var path = 'test/path/party';
req.path = '/ghost/' + path;
middleware.auth(req, res, null);
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
return done();
middleware.auth(req, res, null).then(function () {
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
return done();
});
});
it('should only add one message to the notification array', function (done) {
var path = 'test/path/party';
req.path = '/ghost/' + path;
middleware.auth(req, res, null);
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
assert.equal(ghost.notifications.length, 1);
middleware.auth(req, res, null);
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
assert.equal(ghost.notifications.length, 1);
return done();
middleware.auth(req, res, null).then(function () {
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
return api.notifications.browse().then(function (notifications) {
assert.equal(notifications.length, 1);
return;
});
}).then(function () {
return middleware.auth(req, res, null);
}).then(function () {
assert(res.redirect.calledWithMatch('/ghost/signin/?r=' + encodeURIComponent(path)));
return api.notifications.browse().then(function (notifications) {
assert.equal(notifications.length, 1);
return done();
});
});
});
it('should call next if session user exists', function (done) {
@ -133,33 +143,37 @@ describe('Middleware', function () {
});
describe('cleanNotifications', function () {
var ghost = new Ghost();
beforeEach(function () {
ghost.notifications = [
{
beforeEach(function (done) {
api.notifications.add({
id: 0,
status: 'passive',
message: 'passive-one'
},
{
status: 'passive',
message: 'passive-two'
},
{
status: 'aggressive',
message: 'aggressive'
}
];
}).then(function () {
return api.notifications.add({
id: 1,
status: 'passive',
message: 'passive-two'});
}).then(function () {
return api.notifications.add({
id: 2,
status: 'aggressive',
message: 'aggressive'});
}).then(function () {
done();
});
});
it('should clean all passive messages', function (done) {
middleware.cleanNotifications(null, null, function () {
assert.equal(ghost.notifications.length, 1);
var passiveMsgs = _.filter(ghost.notifications, function (notification) {
return notification.status === 'passive';
api.notifications.browse().then(function (notifications) {
should(notifications.length).eql(1);
var passiveMsgs = _.filter(notifications, function (notification) {
return notification.status === 'passive';
});
assert.equal(passiveMsgs.length, 0);
return done();
});
assert.equal(passiveMsgs.length, 0);
return done();
});
});
});

View File

@ -1,10 +1,11 @@
/*globals describe, beforeEach, it*/
var testUtils = require('../utils'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
_ = require('underscore'),
path = require('path'),
should = require('should'),
sinon = require('sinon'),
when = require('when'),
_ = require('underscore'),
path = require('path'),
api = require('../../server/api'),
// Stuff we are testing
handlebars = require('express-hbs').handlebars,
@ -13,15 +14,37 @@ var testUtils = require('../utils'),
describe('Core Helpers', function () {
var ghost;
var ghost,
sandbox,
blogGlobalsStub,
apiStub;
beforeEach(function (done) {
ghost = new Ghost();
sandbox = sinon.sandbox.create();
apiStub = sandbox.stub(api.settings , 'read', function () {
return when({value: 'casper'});
});
blogGlobalsStub = sandbox.stub(ghost, 'blogGlobals', function () {
return {
path: '',
//url: 'http://127.0.0.1:2368',
title: 'Ghost',
description: 'Just a blogging platform.',
url: 'http://testurl.com'
};
});
helpers.loadCoreHelpers(ghost).then(function () {
done();
}, done);
});
afterEach(function () {
sandbox.restore();
});
describe('Content Helper', function () {
it('has loaded content helper', function () {
should.exist(handlebars.helpers.content);
@ -292,33 +315,36 @@ describe('Core Helpers', function () {
should.exist(handlebars.helpers.url);
});
it('should return a the slug with a prefix slash if the context is a post', function () {
var rendered = helpers.url.call({html: 'content', markdown: "ff", title: "title", slug: "slug", created_at: new Date(0)});
should.exist(rendered);
rendered.should.equal('/slug/');
it('should return the slug with a prefix slash if the context is a post', function () {
helpers.url.call({html: 'content', markdown: "ff", title: "title", slug: "slug", created_at: new Date(0)}).then(function (rendered) {
should.exist(rendered);
rendered.should.equal('/slug/');
});
});
it('should output an absolute URL if the option is present', function () {
var configStub = sinon.stub(ghost, "blogGlobals", function () {
return { url: 'http://testurl.com' };
}),
rendered = helpers.url.call(
helpers.url.call(
{html: 'content', markdown: "ff", title: "title", slug: "slug", created_at: new Date(0)},
{hash: { absolute: 'true'}}
);
should.exist(rendered);
rendered.should.equal('http://testurl.com/slug/');
configStub.restore();
{hash: { absolute: 'true'}})
.then(function (rendered) {
should.exist(rendered);
rendered.should.equal('http://testurl.com/slug/');
});
});
it('should return empty string if not a post', function () {
helpers.url.call({markdown: "ff", title: "title", slug: "slug"}).should.equal('');
helpers.url.call({html: 'content', title: "title", slug: "slug"}).should.equal('');
helpers.url.call({html: 'content', markdown: "ff", slug: "slug"}).should.equal('');
helpers.url.call({html: 'content', markdown: "ff", title: "title"}).should.equal('');
helpers.url.call({markdown: "ff", title: "title", slug: "slug"}).then(function (rendered) {
rendered.should.equal('');
});
helpers.url.call({html: 'content', title: "title", slug: "slug"}).then(function (rendered) {
rendered.should.equal('');
});
helpers.url.call({html: 'content', markdown: "ff", slug: "slug"}).then(function (rendered) {
rendered.should.equal('');
});
helpers.url.call({html: 'content', markdown: "ff", title: "title"}).then(function (rendered) {
rendered.should.equal('');
});
});
});