diff --git a/core/server/config/loader.js b/core/server/config/loader.js index d14943c0ed..ad7c73ece3 100644 --- a/core/server/config/loader.js +++ b/core/server/config/loader.js @@ -12,15 +12,20 @@ var fs = require('fs'), paths = require('./paths'), appRoot = paths().appRoot, - configexample = paths().configExample, - configFile = process.env.GHOST_CONFIG || paths().config; + configExample = paths().configExample, + configFile = process.env.GHOST_CONFIG || paths().config, + rejectMessage = 'Unable to load config'; + +function readConfigFile(envVal) { + return require(configFile)[envVal]; +} function writeConfigFile() { var written = when.defer(); /* Check for config file and copy from config.example.js if one doesn't exist. After that, start the server. */ - fs.exists(configexample, function checkTemplate(templateExists) { + fs.exists(configExample, function checkTemplate(templateExists) { var read, write; @@ -29,7 +34,7 @@ function writeConfigFile() { } // Copy config.example.js => config.js - read = fs.createReadStream(configexample); + read = fs.createReadStream(configExample); read.on('error', function (err) { /*jslint unparam:true*/ return errors.logError(new Error('Could not open config.example.js for read.'), appRoot, 'Please check your deployment for config.js or config.example.js.'); @@ -50,14 +55,13 @@ function writeConfigFile() { function validateConfigEnvironment() { var envVal = process.env.NODE_ENV || undefined, - rejectMessage = 'Unable to load config', hasHostAndPort, hasSocket, config, parsedUrl; try { - config = require(configFile)[envVal]; + config = readConfigFile(envVal); } catch (ignore) { } @@ -76,8 +80,13 @@ function validateConfigEnvironment() { return when.reject(rejectMessage); } + if (/\/ghost(\/|$)/.test(parsedUrl.pathname)) { + errors.logError(new Error('Your site url in config.js cannot contain a subdirectory called ghost.'), config.url, 'Please rename the subdirectory before restarting'); + return when.reject(rejectMessage); + } + // Check that we have database values - if (!config.database) { + if (!config.database || !config.database.client) { errors.logError(new Error('Your database configuration in config.js is invalid.'), JSON.stringify(config.database), 'Please make sure this is a valid Bookshelf database configuration'); return when.reject(rejectMessage); } diff --git a/core/server/storage/localfilesystem.js b/core/server/storage/localfilesystem.js index d6efa1675f..53b30810d5 100644 --- a/core/server/storage/localfilesystem.js +++ b/core/server/storage/localfilesystem.js @@ -8,7 +8,7 @@ var _ = require('underscore'), path = require('path'), when = require('when'), errors = require('../errorHandling'), - config = require('../config'), + configPaths = require('../config/paths'), baseStore = require('./base'), localFileStore; @@ -20,7 +20,7 @@ localFileStore = _.extend(baseStore, { // - returns a promise which ultimately returns the full url to the uploaded image 'save': function (image) { var saved = when.defer(), - targetDir = this.getTargetDir(config.paths().imagesRelPath), + targetDir = this.getTargetDir(configPaths().imagesRelPath), targetFilename; this.getUniqueFileName(this, image, targetDir).then(function (filename) { @@ -33,7 +33,7 @@ localFileStore = _.extend(baseStore, { }).then(function () { // The src for the image must be in URI format, not a file system path, which in Windows uses \ // For local file system storage can use relative path so add a slash - var fullUrl = (config.paths().subdir + '/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/'); + var fullUrl = (configPaths().subdir + '/' + targetFilename).replace(new RegExp('\\' + path.sep, 'g'), '/'); return saved.resolve(fullUrl); }).otherwise(function (e) { errors.logError(e); @@ -56,7 +56,7 @@ localFileStore = _.extend(baseStore, { // middleware for serving the files 'serve': function () { - return express['static'](config.paths().imagesPath); + return express['static'](configPaths().imagesPath); } }); diff --git a/core/test/unit/config_spec.js b/core/test/unit/config_spec.js index 606d28da1f..a19b245266 100644 --- a/core/test/unit/config_spec.js +++ b/core/test/unit/config_spec.js @@ -1,14 +1,221 @@ /*globals describe, it, beforeEach, afterEach */ -var should = require('should'), - sinon = require('sinon'), - when = require('when'), - path = require('path'), +var should = require('should'), + sinon = require('sinon'), + when = require('when'), + path = require('path'), + fs = require('fs'), + _ = require('underscore'), + rewire = require("rewire"), - config = require('../../server/config'); + // Thing we are testing + defaultConfig = require('../../../config.example')[process.env.NODE_ENV], + loader = rewire('../../server/config/loader'), + theme = rewire('../../server/config/theme'), + paths = rewire('../../server/config/paths'); describe('Config', function () { + describe('Loader', function () { + var sandbox, + rejectMessage = loader.__get__('rejectMessage'), + overrideConfig = function (newConfig) { + loader.__set__("readConfigFile", sandbox.stub().returns( + _.extend({}, defaultConfig, newConfig) + )); + }; + + + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function () { + loader = rewire('../../server/config/loader'); + sandbox.restore(); + + }); + + it('loads the config file if one exists', function (done) { + // the test infrastructure is setup so that there is always config present, + // but we want to overwrite the test to actually load config.example.js, so that any local changes + // don't break the tests + loader.__set__("configFile", path.join(paths().appRoot, 'config.example.js')); + + loader().then(function (config) { + config.url.should.equal(defaultConfig.url); + config.database.client.should.equal(defaultConfig.database.client); + config.database.connection.should.eql(defaultConfig.database.connection); + config.server.host.should.equal(defaultConfig.server.host); + config.server.port.should.equal(defaultConfig.server.port); + + done(); + }).then(null, done); + }); + + it('creates the config file if one does not exist', function (done) { + + var deferred = when.defer(), + // trick loader into thinking that the config file doesn't exist yet + existsStub = sandbox.stub(fs, 'exists', function (file, cb) { return cb(false); }), + // create a method which will return a pre-resolved promise + resolvedPromise = sandbox.stub().returns(deferred.promise); + + deferred.resolve(); + + // ensure that the file creation is a stub, the tests shouldn't really create a file + loader.__set__("writeConfigFile", resolvedPromise); + loader.__set__("validateConfigEnvironment", resolvedPromise); + + loader().then(function () { + existsStub.calledOnce.should.be.true; + resolvedPromise.calledTwice.should.be.true; + done(); + }).then(null, done); + }); + + it('accepts valid urls', function (done) { + // replace the config file with invalid data + overrideConfig({url: 'http://testurl.com'}); + + loader().then(function (localConfig) { + localConfig.url.should.equal('http://testurl.com'); + + // Next test + overrideConfig({url: 'https://testurl.com'}); + return loader(); + }).then(function (localConfig) { + localConfig.url.should.equal('https://testurl.com'); + + // Next test + overrideConfig({url: 'http://testurl.com/blog/'}); + return loader(); + }).then(function (localConfig) { + localConfig.url.should.equal('http://testurl.com/blog/'); + + // Next test + overrideConfig({url: 'http://testurl.com/ghostly/'}); + return loader(); + }).then(function (localConfig) { + localConfig.url.should.equal('http://testurl.com/ghostly/'); + + // Next test + overrideConfig({url: '//testurl.com'}); + return loader(); + }).then(function (localConfig) { + localConfig.url.should.equal('//testurl.com'); + + done(); + }).then(null, done); + }); + + it('rejects invalid urls', function (done) { + // replace the config file with invalid data + overrideConfig({url: 'notvalid'}); + + loader().otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({url: 'something.com'}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + done(); + }).then(function () { + should.fail('no error was thrown when it should have been'); + done(); + }).then(done, null); + }); + + it('does not permit subdirectories named ghost', function (done) { + // replace the config file with invalid data + overrideConfig({url: 'http://testurl.com/ghost/'}); + + loader().otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({url: 'http://testurl.com/ghost/blog/'}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({url: 'http://testurl.com/blog/ghost'}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + done(); + }).then(function () { + should.fail('no error was thrown when it should have been'); + done(); + }).then(done, null); + }); + + it('requires a database config', function (done) { + // replace the config file with invalid data + overrideConfig({database: null}); + + loader().otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({database: {}}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + done(); + }).then(function () { + should.fail('no error was thrown when it should have been'); + done(); + }).then(done, null); + }); + + + it('requires a socket or a host and port', function (done) { + // replace the config file with invalid data + overrideConfig({server: {socket: 'test'}}); + + loader().then(function (localConfig) { + localConfig.server.socket.should.equal('test'); + + // Next test + overrideConfig({server: null}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({server: {host: null}}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({server: {port: null}}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + // Next test + overrideConfig({server: {host: null, port: null}}); + return loader(); + }).otherwise(function (error) { + error.should.include(rejectMessage); + + done(); + }).then(function () { + should.fail('no error was thrown when it should have been'); + done(); + }).then(done, null); + }); + }); + describe('Theme', function () { var sandbox, @@ -24,13 +231,13 @@ describe('Config', function () { return when({value: 'casper'}); }); - config.theme.update(settings, 'http://my-ghost-blog.com') + theme.update(settings, 'http://my-ghost-blog.com') .then(done) .then(null, done); }); afterEach(function (done) { - config.theme.update(settings, config().url) + theme.update(settings, defaultConfig.url) .then(done) .then(null, done); @@ -38,14 +245,14 @@ describe('Config', function () { }); it('should have exactly the right keys', function () { - var themeConfig = config.theme(); + var themeConfig = theme(); // This will fail if there are any extra keys themeConfig.should.have.keys('url', 'title', 'description', 'logo', 'cover'); }); it('should have the correct values for each key', function () { - var themeConfig = config.theme(); + var themeConfig = theme(); // Check values are as we expect themeConfig.should.have.property('url', 'http://my-ghost-blog.com'); @@ -67,7 +274,7 @@ describe('Config', function () { }); afterEach(function (done) { - config.paths.update(config().url) + paths.update(defaultConfig.url) .then(done) .then(null, done); @@ -75,7 +282,7 @@ describe('Config', function () { }); it('should have exactly the right keys', function () { - var pathConfig = config.paths(); + var pathConfig = paths(); // This will fail if there are any extra keys pathConfig.should.have.keys( @@ -100,7 +307,7 @@ describe('Config', function () { }); it('should have the correct values for each key', function () { - var pathConfig = config.paths(), + var pathConfig = paths(), appRoot = path.resolve(__dirname, '../../../'); pathConfig.should.have.property('appRoot', appRoot); @@ -108,32 +315,32 @@ describe('Config', function () { }); it('should not return a slash for subdir', function (done) { - config.paths.update('http://my-ghost-blog.com').then(function () { - config.paths().should.have.property('subdir', ''); + paths.update('http://my-ghost-blog.com').then(function () { + paths().should.have.property('subdir', ''); - return config.paths.update('http://my-ghost-blog.com/'); + return paths.update('http://my-ghost-blog.com/'); }).then(function () { - config.paths().should.have.property('subdir', ''); + paths().should.have.property('subdir', ''); done(); }).otherwise(done); }); it('should handle subdirectories properly', function (done) { - config.paths.update('http://my-ghost-blog.com/blog').then(function () { - config.paths().should.have.property('subdir', '/blog'); + paths.update('http://my-ghost-blog.com/blog').then(function () { + paths().should.have.property('subdir', '/blog'); - return config.paths.update('http://my-ghost-blog.com/blog/'); + return paths.update('http://my-ghost-blog.com/blog/'); }).then(function () { - config.paths().should.have.property('subdir', '/blog'); + paths().should.have.property('subdir', '/blog'); - return config.paths.update('http://my-ghost-blog.com/my/blog'); + return paths.update('http://my-ghost-blog.com/my/blog'); }).then(function () { - config.paths().should.have.property('subdir', '/my/blog'); + paths().should.have.property('subdir', '/my/blog'); - return config.paths.update('http://my-ghost-blog.com/my/blog/'); + return paths.update('http://my-ghost-blog.com/my/blog/'); }).then(function () { - config.paths().should.have.property('subdir', '/my/blog'); + paths().should.have.property('subdir', '/my/blog'); done(); }).otherwise(done); diff --git a/package.json b/package.json index 4944553166..015504973c 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "grunt-update-submodules": "~0.2.0", "matchdep": "~0.3.0", "mocha": "~1.15.1", + "rewire": "~2.0.0", "request": "~2.29.0", "require-dir": "~0.1.0", "should": "~2.1.1",