2013-05-11 20:44:25 +04:00
|
|
|
|
// # Ghost Module
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Defines core methods required to build the application
|
2013-05-11 20:44:25 +04:00
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Module dependencies
|
2013-06-25 15:43:15 +04:00
|
|
|
|
var config = require('./../config'),
|
|
|
|
|
when = require('when'),
|
|
|
|
|
express = require('express'),
|
2013-07-11 23:02:18 +04:00
|
|
|
|
errors = require('./server/errorHandling'),
|
2013-07-11 02:45:13 +04:00
|
|
|
|
fs = require('fs'),
|
2013-06-25 15:43:15 +04:00
|
|
|
|
path = require('path'),
|
|
|
|
|
hbs = require('express-hbs'),
|
2013-07-11 02:45:13 +04:00
|
|
|
|
nodefn = require('when/node/function'),
|
2013-06-25 15:43:15 +04:00
|
|
|
|
_ = require('underscore'),
|
|
|
|
|
Polyglot = require('node-polyglot'),
|
2013-08-21 00:19:47 +04:00
|
|
|
|
Mailer = require('./server/mail'),
|
2013-07-11 23:02:18 +04:00
|
|
|
|
models = require('./server/models'),
|
2013-08-05 19:11:32 +04:00
|
|
|
|
plugins = require('./server/plugins'),
|
2013-07-11 23:02:18 +04:00
|
|
|
|
requireTree = require('./server/require-tree'),
|
2013-08-16 03:22:08 +04:00
|
|
|
|
permissions = require('./server/permissions'),
|
2013-08-24 04:02:01 +04:00
|
|
|
|
uuid = require('node-uuid'),
|
2013-08-06 23:27:56 +04:00
|
|
|
|
|
|
|
|
|
// Variables
|
2013-08-12 18:13:15 +04:00
|
|
|
|
appRoot = path.resolve(__dirname, '../'),
|
|
|
|
|
themePath = path.resolve(appRoot + '/content/themes'),
|
|
|
|
|
pluginPath = path.resolve(appRoot + '/content/plugins'),
|
2013-07-01 23:24:48 +04:00
|
|
|
|
themeDirectories = requireTree(themePath),
|
|
|
|
|
pluginDirectories = requireTree(pluginPath),
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
|
|
Ghost,
|
|
|
|
|
instance,
|
|
|
|
|
defaults,
|
|
|
|
|
statuses;
|
|
|
|
|
|
|
|
|
|
// ## Default values
|
|
|
|
|
/**
|
|
|
|
|
* A hash of default values to use instead of 'magic' numbers/strings.
|
|
|
|
|
* @type {Object}
|
|
|
|
|
*/
|
|
|
|
|
defaults = {
|
|
|
|
|
filterPriority: 5,
|
|
|
|
|
maxPriority: 9
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ## Article Statuses
|
|
|
|
|
/**
|
2013-09-04 21:45:14 +04:00
|
|
|
|
* A list of article status types
|
2013-06-25 15:43:15 +04:00
|
|
|
|
* @type {Object}
|
|
|
|
|
*/
|
|
|
|
|
statuses = {
|
|
|
|
|
'draft': 'draft',
|
|
|
|
|
'complete': 'complete',
|
|
|
|
|
'approved': 'approved',
|
|
|
|
|
'scheduled': 'scheduled',
|
|
|
|
|
'published': 'published'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ## Module Methods
|
|
|
|
|
/**
|
|
|
|
|
* @method Ghost
|
|
|
|
|
* @returns {*}
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
|
|
|
|
Ghost = function () {
|
2013-09-06 19:54:50 +04:00
|
|
|
|
var polyglot;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
|
|
if (!instance) {
|
|
|
|
|
instance = this;
|
|
|
|
|
|
|
|
|
|
// Holds the filters
|
|
|
|
|
instance.filterCallbacks = [];
|
|
|
|
|
|
|
|
|
|
// Holds the filter hooks (that are built in to Ghost Core)
|
|
|
|
|
instance.filters = [];
|
|
|
|
|
|
|
|
|
|
// Holds the theme directories temporarily
|
|
|
|
|
instance.themeDirectories = {};
|
|
|
|
|
|
|
|
|
|
// Holds the plugin directories temporarily
|
|
|
|
|
instance.pluginDirectories = {};
|
|
|
|
|
|
2013-08-03 19:11:16 +04:00
|
|
|
|
// Holds the persistent notifications
|
|
|
|
|
instance.notifications = [];
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Holds the available plugins
|
2013-08-05 19:11:32 +04:00
|
|
|
|
instance.availablePlugins = {};
|
|
|
|
|
|
2013-08-24 04:02:01 +04:00
|
|
|
|
// Holds the dbhash (mainly used for cookie secret)
|
|
|
|
|
instance.dbHash = undefined;
|
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
polyglot = new Polyglot();
|
|
|
|
|
|
|
|
|
|
_.extend(instance, {
|
2013-09-11 19:23:07 +04:00
|
|
|
|
config: function () { return config[process.env.NODE_ENV]; },
|
2013-06-25 15:43:15 +04:00
|
|
|
|
|
|
|
|
|
// there's no management here to be sure this has loaded
|
|
|
|
|
settings: function () { return instance.settingsCache; },
|
|
|
|
|
dataProvider: models,
|
2013-09-02 21:27:26 +04:00
|
|
|
|
blogGlobals: function () {
|
|
|
|
|
/* 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 {
|
2013-09-11 19:23:07 +04:00
|
|
|
|
url: instance.config().url,
|
2013-09-02 21:27:26 +04:00
|
|
|
|
title: instance.settings().title,
|
|
|
|
|
description: instance.settings().description,
|
2013-09-09 20:40:20 +04:00
|
|
|
|
logo: instance.settings().logo,
|
|
|
|
|
cover: instance.settings().cover
|
2013-09-02 21:27:26 +04:00
|
|
|
|
};
|
|
|
|
|
},
|
2013-06-25 15:43:15 +04:00
|
|
|
|
statuses: function () { return statuses; },
|
|
|
|
|
polyglot: function () { return polyglot; },
|
2013-08-21 00:19:47 +04:00
|
|
|
|
mail: new Mailer(),
|
2013-06-25 15:43:15 +04:00
|
|
|
|
getPaths: function () {
|
|
|
|
|
return when.all([themeDirectories, pluginDirectories]).then(function (paths) {
|
|
|
|
|
instance.themeDirectories = paths[0];
|
|
|
|
|
instance.pluginDirectories = paths[1];
|
|
|
|
|
return;
|
2013-06-16 20:14:01 +04:00
|
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
|
},
|
|
|
|
|
paths: function () {
|
|
|
|
|
return {
|
2013-08-12 18:13:15 +04:00
|
|
|
|
'appRoot': appRoot,
|
|
|
|
|
'themePath': themePath,
|
|
|
|
|
'pluginPath': pluginPath,
|
2013-08-30 15:20:30 +04:00
|
|
|
|
'activeTheme': path.join(themePath, !instance.settingsCache ? "" : instance.settingsCache.activeTheme),
|
2013-08-12 18:13:15 +04:00
|
|
|
|
'adminViews': path.join(appRoot, '/core/server/views/'),
|
|
|
|
|
'helperTemplates': path.join(appRoot, '/core/server/helpers/tpl/'),
|
|
|
|
|
'lang': path.join(appRoot, '/core/shared/lang/'),
|
2013-07-11 23:02:18 +04:00
|
|
|
|
'availableThemes': instance.themeDirectories,
|
2013-06-25 15:43:15 +04:00
|
|
|
|
'availablePlugins': instance.pluginDirectories
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return instance;
|
|
|
|
|
};
|
2013-06-16 20:14:01 +04:00
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Initialise the application
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.init = function () {
|
|
|
|
|
var self = this;
|
2013-05-11 20:44:25 +04:00
|
|
|
|
|
2013-08-21 00:19:47 +04:00
|
|
|
|
return when.join(
|
|
|
|
|
instance.dataProvider.init(),
|
|
|
|
|
instance.getPaths(),
|
|
|
|
|
instance.mail.init(self)
|
|
|
|
|
).then(function () {
|
2013-09-02 05:49:08 +04:00
|
|
|
|
return models.Settings.populateDefaults();
|
2013-09-02 05:49:08 +04:00
|
|
|
|
}).then(function () {
|
|
|
|
|
// Initialize plugins
|
|
|
|
|
return self.initPlugins();
|
2013-08-16 03:22:08 +04:00
|
|
|
|
}).then(function () {
|
|
|
|
|
// Initialize the settings cache
|
2013-06-25 15:43:15 +04:00
|
|
|
|
return self.updateSettingsCache();
|
2013-09-06 15:34:44 +04:00
|
|
|
|
}).then(function () {
|
|
|
|
|
return self.initPlugins();
|
2013-08-16 03:22:08 +04:00
|
|
|
|
}).then(function () {
|
|
|
|
|
// Initialize the permissions actions and objects
|
|
|
|
|
return permissions.init();
|
2013-08-24 04:02:01 +04:00
|
|
|
|
}).then(function () {
|
|
|
|
|
// get the settings and whatnot
|
|
|
|
|
return when(models.Settings.read('dbHash')).then(function (dbhash) {
|
|
|
|
|
// we already ran this, chill
|
|
|
|
|
self.dbHash = dbhash.attributes.value;
|
|
|
|
|
return dbhash.attributes.value;
|
|
|
|
|
}).otherwise(function (error) {
|
|
|
|
|
// this is where all the "first run" functionality should go
|
|
|
|
|
var dbhash = uuid.v4();
|
|
|
|
|
return when(models.Settings.add({key: 'dbHash', value: dbhash})).then(function (returned) {
|
|
|
|
|
self.dbHash = dbhash;
|
|
|
|
|
return dbhash;
|
|
|
|
|
});
|
|
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
|
}, errors.logAndThrowError);
|
|
|
|
|
};
|
2013-05-11 20:44:25 +04:00
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Maintain the internal cache of the settings object
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.updateSettingsCache = function (settings) {
|
|
|
|
|
var self = this;
|
2013-06-09 20:16:25 +04:00
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
settings = settings || {};
|
2013-06-09 20:16:25 +04:00
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
if (!_.isEmpty(settings)) {
|
|
|
|
|
self.settingsCache = settings;
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: this should use api.browse
|
|
|
|
|
return models.Settings.findAll().then(function (result) {
|
|
|
|
|
var settings = {};
|
|
|
|
|
_.map(result.models, function (member) {
|
|
|
|
|
if (!settings.hasOwnProperty(member.attributes.key)) {
|
2013-08-30 15:20:30 +04:00
|
|
|
|
if (member.attributes.key === 'activeTheme') {
|
|
|
|
|
member.attributes.value = member.attributes.value.substring(member.attributes.value.lastIndexOf('/') + 1);
|
|
|
|
|
var settingsThemePath = path.join(themePath, member.attributes.value);
|
|
|
|
|
fs.exists(settingsThemePath, function (exists) {
|
|
|
|
|
if (!exists) {
|
|
|
|
|
member.attributes.value = "casper";
|
|
|
|
|
}
|
|
|
|
|
settings[member.attributes.key] = member.attributes.value;
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2013-06-25 15:43:15 +04:00
|
|
|
|
settings[member.attributes.key] = member.attributes.value;
|
|
|
|
|
}
|
|
|
|
|
});
|
2013-05-11 20:44:25 +04:00
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
self.settingsCache = settings;
|
|
|
|
|
}, errors.logAndThrowError);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2013-07-11 02:45:13 +04:00
|
|
|
|
// ## Template utils
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Compile a template for a handlebars helper
|
2013-07-11 02:45:13 +04:00
|
|
|
|
Ghost.prototype.compileTemplate = function (templatePath) {
|
|
|
|
|
return nodefn.call(fs.readFile, templatePath).then(function (templateContents) {
|
|
|
|
|
return hbs.handlebars.compile(templateContents.toString());
|
|
|
|
|
}, errors.logAndThrowError);
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Load a template for a handlebars helper
|
2013-07-11 02:45:13 +04:00
|
|
|
|
Ghost.prototype.loadTemplate = function (name) {
|
2013-08-02 21:51:33 +04:00
|
|
|
|
var self = this,
|
|
|
|
|
templateFileName = name + '.hbs',
|
|
|
|
|
// Check for theme specific version first
|
|
|
|
|
templatePath = path.join(this.paths().activeTheme, "partials", templateFileName),
|
|
|
|
|
deferred = when.defer();
|
|
|
|
|
|
|
|
|
|
// 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(self.paths().helperTemplates, templateFileName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.compileTemplate(templatePath).then(deferred.resolve, deferred.reject);
|
|
|
|
|
});
|
2013-07-11 02:45:13 +04:00
|
|
|
|
|
2013-08-02 21:51:33 +04:00
|
|
|
|
return deferred.promise;
|
2013-07-11 02:45:13 +04:00
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Register a handlebars helper for themes
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.registerThemeHelper = function (name, fn) {
|
|
|
|
|
hbs.registerHelper(name, fn);
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Register a new filter callback function
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.registerFilter = function (name, priority, fn) {
|
|
|
|
|
// Curry the priority optional parameter to a default of 5
|
|
|
|
|
if (_.isFunction(priority)) {
|
|
|
|
|
fn = priority;
|
|
|
|
|
priority = defaults.filterPriority;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.filterCallbacks[name] = this.filterCallbacks[name] || {};
|
|
|
|
|
this.filterCallbacks[name][priority] = this.filterCallbacks[name][priority] || [];
|
|
|
|
|
|
|
|
|
|
this.filterCallbacks[name][priority].push(fn);
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Unregister a filter callback function
|
2013-08-05 19:11:32 +04:00
|
|
|
|
Ghost.prototype.unregisterFilter = function (name, priority, fn) {
|
|
|
|
|
// Curry the priority optional parameter to a default of 5
|
|
|
|
|
if (_.isFunction(priority)) {
|
|
|
|
|
fn = priority;
|
|
|
|
|
priority = defaults.filterPriority;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if it even exists
|
|
|
|
|
if (this.filterCallbacks[name] && this.filterCallbacks[name][priority]) {
|
|
|
|
|
// Remove the function from the list of filter funcs
|
|
|
|
|
this.filterCallbacks[name][priority] = _.without(this.filterCallbacks[name][priority], fn);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Execute filter functions in priority order
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.doFilter = function (name, args, callback) {
|
|
|
|
|
var callbacks = this.filterCallbacks[name];
|
|
|
|
|
|
|
|
|
|
// Bug out early if no callbacks by that name
|
|
|
|
|
if (!callbacks) {
|
|
|
|
|
return callback(args);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_.times(defaults.maxPriority + 1, function (priority) {
|
|
|
|
|
// Bug out if no handlers on this priority
|
|
|
|
|
if (!_.isArray(callbacks[priority])) {
|
|
|
|
|
return;
|
2013-05-11 20:44:25 +04:00
|
|
|
|
}
|
2013-05-27 23:27:41 +04:00
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
// Call each handler for this priority level
|
|
|
|
|
_.each(callbacks[priority], function (filterHandler) {
|
2013-07-07 22:02:26 +04:00
|
|
|
|
try {
|
|
|
|
|
args = filterHandler(args);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
// If a filter causes an error, we log it so that it can be debugged, but do not throw the error
|
|
|
|
|
errors.logError(e);
|
|
|
|
|
}
|
2013-06-09 20:16:25 +04:00
|
|
|
|
});
|
2013-06-25 15:43:15 +04:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
callback(args);
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Initialise plugins. Will load from config.activePlugins by default
|
2013-08-05 19:11:32 +04:00
|
|
|
|
Ghost.prototype.initPlugins = function (pluginsToLoad) {
|
2013-09-06 15:34:44 +04:00
|
|
|
|
pluginsToLoad = pluginsToLoad || models.Settings.activePlugins;
|
2013-08-05 19:11:32 +04:00
|
|
|
|
|
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
|
|
return plugins.init(this, pluginsToLoad).then(function (loadedPlugins) {
|
|
|
|
|
// Extend the loadedPlugins onto the available plugins
|
|
|
|
|
_.extend(self.availablePlugins, loadedPlugins);
|
|
|
|
|
}, errors.logAndThrowError);
|
|
|
|
|
};
|
|
|
|
|
|
2013-08-06 23:27:56 +04:00
|
|
|
|
// Initialise Theme or admin
|
2013-06-25 15:43:15 +04:00
|
|
|
|
Ghost.prototype.initTheme = function (app) {
|
2013-09-02 21:27:26 +04:00
|
|
|
|
var self = this,
|
|
|
|
|
hbsOptions;
|
2013-06-25 15:43:15 +04:00
|
|
|
|
return function initTheme(req, res, next) {
|
|
|
|
|
app.set('view engine', 'hbs');
|
2013-06-26 01:47:29 +04:00
|
|
|
|
// return the correct mime type for woff files
|
|
|
|
|
express['static'].mime.define({'application/font-woff': ['woff']});
|
|
|
|
|
|
2013-06-26 01:36:53 +04:00
|
|
|
|
if (!res.isAdmin) {
|
2013-09-02 21:27:26 +04:00
|
|
|
|
|
|
|
|
|
// self.globals is a hack til we have a better way of getting combined settings & config
|
|
|
|
|
hbsOptions = {templateOptions: {data: {blog: self.blogGlobals()}}};
|
|
|
|
|
|
2013-09-02 14:16:01 +04:00
|
|
|
|
if (!self.themeDirectories.hasOwnProperty(self.settings().activeTheme)) {
|
|
|
|
|
// Throw an error if the theme is not available...
|
|
|
|
|
// TODO: move this to happen on app start
|
|
|
|
|
errors.logAndThrowError('The currently active theme ' + self.settings().activeTheme + ' is missing.');
|
|
|
|
|
} else if (self.themeDirectories[self.settings().activeTheme].hasOwnProperty('partials')) {
|
|
|
|
|
// Check that the theme has a partials directory before trying to use it
|
2013-09-02 21:27:26 +04:00
|
|
|
|
hbsOptions.partialsDir = path.join(self.paths().activeTheme, 'partials');
|
2013-09-02 14:16:01 +04:00
|
|
|
|
}
|
|
|
|
|
|
2013-09-02 21:27:26 +04:00
|
|
|
|
app.engine('hbs', hbs.express3(hbsOptions));
|
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
app.set('views', self.paths().activeTheme);
|
|
|
|
|
} else {
|
|
|
|
|
app.engine('hbs', hbs.express3({partialsDir: self.paths().adminViews + 'partials'}));
|
|
|
|
|
app.set('views', self.paths().adminViews);
|
2013-07-11 23:02:18 +04:00
|
|
|
|
app.use('/public', express['static'](path.join(__dirname, '/client/assets')));
|
|
|
|
|
app.use('/public', express['static'](path.join(__dirname, '/client')));
|
2013-06-25 15:43:15 +04:00
|
|
|
|
}
|
|
|
|
|
app.use(express['static'](self.paths().activeTheme));
|
2013-08-25 17:19:27 +04:00
|
|
|
|
app.use('/shared', express['static'](path.join(__dirname, '/shared')));
|
2013-06-25 15:43:15 +04:00
|
|
|
|
app.use('/content/images', express['static'](path.join(__dirname, '/../content/images')));
|
|
|
|
|
next();
|
2013-05-11 20:44:25 +04:00
|
|
|
|
};
|
2013-06-25 15:43:15 +04:00
|
|
|
|
};
|
2013-05-11 20:44:25 +04:00
|
|
|
|
|
2013-06-25 15:43:15 +04:00
|
|
|
|
// TODO: Expose the defaults for other people to see/manipulate as a static value?
|
|
|
|
|
// Ghost.defaults = defaults;
|
2013-06-09 20:16:25 +04:00
|
|
|
|
|
2013-08-09 05:22:49 +04:00
|
|
|
|
module.exports = Ghost;
|