0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Merge pull request #7412 from TryGhost/playground

Refactored config, utf8mb4 support, and more alpha features
This commit is contained in:
Hannah Wolfe 2016-09-20 16:33:21 +01:00 committed by GitHub
commit 58c5b96b30
154 changed files with 1647 additions and 2871 deletions

View file

@ -100,7 +100,7 @@ Any other info e.g. Why do you consider this to be a bug? What did you expect to
* Ghost Version: master (latest commit: a761de2079dca4df49567b1bddac492f25033985)
* Node Version: 4.4.7
* Browser: Chrome 48.0.2564.109 on Mac OS X 10.10.4
* Database: SQLite / MySQL / postgres
* Database: SQLite / MySQL
```
<a name="features"></a>

3
.gitignore vendored
View file

@ -60,7 +60,8 @@ CHANGELOG.md
/core/test/functional/*.png
/core/test/coverage
/config.js
# ignore all custom json files for config
config.*.json
# Built asset files
/core/built

View file

@ -3,7 +3,6 @@ language: node_js
node_js:
- "4"
- "0.12"
- "0.10"
- "6"
sudo: false
cache:
@ -11,8 +10,6 @@ cache:
- node_modules
- core/client/node_modules
- core/client/bower_components
addons:
postgresql: "9.3"
env:
global:
- GITHUB_OAUTH_KEY=003a44d58f12089d0c0261338298af3813330949
@ -21,20 +18,16 @@ env:
matrix:
- DB=sqlite3 NODE_ENV=testing
- DB=mysql NODE_ENV=testing-mysql
- DB=pg NODE_ENV=testing-pg
matrix:
include:
- node_js: "4"
env: TEST_SUITE=lint
allow_failures:
- node_js: "6"
fast_finish: true
branches:
except:
- /^greenkeeper-.+$/
before_install:
- if [ $DB == "mysql" ]; then mysql -e 'create database ghost_testing'; fi
- if [ $DB == "pg" ]; then psql -c 'create database ghost_testing;' -U postgres; fi
after_success:
- |
if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then

View file

@ -407,25 +407,6 @@ var overrides = require('./core/server/overrides'),
cfg.express.test.options.node_env = process.env.NODE_ENV;
});
// #### Ensure Config *(Utility Task)*
// Make sure that we have a `config.js` file when running tests
// Ghost requires a `config.js` file to specify the database settings etc. Ghost comes with an example file:
// `config.example.js` which is copied and renamed to `config.js` by the bootstrap process
grunt.registerTask('ensureConfig', function () {
var config = require('./core/server/config'),
done = this.async();
if (!process.env.TEST_SUITE || process.env.TEST_SUITE !== 'client') {
config.load().then(function () {
done();
}).catch(function (err) {
grunt.fail.fatal(err.stack);
});
} else {
done();
}
});
// #### Reset Database to "New" state *(Utility Task)*
// Drops all database tables and then runs the migration process to put the database
// in a "new" state.
@ -538,7 +519,7 @@ var overrides = require('./core/server/overrides'),
// ### test-setup *(utility)(
// `grunt test-setup` will run all the setup tasks required for running tests
grunt.registerTask('test-setup', 'Setup ready to run tests',
['clean:test', 'setTestEnv', 'ensureConfig']
['clean:test', 'setTestEnv']
);
// ### Unit Tests *(sub task)*
@ -580,8 +561,8 @@ var overrides = require('./core/server/overrides'),
// `grunt test:integration/api/api_tags_spec.js`
//
// Their purpose is to test that both the api and models behave as expected when the database layer is involved.
// These tests are run against sqlite3, mysql and pg on travis and ensure that differences between the databases
// don't cause bugs. At present, pg often fails and is not officially supported.
// These tests are run against sqlite3 and mysql on travis and ensure that differences between the databases
// don't cause bugs.
//
// A coverage report can be generated for these tests using the `grunt test-coverage` task.
grunt.registerTask('test-integration', 'Run integration tests (mocha + db access)',

View file

@ -1,9 +1,7 @@
<a href="https://github.com/TryGhost/Ghost"><img src="https://cloud.githubusercontent.com/assets/120485/6622822/c4c639fe-c8e7-11e4-9e64-5bec06c8b4c3.png" alt="Ghost" /></a>
<a href="https://github.com/TryGhost/Ghost"><img src="https://cloud.githubusercontent.com/assets/120485/18661790/cf942eda-7f17-11e6-9eb6-9c65bfc2abd8.png" alt="Ghost" /></a>
<a href="https://travis-ci.org/TryGhost/Ghost"><img align="right" src="https://travis-ci.org/TryGhost/Ghost.svg?branch=master" alt="Build status" /></a>
![Ghost Screenshot](https://cloud.githubusercontent.com/assets/120485/6626466/6dae46b2-c8ff-11e4-8c7c-8dd63b215f7b.jpg)
![Ghost is a simple, powerful publishing platform that allows you to share your stories with the world.](https://cloud.githubusercontent.com/assets/120485/6626501/b2bb072c-c8ff-11e4-8e1a-2e78e68fd5c3.png)
<a href="https://dev.ghost.org/lts"><img src="https://cloud.githubusercontent.com/assets/120485/18661856/0930282e-7f18-11e6-948a-00546393fd93.png" alt="Warning: Major release in progress. Expect things to be broken in master." /></a>
The project is maintained by a non-profit organisation called the **Ghost Foundation**, along with an amazing group of independent [contributors](https://github.com/TryGhost/Ghost/contributors). We're trying to make publishing software that changes the shape of online journalism.
@ -13,52 +11,45 @@ The project is maintained by a non-profit organisation called the **Ghost Founda
- [Theme Docs](http://themes.ghost.org)
- [Contributing Guide](https://github.com/TryGhost/Ghost/blob/master/.github/CONTRIBUTING.md)
- [Feature Requests](http://ideas.ghost.org/)
- [Dev Blog](http://dev.ghost.org)
- [Developer Blog](http://dev.ghost.org)
**NOTE: If youre stuck, cant get something working or need some help, please head on over and join our [Slack community](https://ghost.org/slack/) rather than opening an issue.**
&nbsp;
# Quick Start Install
Make sure you've installed Node.js - We recommend the latest **Node v4 LTS** release. For other versions [click here](http://support.ghost.org/supported-node-versions/). May contain nuts.
First, youll need **Node.js v4 LTS** or a [supported version](http://support.ghost.org/supported-node-versions/).
1. Download the [latest release](https://ghost.org/developers/) of Ghost
1. Unzip in the location you want to install
1. Fire up a terminal
1. Unzip, and fire up terminal
1. `npm install --production`
1. Start Ghost!
- Local environment: `npm start`
- On a server: `npm start --production`
1. `http://localhost:2368/ghost` :tada:
More [install docs](http://support.ghost.org/installation/) here in case you got stuck.
More [install docs](http://support.ghost.org/installation/) here in case you get stuck.
&nbsp;
<a name="getting-started"></a>
# Developer Install (from git)
# Developer Install
Install Node.js. (See [Supported Node.js versions](http://support.ghost.org/supported-node-versions/))
```bash
# Node v4.2+ LTS - recommended
# Node v0.10.x and v0.12.x - supported
#
# Choose wisely
```
Clone :ghost:
This is for if you want to hack on Ghost core. First, youll need **Node.js v4 LTS** or a [supported version](http://support.ghost.org/supported-node-versions/). Then:
```bash
git clone git://github.com/tryghost/ghost.git
cd ghost
```
Install grunt. No prizes here.
Install grunt
```bash
npm install -g grunt-cli
```
Install Ghost. If you're running locally, use [master](https://github.com/TryGhost/Ghost/tree/master). For production, use [stable](https://github.com/TryGhost/Ghost/tree/stable). :no_entry_sign::rocket::microscope:
Install Ghost
```bash
npm install
@ -70,34 +61,28 @@ Build the things!
grunt init
```
Minify that shit for production?
Start your engines
```bash
grunt prod
grunt dev
```
Start your engines.
Congrats! You made it. BTW you can also just `npm install ghost` if you're into that sort of thing. NPM aficionados can also read up on using [Ghost as an NPM module](https://github.com/TryGhost/Ghost/wiki/Using-Ghost-as-an-npm-module). More general [install docs](http://support.ghost.org/installation/) here in case you got stuck.
```bash
npm start
## running production? Add --production
```
Congrats! You made it. BTW you can also just `npm install ghost` if you're into that sort of thing. NPM aficionados can also read up on using [Ghost as an NPM module](https://github.com/TryGhost/Ghost/wiki/Using-Ghost-as-an-npm-module).
More general [install docs](http://support.ghost.org/installation/) here in case you got stuck.
&nbsp;
# Deploying Ghost
![Ghost(Pro) + DigitalOcean](https://cloud.githubusercontent.com/assets/120485/8180331/d6674e32-1414-11e5-8ce4-2250e9994906.png)
<a href="https://ghost.org/pricing"><img src="https://cloud.githubusercontent.com/assets/120485/18662071/f30da886-7f18-11e6-90f2-42c0ade79fd1.png" alt="Ghost(Pro)" /></a>
Save yourself time and headaches with our fully managed **[Ghost(Pro)](https://ghost.org/pricing/)** service. Deploy a new instance of Ghost in a couple of clicks running on [DigitalOcean](https://digitalocean.com)s rock solid infrastructure, with a worldwide CDN thrown in at no extra charge.
The easiest way to deploy Ghost is with our official **[Ghost(Pro)](https://ghost.org/pricing/)** managed service. You can have a fresh instance up and running in a couple of clicks with a worldwide CDN, backups, security and maintenance all done for you.
All revenue from **Ghost(Pro)** goes to the Ghost Foundation, the non-profit org which funds the maintenance and further development of Ghost.
Not only will it save you [many hours per month](https://ghost.org/ghost-pro-vs-self-hosting/), but all revenue goes to the Ghost Foundation, which funds the maintenance and further development of Ghost itself. So youll be supporting open source software *and* getting a great service **at the same time**! Talk about win/win. :trophy:
[Other options](http://support.ghost.org/deploying-ghost/) are also available if you prefer playing around with servers by yourself.
[Other options](http://support.ghost.org/deploying-ghost/) are also available if you prefer playing around with servers by yourself, of course. The freedom of choice is in your hands.
&nbsp;
# Staying Up to Date
@ -106,9 +91,9 @@ When a new version of Ghost comes out, you'll want to look over these [upgrade i
You can talk to other Ghost users and developers in our [public Slack team](https://ghost.org/slack/) (it's pretty awesome). We have a public meeting every Tuesday at 5:30pm UK time.
New releases are announced on the [dev blog](http://dev.ghost.org/tag/releases/). You can subscribe by email or follow [@TryGhost_Dev](https://twitter.com/tryghost_dev) on Twitter, if you prefer your updates bite-sized and facetious.
New releases are announced on the [dev blog](http://dev.ghost.org/tag/releases/). You can subscribe by email or follow [@TryGhost_Dev](https://twitter.com/tryghost_dev) on Twitter, if you prefer your updates bite-sized and facetious. :saxophone::turtle:
:saxophone::turtle:
&nbsp;
# Copyright & License

View file

@ -1,152 +0,0 @@
// # Ghost Configuration
// Setup your Ghost install for various [environments](http://support.ghost.org/config/#about-environments).
// Ghost runs in `development` mode by default. Full documentation can be found at http://support.ghost.org/config/
var path = require('path'),
config;
config = {
// ### Production
// When running Ghost in the wild, use the production environment.
// Configure your URL and mail settings here
production: {
url: 'http://my-ghost-blog.com',
mail: {},
database: {
client: 'sqlite3',
connection: {
filename: path.join(__dirname, '/content/data/ghost.db')
},
debug: false
},
server: {
host: '127.0.0.1',
port: '2368'
}
},
// ### Development **(default)**
development: {
// The url to use when providing links to the site, E.g. in RSS and email.
// Change this to your Ghost blog's published URL.
url: 'http://localhost:2368',
// Example refferer policy
// Visit https://www.w3.org/TR/referrer-policy/ for instructions
// default 'origin-when-cross-origin',
// referrerPolicy: 'origin-when-cross-origin',
// Example mail config
// Visit http://support.ghost.org/mail for instructions
// ```
// mail: {
// transport: 'SMTP',
// options: {
// service: 'Mailgun',
// auth: {
// user: '', // mailgun username
// pass: '' // mailgun password
// }
// }
// },
// ```
// #### Database
// Ghost supports sqlite3 (default), MySQL & PostgreSQL
database: {
client: 'sqlite3',
connection: {
filename: path.join(__dirname, '/content/data/ghost-dev.db')
},
debug: false
},
// #### Server
// Can be host & port (default), or socket
server: {
// Host to be passed to node's `net.Server#listen()`
host: '127.0.0.1',
// Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
port: '2368'
},
// #### Paths
// Specify where your content directory lives
paths: {
contentPath: path.join(__dirname, '/content/')
}
},
// **Developers only need to edit below here**
// ### Testing
// Used when developing Ghost to run tests and check the health of Ghost
// Uses a different port number
testing: {
url: 'http://127.0.0.1:2369',
database: {
client: 'sqlite3',
connection: {
filename: path.join(__dirname, '/content/data/ghost-test.db')
},
pool: {
afterCreate: function (conn, done) {
conn.run('PRAGMA synchronous=OFF;' +
'PRAGMA journal_mode=MEMORY;' +
'PRAGMA locking_mode=EXCLUSIVE;' +
'BEGIN EXCLUSIVE; COMMIT;', done);
}
},
useNullAsDefault: true
},
server: {
host: '127.0.0.1',
port: '2369'
},
logging: false
},
// ### Testing MySQL
// Used by Travis - Automated testing run through GitHub
'testing-mysql': {
url: 'http://127.0.0.1:2369',
database: {
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'root',
password : '',
database : 'ghost_testing',
charset : 'utf8'
}
},
server: {
host: '127.0.0.1',
port: '2369'
},
logging: false
},
// ### Testing pg
// Used by Travis - Automated testing run through GitHub
'testing-pg': {
url: 'http://127.0.0.1:2369',
database: {
client: 'pg',
connection: {
host : '127.0.0.1',
user : 'postgres',
password : '',
database : 'ghost_testing',
charset : 'utf8'
}
},
server: {
host: '127.0.0.1',
port: '2369'
},
logging: false
}
};
module.exports = config;

@ -1 +1 @@
Subproject commit e432af7b107c53f141f2fed7d3aec1dabbaadd58
Subproject commit c4e548a85888708ab214a43fdc50e25a6d37beb9

View file

@ -168,7 +168,7 @@ authentication = {
}
function sendResetNotification(data) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url,
var baseUrl = config.get('forceAdminSSL') ? (config.get('urlSSL') || config.get('url')) : config.get('url'),
resetUrl = baseUrl.replace(/\/$/, '') +
'/ghost/reset/' +
globalUtils.encodeBase64URLsafe(data.resetToken) + '/';

View file

@ -20,10 +20,10 @@ function fetchAvailableTimezones() {
function getAboutConfig() {
return {
version: config.ghostVersion,
version: config.get('ghostVersion'),
environment: process.env.NODE_ENV,
database: config.database.client,
mail: _.isObject(config.mail) ? config.mail.transport : ''
database: config.get('database').client,
mail: _.isObject(config.get('mail')) ? config.get('mail').transport : ''
};
}
@ -33,9 +33,9 @@ function getBaseConfig() {
useGravatar: {value: !config.isPrivacyDisabled('useGravatar'), type: 'bool'},
publicAPI: labsFlag('publicAPI'),
internalTags: labsFlag('internalTags'),
blogUrl: {value: config.url.replace(/\/$/, ''), type: 'string'},
blogTitle: {value: config.theme.title, type: 'string'},
routeKeywords: {value: JSON.stringify(config.routeKeywords), type: 'json'}
blogUrl: {value: config.get('url').replace(/\/$/, ''), type: 'string'},
blogTitle: {value: config.get('theme').title, type: 'string'},
routeKeywords: {value: JSON.stringify(config.get('routeKeywords')), type: 'json'}
};
}

View file

@ -7,6 +7,7 @@
var _ = require('lodash'),
Promise = require('bluebird'),
config = require('../config'),
utils = require('../utils'),
configuration = require('./configuration'),
db = require('./db'),
mail = require('./mail'),
@ -99,7 +100,7 @@ cacheInvalidationHeader = function cacheInvalidationHeader(req, result) {
if (hasStatusChanged || wasPublishedUpdated) {
return INVALIDATE_ALL;
} else {
return config.urlFor({relativeUrl: '/' + config.routeKeywords.preview + '/' + post.uuid + '/'});
return utils.url.urlFor({relativeUrl: '/' + config.get('routeKeywords').preview + '/' + post.uuid + '/'});
}
}
}
@ -117,7 +118,7 @@ cacheInvalidationHeader = function cacheInvalidationHeader(req, result) {
* @return {String} Resolves to header string
*/
locationHeader = function locationHeader(req, result) {
var apiRoot = config.urlFor('api'),
var apiRoot = utils.url.urlFor('api'),
location,
newObject;

View file

@ -2,11 +2,11 @@ var _ = require('lodash'),
Promise = require('bluebird'),
moment = require('moment'),
config = require('../config'),
pipeline = require(config.paths.corePath + '/server/utils/pipeline'),
dataProvider = require(config.paths.corePath + '/server/models'),
i18n = require(config.paths.corePath + '/server/i18n'),
errors = require(config.paths.corePath + '/server/errors'),
apiPosts = require(config.paths.corePath + '/server/api/posts'),
pipeline = require(config.get('paths').corePath + '/server/utils/pipeline'),
dataProvider = require(config.get('paths').corePath + '/server/models'),
i18n = require(config.get('paths').corePath + '/server/i18n'),
errors = require(config.get('paths').corePath + '/server/errors'),
apiPosts = require(config.get('paths').corePath + '/server/api/posts'),
utils = require('./utils');
/**
@ -21,7 +21,7 @@ exports.publishPost = function publishPost(object, options) {
}
var post, publishedAtMoment,
publishAPostBySchedulerToleranceInMinutes = config.times.publishAPostBySchedulerToleranceInMinutes;
publishAPostBySchedulerToleranceInMinutes = config.get('times').publishAPostBySchedulerToleranceInMinutes;
// CASE: only the scheduler client is allowed to publish (hardcoded because of missing client permission system)
if (!options.context || !options.context.client || options.context.client !== 'ghost-scheduler') {

View file

@ -50,20 +50,20 @@ updateConfigCache = function () {
}
}
config.set({
theme: {
title: (settingsCache.title && settingsCache.title.value) || '',
description: (settingsCache.description && settingsCache.description.value) || '',
logo: (settingsCache.logo && settingsCache.logo.value) || '',
cover: (settingsCache.cover && settingsCache.cover.value) || '',
navigation: (settingsCache.navigation && JSON.parse(settingsCache.navigation.value)) || [],
postsPerPage: (settingsCache.postsPerPage && settingsCache.postsPerPage.value) || 5,
permalinks: (settingsCache.permalinks && settingsCache.permalinks.value) || '/:slug/',
twitter: (settingsCache.twitter && settingsCache.twitter.value) || '',
facebook: (settingsCache.facebook && settingsCache.facebook.value) || '',
timezone: (settingsCache.activeTimezone && settingsCache.activeTimezone.value) || config.theme.timezone
},
labs: labsValue
config.set('theme:title', (settingsCache.title && settingsCache.title.value) || '');
config.set('theme:description', (settingsCache.description && settingsCache.description.value) || '');
config.set('theme:logo', (settingsCache.logo && settingsCache.logo.value) || '');
config.set('theme:cover', (settingsCache.cover && settingsCache.cover.value) || '');
config.set('theme:navigation', (settingsCache.navigation && JSON.parse(settingsCache.navigation.value)) || []);
config.set('theme:postsPerPage', (settingsCache.postsPerPage && settingsCache.postsPerPage.value) || config.get('theme').postsPerPage);
config.set('theme:permalinks', (settingsCache.permalinks && settingsCache.permalinks.value) || config.get('theme').permalinks);
config.set('theme:twitter', (settingsCache.twitter && settingsCache.twitter.value) || '');
config.set('theme:facebook', (settingsCache.facebook && settingsCache.facebook.value) || '');
config.set('theme:timezone', (settingsCache.activeTimezone && settingsCache.activeTimezone.value) || config.get('theme').timezone);
config.set('theme:url', config.get('url') ? config.get('url').replace(/\/$/, '') : '');
_.each(labsValue, function (value, key) {
config.set('labs:' + key, value);
});
};
@ -177,8 +177,8 @@ readSettingsResult = function (settingsModels) {
return memo;
}, {}),
themes = config.paths.availableThemes,
apps = config.paths.availableApps,
themes = config.get('paths').availableThemes,
apps = config.get('paths').availableApps,
res;
if (settings.activeTheme && themes) {

View file

@ -9,7 +9,8 @@ var Promise = require('bluebird'),
events = require('../events'),
storage = require('../storage'),
settings = require('./settings'),
utils = require('./utils'),
apiUtils = require('./utils'),
utils = require('./../utils'),
i18n = require('../i18n'),
themes;
@ -19,6 +20,13 @@ var Promise = require('bluebird'),
* **See:** [API Methods](index.js.html#api%20methods)
*/
themes = {
loadThemes: function () {
return utils.readThemes(config.getContentPath('themes'))
.then(function (result) {
config.set('paths:availableThemes', result);
});
},
upload: function upload(options) {
options = options || {};
@ -37,7 +45,7 @@ themes = {
throw new errors.ValidationError(i18n.t('errors.api.themes.overrideCasper'));
}
return utils.handlePermissions('themes', 'add')(options)
return apiUtils.handlePermissions('themes', 'add')(options)
.then(function () {
return gscan.checkZip(zip, {keepExtractedDir: true});
})
@ -55,12 +63,12 @@ themes = {
);
})
.then(function () {
return storageAdapter.exists(config.paths.themePath + '/' + zip.shortName);
return storageAdapter.exists(config.getContentPath('themes') + '/' + zip.shortName);
})
.then(function (themeExists) {
// delete existing theme
if (themeExists) {
return storageAdapter.delete(zip.shortName, config.paths.themePath);
return storageAdapter.delete(zip.shortName, config.getContentPath('themes'));
}
})
.then(function () {
@ -69,13 +77,13 @@ themes = {
return storageAdapter.save({
name: zip.shortName,
path: theme.path
}, config.paths.themePath);
}, config.getContentPath('themes'));
})
.then(function () {
// force reload of availableThemes
// right now the logic is in the ConfigManager
// if we create a theme collection, we don't have to read them from disk
return config.loadThemes();
return themes.loadThemes();
})
.then(function () {
// the settings endpoint is used to fetch the availableThemes
@ -111,14 +119,14 @@ themes = {
download: function download(options) {
var themeName = options.name,
theme = config.paths.availableThemes[themeName],
theme = config.get('paths').availableThemes[themeName],
storageAdapter = storage.getStorage('themes');
if (!theme) {
return Promise.reject(new errors.BadRequestError(i18n.t('errors.api.themes.invalidRequest')));
}
return utils.handlePermissions('themes', 'read')(options)
return apiUtils.handlePermissions('themes', 'read')(options)
.then(function () {
events.emit('theme.downloaded', themeName);
return storageAdapter.serve({isTheme: true, name: themeName});
@ -134,23 +142,23 @@ themes = {
theme,
storageAdapter = storage.getStorage('themes');
return utils.handlePermissions('themes', 'destroy')(options)
return apiUtils.handlePermissions('themes', 'destroy')(options)
.then(function () {
if (name === 'casper') {
throw new errors.ValidationError(i18n.t('errors.api.themes.destroyCasper'));
}
theme = config.paths.availableThemes[name];
theme = config.get('paths').availableThemes[name];
if (!theme) {
throw new errors.NotFoundError(i18n.t('errors.api.themes.themeDoesNotExist'));
}
events.emit('theme.deleted', name);
return storageAdapter.delete(name, config.paths.themePath);
return storageAdapter.delete(name, config.getContentPath('themes'));
})
.then(function () {
return config.loadThemes();
return themes.loadThemes();
})
.then(function () {
return settings.updateSettingsCache();

View file

@ -41,7 +41,7 @@ sendInviteEmail = function sendInviteEmail(user) {
return dataProvider.User.generateResetToken(user.email, expires, dbHash);
}).then(function (resetToken) {
var baseUrl = config.forceAdminSSL ? (config.urlSSL || config.url) : config.url;
var baseUrl = config.get('forceAdminSSL') ? (config.get('urlSSL') || config.get('url')) : config.get('url');
emailData.resetLink = baseUrl.replace(/\/$/, '') + '/ghost/signup/' + globalUtils.encodeBase64URLsafe(resetToken) + '/';

View file

@ -10,6 +10,6 @@ module.exports = {
},
setupRoutes: function setupRoutes(blogRouter) {
blogRouter.use('*/' + config.routeKeywords.amp + '/', router);
blogRouter.use('*/' + config.get('routeKeywords').amp + '/', router);
}
};

View file

@ -119,7 +119,7 @@ function getAmperizeHTML(html, post) {
}
// make relative URLs abolute
html = makeAbsoluteUrl(html, config.url, post.url).html();
html = makeAbsoluteUrl(html, config.get('url'), post.url).html();
if (!amperizeCache[post.id] || moment(new Date(amperizeCache[post.id].updated_at)).diff(new Date(post.updated_at)) < 0) {
return new Promise(function (resolve) {

View file

@ -40,7 +40,7 @@ describe('AMP Controller', function () {
body: {}
};
defaultPath = path.join(configUtils.config.paths.appRoot, '/core/server/apps/amp/lib/views/amp.hbs');
defaultPath = path.join(configUtils.config.get('paths').appRoot, '/core/server/apps/amp/lib/views/amp.hbs');
configUtils.set({
theme: {

View file

@ -21,13 +21,13 @@ function getInstalledApps() {
return Promise.reject(e);
}
return installed.concat(config.internalApps);
return installed.concat(config.get('internalApps'));
});
}
function saveInstalledApps(installedApps) {
return getInstalledApps().then(function (currentInstalledApps) {
var updatedAppsInstalled = _.difference(_.uniq(installedApps.concat(currentInstalledApps)), config.internalApps);
var updatedAppsInstalled = _.difference(_.uniq(installedApps.concat(currentInstalledApps)), config.get('internalApps'));
return api.settings.edit({settings: [{key: 'installedApps', value: updatedAppsInstalled}]}, {context: {internal: true}});
});
@ -44,7 +44,7 @@ module.exports = {
appsToLoad = JSON.parse(aApps.value) || [];
appsToLoad = appsToLoad.concat(config.internalApps);
appsToLoad = appsToLoad.concat(config.get('internalApps'));
});
} catch (e) {
errors.logError(

View file

@ -11,16 +11,16 @@ var path = require('path'),
loader;
function isInternalApp(name) {
return _.includes(config.internalApps, name);
return _.includes(config.get('internalApps'), name);
}
// Get the full path to an app by name
function getAppAbsolutePath(name) {
if (isInternalApp(name)) {
return path.join(config.paths.internalAppPath, name);
return path.join(config.get('paths').internalAppPath, name);
}
return path.join(config.paths.appPath, name);
return path.join(config.getContentPath('apps'), name);
}
// Get a relative path to the given apps root, defaults

View file

@ -1,4 +1,5 @@
var config = require('../../config'),
utils = require('../../utils'),
errors = require('../../errors'),
i18n = require('../../i18n'),
middleware = require('./lib/middleware'),
@ -6,10 +7,10 @@ var config = require('../../config'),
module.exports = {
activate: function activate() {
if (config.paths.subdir) {
var paths = config.paths.subdir.split('/');
if (utils.url.getSubdir()) {
var paths = utils.url.getSubdir().split('/');
if (paths.pop() === config.routeKeywords.private) {
if (paths.pop() === config.get('routeKeywords').private) {
errors.logErrorAndExit(
new Error(i18n.t('errors.config.urlCannotContainPrivateSubdir.error')),
i18n.t('errors.config.urlCannotContainPrivateSubdir.description'),
@ -25,6 +26,6 @@ module.exports = {
},
setupRoutes: function setupRoutes(blogRouter) {
blogRouter.use('/' + config.routeKeywords.private + '/', router);
blogRouter.use('/' + config.get('routeKeywords').private + '/', router);
}
};

View file

@ -9,7 +9,7 @@ var _ = require('lodash'),
session = require('cookie-session'),
utils = require('../../../utils'),
i18n = require('../../../i18n'),
privateRoute = '/' + config.routeKeywords.private + '/',
privateRoute = '/' + config.get('routeKeywords').private + '/',
protectedSecurity = [],
privateBlogging;
@ -82,7 +82,7 @@ privateBlogging = {
if (isVerified) {
return next();
} else {
url = config.urlFor({relativeUrl: privateRoute});
url = utils.url.urlFor({relativeUrl: privateRoute});
url += req.url === '/' ? '' : '?r=' + encodeURIComponent(req.url);
return res.redirect(url);
}
@ -92,7 +92,7 @@ privateBlogging = {
// This is here so a call to /private/ after a session is verified will redirect to home;
isPrivateSessionAuth: function isPrivateSessionAuth(req, res, next) {
if (!res.isPrivateBlog) {
return res.redirect(config.urlFor('home', true));
return res.redirect(utils.url.urlFor('home', true));
}
var hash = req.session.token || '',
@ -101,7 +101,7 @@ privateBlogging = {
return verifySessionHash(salt, hash).then(function then(isVerified) {
if (isVerified) {
// redirect to home if user is already authenticated
return res.redirect(config.urlFor('home', true));
return res.redirect(utils.url.urlFor('home', true));
} else {
return next();
}
@ -127,7 +127,7 @@ privateBlogging = {
req.session.token = hasher.digest('hex');
req.session.salt = salt;
return res.redirect(config.urlFor({relativeUrl: decodeURIComponent(forward)}));
return res.redirect(utils.url.urlFor({relativeUrl: decodeURIComponent(forward)}));
} else {
res.error = {
message: i18n.t('errors.middleware.privateblogging.wrongPassword')

View file

@ -30,7 +30,7 @@ describe('Private Controller', function () {
params: {}
};
defaultPath = path.join(configUtils.config.paths.appRoot, '/core/server/apps/private-blogging/lib/views/private.hbs');
defaultPath = path.join(configUtils.config.get('paths').appRoot, '/core/server/apps/private-blogging/lib/views/private.hbs');
configUtils.set({
theme: {

View file

@ -91,7 +91,7 @@ AppSandbox.prototype.loadModule = function loadModuleSandboxed(modulePath) {
};
AppSandbox.defaults = {
blacklist: ['knex', 'fs', 'http', 'sqlite3', 'pg', 'mysql', 'ghost']
blacklist: ['knex', 'fs', 'http', 'sqlite3', 'mysql', 'ghost']
};
module.exports = AppSandbox;

View file

@ -10,6 +10,7 @@ var _ = require('lodash'),
labs = require('../../utils/labs'),
template = require('../../helpers/template'),
utils = require('../../helpers/utils'),
globalUtils = require('../../utils'),
params = ['error', 'success', 'email'],
@ -38,7 +39,7 @@ function makeHidden(name, extras) {
function subscribeFormHelper(options) {
var root = options.data.root,
data = _.merge({}, options.hash, _.pick(root, params), {
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/'),
action: path.join('/', globalUtils.url.getSubdir(), config.get('routeKeywords').subscribe, '/'),
script: new hbs.handlebars.SafeString(subscribeScript),
hidden: new hbs.handlebars.SafeString(
makeHidden('confirm') +
@ -70,7 +71,7 @@ module.exports = {
},
setupRoutes: function setupRoutes(blogRouter) {
blogRouter.use('/' + config.routeKeywords.subscribe + '/', function labsEnabledRouter(req, res, next) {
blogRouter.use('/' + config.get('routeKeywords').subscribe + '/', function labsEnabledRouter(req, res, next) {
if (labs.isSet('subscribers') === true) {
return router.apply(this, arguments);
}

View file

@ -0,0 +1,28 @@
{
"url": "http://localhost:2368",
"urlSSL": false,
"forceAdminSSL": false,
"server": {
"host": "127.0.0.1",
"port": 2368
},
"database": {
"client": "sqlite3",
"debug": false,
"connection": {
"filename": "content/data/ghost.db"
}
},
"privacy": false,
"paths": {
"contentPath": "content/"
},
"storage": {
"active": {
"images": "local-file-store"
}
},
"scheduling": {
"active": "SchedulingDefault"
}
}

View file

@ -0,0 +1,12 @@
{
"url": "http://localhost:2368",
"database": {
"connection": {
"filename": "content/data/ghost-dev.db"
},
"debug": false
},
"paths": {
"contentPath": "content/"
}
}

View file

@ -0,0 +1,12 @@
{
"url": "http://my-ghost-blog.com",
"database": {
"connection": {
"filename": "content/data/ghost.db"
},
"debug": false
},
"paths": {
"contentPath": "content/"
}
}

View file

@ -0,0 +1,16 @@
{
"url": "http://127.0.0.1:2369",
"server": {
"port": 2369
},
"database": {
"client": "mysql",
"connection": {
"host" : "127.0.0.1",
"user" : "root",
"password" : "",
"database" : "ghost_testing"
}
},
"logging": false
}

View file

@ -0,0 +1,13 @@
{
"url": "http://127.0.0.1:2369",
"database": {
"connection": {
"filename": "content/data/ghost-test.db"
},
"useNullAsDefault": true
},
"server": {
"port": 2369
},
"logging": false
}

View file

@ -1,518 +1,40 @@
// # Config
// General entry point for all configuration data
var path = require('path'),
Promise = require('bluebird'),
chalk = require('chalk'),
crypto = require('crypto'),
fs = require('fs'),
url = require('url'),
_ = require('lodash'),
var nconf = require('nconf'),
path = require('path'),
localUtils = require('./utils'),
packageInfo = require('../../../package.json'),
env = process.env.NODE_ENV || 'development';
validator = require('validator'),
readDirectory = require('../utils/read-directory'),
readThemes = require('../utils/read-themes'),
errors = require('../errors'),
configUrl = require('./url'),
packageInfo = require('../../../package.json'),
i18n = require('../i18n'),
appRoot = path.resolve(__dirname, '../../../'),
corePath = path.resolve(appRoot, 'core/'),
testingEnvs = ['testing', 'testing-mysql', 'testing-pg'],
defaultConfig = {};
function ConfigManager(config) {
/**
* Our internal true representation of our current config object.
* @private
* @type {Object}
*/
this._config = {};
// Allow other modules to be externally accessible.
this.urlJoin = configUrl.urlJoin;
this.urlFor = configUrl.urlFor;
this.urlPathForPost = configUrl.urlPathForPost;
this.apiUrl = configUrl.apiUrl;
this.getBaseUrl = configUrl.getBaseUrl;
// If we're given an initial config object then we can set it.
if (config && _.isObject(config)) {
this.set(config);
}
}
// Are we using sockets? Custom socket or the default?
ConfigManager.prototype.getSocket = function () {
var socketConfig,
values = {
path: path.join(this._config.paths.contentPath, process.env.NODE_ENV + '.socket'),
permissions: '660'
};
if (this._config.server.hasOwnProperty('socket')) {
socketConfig = this._config.server.socket;
if (_.isString(socketConfig)) {
values.path = socketConfig;
return values;
}
if (_.isObject(socketConfig)) {
values.path = socketConfig.path || values.path;
values.permissions = socketConfig.permissions || values.permissions;
return values;
}
}
return false;
};
ConfigManager.prototype.init = function (rawConfig) {
var self = this;
// Cache the config.js object's environment
// object so we can later refer to it.
// Note: this is not the entirety of config.js,
// just the object appropriate for this NODE_ENV
self.set(rawConfig);
return self.loadThemes()
.then(function () {
return self.loadApps();
})
.then(function () {
return self._config;
});
};
ConfigManager.prototype.loadThemes = function () {
var self = this;
return readThemes(self._config.paths.themePath)
.then(function (result) {
self._config.paths.availableThemes = result;
});
};
ConfigManager.prototype.loadApps = function () {
var self = this;
return readDirectory(self._config.paths.appPath)
.then(function (result) {
self._config.paths.availableApps = result;
});
};
nconf.set('NODE_ENV', env);
/**
* Allows you to set the config object.
* @param {Object} config Only accepts an object at the moment.
* command line arguments
*/
ConfigManager.prototype.set = function (config) {
var localPath = '',
defaultStorageAdapter = 'local-file-store',
defaultSchedulingAdapter = 'SchedulingDefault',
activeStorageAdapter,
activeSchedulingAdapter,
contentPath,
schedulingPath,
subdir,
assetHash;
// Merge passed in config object onto our existing config object.
// We're using merge here as it doesn't assign `undefined` properties
// onto our cached config object. This allows us to only update our
// local copy with properties that have been explicitly set.
_.merge(this._config, config);
// Special case for the database config, which should be overridden not merged
if (config && config.database) {
this._config.database = config.database;
}
// Special case for the them.navigation JSON object, which should be overridden not merged
if (config && config.theme && config.theme.navigation) {
this._config.theme.navigation = config.theme.navigation;
}
// Special case for theme.timezone, which should be overridden not merged
if (config && config.theme && config.theme.timezone) {
this._config.theme.timezone = config.theme.timezone;
} else {
// until we have set the timezone from settings, we use the default
this._config.theme = this._config.theme ? this._config.theme : {};
this._config.theme.timezone = 'Etc/UTC';
}
// Protect against accessing a non-existant object.
// This ensures there's always at least a paths object
// because it's referenced in multiple places.
this._config.paths = this._config.paths || {};
// Parse local path location
if (this._config.url) {
localPath = url.parse(this._config.url).path;
// Remove trailing slash
if (localPath !== '/') {
localPath = localPath.replace(/\/$/, '');
}
}
subdir = localPath === '/' ? '' : localPath;
if (!_.isEmpty(subdir)) {
this._config.slugs.protected.push(subdir.split('/').pop());
}
// Allow contentPath to be over-written by passed in config object
// Otherwise default to default content path location
contentPath = this._config.paths.contentPath || path.resolve(appRoot, 'content');
assetHash = this._config.assetHash ||
(crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10);
// read storage adapter from config file or attach default adapter
this._config.storage = this._config.storage || {};
activeStorageAdapter = this._config.storage.active || defaultStorageAdapter;
// read scheduling adapter(s) from config file or attach default adapter
this._config.scheduling = this._config.scheduling || {};
activeSchedulingAdapter = this._config.scheduling.active || defaultSchedulingAdapter;
// storage.active can be an object like {images: 'my-custom-image-storage-adapter', themes: 'local-file-storage'}
// we ensure that passing a string to storage.active still works, but internal it's always an object
if (_.isString(activeStorageAdapter)) {
this._config.storage = _.merge(this._config.storage, {
active: {
images: activeStorageAdapter,
themes: defaultStorageAdapter
}
});
} else {
// ensure there is a default image storage adapter
if (!this._config.storage.active.images) {
this._config.storage.active.images = defaultStorageAdapter;
}
// ensure there is a default theme storage adapter
// @TODO: right now we only support theme uploads to local file storage
// @TODO: we need to change reading themes from disk on bootstrap (see loadThemes)
this._config.storage.active.themes = defaultStorageAdapter;
}
if (activeSchedulingAdapter === defaultSchedulingAdapter) {
schedulingPath = path.join(corePath, '/server/scheduling/');
} else {
schedulingPath = path.join(contentPath, '/scheduling/');
}
_.merge(this._config, {
ghostVersion: packageInfo.version,
paths: {
appRoot: appRoot,
subdir: subdir,
config: this._config.paths.config || path.join(appRoot, 'config.js'),
configExample: path.join(appRoot, 'config.example.js'),
corePath: corePath,
storagePath: {
default: path.join(corePath, '/server/storage/'),
custom: path.join(contentPath, 'storage/')
},
contentPath: contentPath,
themePath: path.resolve(contentPath, 'themes'),
appPath: path.resolve(contentPath, 'apps'),
imagesPath: path.resolve(contentPath, 'images'),
internalAppPath: path.join(corePath, '/server/apps/'),
imagesRelPath: 'content/images',
adminViews: path.join(corePath, '/server/views/'),
helperTemplates: path.join(corePath, '/server/helpers/tpl/'),
availableThemes: this._config.paths.availableThemes || {},
availableApps: this._config.paths.availableApps || {},
clientAssets: path.join(corePath, '/built/assets/')
},
maintenance: {},
scheduling: {
active: activeSchedulingAdapter,
path: schedulingPath
},
theme: {
// normalise the URL by removing any trailing slash
url: this._config.url ? this._config.url.replace(/\/$/, '') : ''
},
routeKeywords: {
tag: 'tag',
author: 'author',
page: 'page',
preview: 'p',
private: 'private',
subscribe: 'subscribe',
amp: 'amp'
},
internalApps: ['private-blogging', 'subscribers', 'amp'],
slugs: {
// Used by generateSlug to generate slugs for posts, tags, users, ..
// reserved slugs are reserved but can be extended/removed by apps
// protected slugs cannot be changed or removed
reserved: ['admin', 'app', 'apps', 'archive', 'archives', 'categories',
'category', 'dashboard', 'feed', 'ghost-admin', 'login', 'logout',
'page', 'pages', 'post', 'posts', 'public', 'register', 'setup',
'signin', 'signout', 'signup', 'user', 'users', 'wp-admin', 'wp-login'],
protected: ['ghost', 'rss', 'amp']
},
// used in middleware/validation/upload.js
// if we finish the data/importer logic, each type selects an importer
uploads: {
subscribers: {
extensions: ['.csv'],
contentTypes: ['text/csv', 'application/csv', 'application/octet-stream']
},
images: {
extensions: ['.jpg', '.jpeg', '.gif', '.png', '.svg', '.svgz'],
contentTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
},
db: {
extensions: ['.json', '.zip'],
contentTypes: ['application/octet-stream', 'application/json', 'application/zip', 'application/x-zip-compressed']
},
themes: {
extensions: ['.zip'],
contentTypes: ['application/zip', 'application/x-zip-compressed', 'application/octet-stream']
}
},
deprecatedItems: ['updateCheck', 'mail.fromaddress'],
// create a hash for cache busting assets
assetHash: assetHash,
preloadHeaders: this._config.preloadHeaders || false,
times: {
cannotScheduleAPostBeforeInMinutes: 2,
publishAPostBySchedulerToleranceInMinutes: 2
}
});
// Also pass config object to
// configUrl object to maintain
// clean dependency tree
configUrl.setConfig(this._config);
// For now we're going to copy the current state of this._config
// so it's directly accessible on the instance.
// @TODO: perhaps not do this? Put access of the config object behind
// a function?
_.extend(this, this._config);
};
nconf.argv();
/**
* Allows you to read the config object.
* @return {Object} The config object.
* env arguments
*/
ConfigManager.prototype.get = function () {
return this._config;
};
ConfigManager.prototype.load = function (configFilePath) {
var self = this;
self._config.paths.config = process.env.GHOST_CONFIG || configFilePath || self._config.paths.config;
/* Check for config file and copy from config.example.js
if one doesn't exist. After that, start the server. */
return new Promise(function (resolve, reject) {
fs.stat(self._config.paths.config, function (err) {
var exists = (err) ? false : true,
pendingConfig;
if (!exists) {
pendingConfig = self.writeFile();
}
Promise.resolve(pendingConfig).then(function () {
return self.validate();
}).then(function (rawConfig) {
return self.init(rawConfig);
}).then(resolve)
.catch(reject);
});
});
};
/* Check for config file and copy from config.example.js
if one doesn't exist. After that, start the server. */
ConfigManager.prototype.writeFile = function () {
var configPath = this._config.paths.config,
configExamplePath = this._config.paths.configExample;
return new Promise(function (resolve, reject) {
fs.stat(configExamplePath, function checkTemplate(err) {
var templateExists = (err) ? false : true,
read,
write,
error;
if (!templateExists) {
error = new Error(i18n.t('errors.config.couldNotLocateConfigFile.error'));
error.context = appRoot;
error.help = i18n.t('errors.config.couldNotLocateConfigFile.help');
return reject(error);
}
// Copy config.example.js => config.js
read = fs.createReadStream(configExamplePath);
read.on('error', function (err) {
errors.logError(
new Error(i18n.t('errors.config.couldNotOpenForReading.error', {file: 'config.example.js'})),
appRoot,
i18n.t('errors.config.couldNotOpenForReading.help'));
reject(err);
});
write = fs.createWriteStream(configPath);
write.on('error', function (err) {
errors.logError(
new Error(i18n.t('errors.config.couldNotOpenForWriting.error', {file: 'config.js'})),
appRoot,
i18n.t('errors.config.couldNotOpenForWriting.help'));
reject(err);
});
write.on('finish', resolve);
read.pipe(write);
});
});
};
nconf.env();
/**
* Read config.js file from file system using node's require
* @param {String} envVal Which environment we're in.
* @return {Object} The config object.
* load config files
*/
ConfigManager.prototype.readFile = function (envVal) {
return require(this._config.paths.config)[envVal];
};
nconf.file('1', __dirname + '/overrides.json');
nconf.file('2', path.join(process.cwd(), 'config.' + env + '.json'));
nconf.file('3', __dirname + '/env/config.' + env + '.json');
nconf.file('4', __dirname + '/defaults.json');
/**
* Validates the config object has everything we want and in the form we want.
* @return {Promise.<Object>} Returns a promise that resolves to the config object.
* transform all relative paths to absolute paths
*/
ConfigManager.prototype.validate = function () {
var envVal = process.env.NODE_ENV || undefined,
hasHostAndPort,
hasSocket,
config,
parsedUrl;
try {
config = this.readFile(envVal);
}
catch (e) {
return Promise.reject(e);
}
// Check that our url is valid
if (!validator.isURL(config.url, {protocols: ['http', 'https'], require_protocol: true})) {
errors.logError(
new Error(i18n.t('errors.config.invalidUrlInConfig.description'),
config.url,
i18n.t('errors.config.invalidUrlInConfig.help')));
return Promise.reject(new Error(i18n.t('errors.config.invalidUrlInConfig.error')));
}
parsedUrl = url.parse(config.url || 'invalid', false, true);
if (/\/ghost(\/|$)/.test(parsedUrl.pathname)) {
errors.logError(
new Error(i18n.t('errors.config.urlCannotContainGhostSubdir.description'),
config.url,
i18n.t('errors.config.urlCannotContainGhostSubdir.help')));
return Promise.reject(new Error(i18n.t('errors.config.urlCannotContainGhostSubdir.error')));
}
// Check that we have database values
if (!config.database || !config.database.client) {
errors.logError(
new Error(i18n.t('errors.config.dbConfigInvalid.description')),
JSON.stringify(config.database),
i18n.t('errors.config.dbConfigInvalid.help'));
return Promise.reject(new Error(i18n.t('errors.config.dbConfigInvalid.error')));
}
hasHostAndPort = config.server && !!config.server.host && !!config.server.port;
hasSocket = config.server && !!config.server.socket;
// Check for valid server host and port values
if (!config.server || !(hasHostAndPort || hasSocket)) {
errors.logError(
new Error(i18n.t('errors.config.invalidServerValues.description')),
JSON.stringify(config.server),
i18n.t('errors.config.invalidServerValues.help'));
return Promise.reject(new Error(i18n.t('errors.config.invalidServerValues.error')));
}
return Promise.resolve(config);
};
localUtils.makePathsAbsolute.bind(nconf)();
/**
* Helper method for checking the state of a particular privacy flag
* @param {String} privacyFlag The flag to check
* @returns {boolean}
* values we have to set manual
* @TODO: ghost-cli?
*/
ConfigManager.prototype.isPrivacyDisabled = function (privacyFlag) {
if (!this.privacy) {
return false;
}
nconf.set('ghostVersion', packageInfo.version);
if (this.privacy.useTinfoil === true) {
return true;
}
return this.privacy[privacyFlag] === false;
};
/**
* Check if any of the currently set config items are deprecated, and issues a warning.
*/
ConfigManager.prototype.checkDeprecated = function () {
var self = this;
_.each(this.deprecatedItems, function (property) {
self.displayDeprecated(self._config, property.split('.'), []);
});
};
ConfigManager.prototype.displayDeprecated = function (item, properties, address) {
var self = this,
property = properties.shift(),
errorText,
explanationText,
helpText;
address.push(property);
if (item.hasOwnProperty(property)) {
if (properties.length) {
return self.displayDeprecated(item[property], properties, address);
}
errorText = i18n.t('errors.config.deprecatedProperty.error', {property: chalk.bold(address.join('.'))});
explanationText = i18n.t('errors.config.deprecatedProperty.explanation');
helpText = i18n.t('errors.config.deprecatedProperty.help', {url: 'http://support.ghost.org/config'});
errors.logWarn(errorText, explanationText, helpText);
}
};
if (testingEnvs.indexOf(process.env.NODE_ENV) > -1) {
defaultConfig = require('../../../config.example')[process.env.NODE_ENV];
}
module.exports = new ConfigManager(defaultConfig);
module.exports = nconf;
module.exports.isPrivacyDisabled = localUtils.isPrivacyDisabled.bind(nconf);
module.exports.getContentPath = localUtils.getContentPath.bind(nconf);

View file

@ -0,0 +1,67 @@
{
"paths": {
"appRoot": ".",
"corePath": "core/",
"clientAssets": "core/built/assets",
"imagesRelPath": "content/images",
"helperTemplates": "core/server/helpers/tpl/",
"adminViews": "core/server/views/",
"internalAppPath": "core/server/apps/",
"internalStoragePath": "core/server/storage/",
"internalSchedulingPath": "core/server/scheduling/"
},
"internalApps": [
"private-blogging",
"subscribers",
"amp"
],
"routeKeywords": {
"tag": "tag",
"author": "author",
"page": "page",
"preview": "p",
"private": "private",
"subscribe": "subscribe",
"amp": "amp"
},
"slugs": {
"reserved": ["admin", "app", "apps", "archive", "archives", "categories",
"category", "dashboard", "feed", "ghost-admin", "login", "logout",
"page", "pages", "post", "posts", "public", "register", "setup",
"signin", "signout", "signup", "user", "users", "wp-admin", "wp-login"],
"protected": ["ghost", "rss", "amp"]
},
"uploads": {
"subscribers": {
"extensions": [".csv"],
"contentTypes": ["text/csv", "application/csv", "application/octet-stream"]
},
"images": {
"extensions": [".jpg", ".jpeg", ".gif", ".png", ".svg", ".svgz"],
"contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml"]
},
"db": {
"extensions": [".json", ".zip"],
"contentTypes": ["application/octet-stream", "application/json", "application/zip", "application/x-zip-compressed"]
},
"themes": {
"extensions": [".zip"],
"contentTypes": ["application/zip", "application/x-zip-compressed", "application/octet-stream"]
}
},
"storage": {
"active": {
"themes": "local-file-store"
}
},
"times": {
"cannotScheduleAPostBeforeInMinutes": 2,
"publishAPostBySchedulerToleranceInMinutes": 2
},
"theme": {
"timezone": "Etc/UTC"
},
"maintenance": {
"enabled": false
}
}

View file

@ -0,0 +1,57 @@
var path = require('path'),
_ = require('lodash');
exports.isPrivacyDisabled = function isPrivacyDisabled(privacyFlag) {
if (!this.get('privacy')) {
return false;
}
if (this.get('privacy').useTinfoil === true) {
return true;
}
return this.get('privacy')[privacyFlag] === false;
};
/**
* transform all relative paths to absolute paths
* @TODO: imagesRelPath is a dirty little attribute (especially when looking at the usages)
*/
exports.makePathsAbsolute = function makePathsAbsolute(paths, parent) {
var self = this;
if (!paths && !parent) {
paths = this.get('paths');
parent = 'paths';
}
_.each(paths, function (configValue, pathsKey) {
if (_.isObject(configValue)) {
makePathsAbsolute.bind(self)(configValue, parent + ':' + pathsKey);
} else {
if (configValue[0] !== '/' && pathsKey !== 'imagesRelPath') {
self.set(parent + ':' + pathsKey, path.join(__dirname + '/../../../', configValue));
}
}
});
};
/**
* we can later support setting folder names via custom config values
*/
exports.getContentPath = function getContentPath(type) {
switch (type) {
case 'storage':
return path.join(this.get('paths:contentPath'), 'storage/');
case 'images':
return path.join(this.get('paths:contentPath'), 'images/');
case 'apps':
return path.join(this.get('paths:contentPath'), 'apps/');
case 'themes':
return path.join(this.get('paths:contentPath'), 'themes/');
case 'scheduling':
return path.join(this.get('paths:contentPath'), 'scheduling/');
default:
throw new Error('getContentPath was called with: ' + type);
}
};

View file

@ -11,7 +11,7 @@ channelConfig = function channelConfig() {
},
tag: {
name: 'tag',
route: '/' + config.routeKeywords.tag + '/:slug/',
route: '/' + config.get('routeKeywords').tag + '/:slug/',
postOptions: {
filter: 'tags:\'%s\''
},
@ -27,7 +27,7 @@ channelConfig = function channelConfig() {
},
author: {
name: 'author',
route: '/' + config.routeKeywords.author + '/:slug/',
route: '/' + config.get('routeKeywords').author + '/:slug/',
postOptions: {
filter: 'author:\'%s\''
},

View file

@ -12,7 +12,7 @@ var express = require('express'),
channelRouter;
function handlePageParam(req, res, next, page) {
var pageRegex = new RegExp('/' + config.routeKeywords.page + '/(.*)?/'),
var pageRegex = new RegExp('/' + config.get('routeKeywords').page + '/(.*)?/'),
rssRegex = new RegExp('/rss/(.*)?/');
page = parseInt(page, 10);
@ -48,10 +48,10 @@ rssRouter = function rssRouter(channelConfig) {
router.get(baseRoute, stack);
router.get(baseRoute + ':page/', stack);
router.get('/feed/', function redirectToRSS(req, res) {
return utils.redirect301(res, config.paths.subdir + req.baseUrl + baseRoute);
return utils.redirect301(res, utils.url.getSubdir() + req.baseUrl + baseRoute);
});
router.param('page', handlePageParam);
router.param('page', handlePageParam);
return router;
};
@ -65,7 +65,7 @@ channelRouter = function router() {
var channelsRouter = express.Router({mergeParams: true}),
baseRoute = '/',
pageRoute = '/' + config.routeKeywords.page + '/:page/';
pageRoute = '/' + config.get('routeKeywords').page + '/:page/';
_.each(channelConfig.list(), function (channel) {
var channelRouter = express.Router({mergeParams: true}),
@ -79,7 +79,7 @@ channelRouter = function router() {
if (channel.editRedirect) {
channelRouter.get('/edit/', function redirect(req, res) {
res.redirect(config.paths.subdir + channel.editRedirect.replace(':slug', req.params.slug));
res.redirect(utils.url.getSubdir() + channel.editRedirect.replace(':slug', req.params.slug));
});
}

View file

@ -14,9 +14,9 @@
var config = require('../../config'),
// Context patterns, should eventually come from Channel configuration
privatePattern = new RegExp('^\\/' + config.routeKeywords.private + '\\/'),
subscribePattern = new RegExp('^\\/' + config.routeKeywords.subscribe + '\\/'),
ampPattern = new RegExp('\\/' + config.routeKeywords.amp + '\\/$'),
privatePattern = new RegExp('^\\/' + config.get('routeKeywords').private + '\\/'),
subscribePattern = new RegExp('^\\/' + config.get('routeKeywords').subscribe + '\\/'),
ampPattern = new RegExp('\\/' + config.get('routeKeywords').amp + '\\/$'),
rssPattern = new RegExp('^\\/rss\\/'),
homePattern = new RegExp('^\\/$');

View file

@ -33,7 +33,7 @@ _.extend(defaultPostQuery, queryDefaults, {
function fetchPostsPerPage(options) {
options = options || {};
var postsPerPage = parseInt(config.theme.postsPerPage);
var postsPerPage = parseInt(config.get('theme').postsPerPage);
// No negative posts per page, must be number
if (!isNaN(postsPerPage) && postsPerPage > 0) {

View file

@ -5,7 +5,7 @@
/*global require, module */
var api = require('../../api'),
config = require('../../config'),
utils = require('../../utils'),
filters = require('../../filters'),
templates = require('./templates'),
handleError = require('./error'),
@ -48,7 +48,7 @@ frontendControllers = {
}
if (post.status === 'published') {
return res.redirect(301, config.urlFor('post', {post: post}));
return res.redirect(301, utils.url.urlFor('post', {post: post}));
}
setRequestIsSecure(req, post);
@ -68,7 +68,8 @@ frontendControllers = {
// CASE: last param is of url is /edit, redirect to admin
if (lookup.isEditURL) {
return res.redirect(config.paths.subdir + '/ghost/editor/' + post.id + '/');
return res.redirect(utils.url.getSubdir()
+ '/ghost/editor/' + post.id + '/');
}
// CASE: permalink is not valid anymore, we redirect him permanently to the correct one

View file

@ -13,7 +13,7 @@ function getOptionsFormat(linkStructure) {
function postLookup(postUrl) {
var postPath = url.parse(postUrl).path,
postPermalink = config.theme.permalinks,
postPermalink = config.get('theme').permalinks,
pagePermalink = '/:slug/',
isEditURL = false,
isAmpURL = false,

View file

@ -6,7 +6,7 @@ var _ = require('lodash'),
config = require('../../config');
function getActiveThemePaths(activeTheme) {
return config.paths.availableThemes[activeTheme];
return config.get('paths').availableThemes[activeTheme];
}
/**

View file

@ -7,37 +7,7 @@ var knex = require('knex'),
// - then this file is cached and you have no chance to connect to the db anymore
// - bring dynamic into this file (db.connect())
function configure(dbConfig) {
var client = dbConfig.client,
pg;
dbConfig.isPostgreSQL = function () {
return client === 'pg' || client === 'postgres' || client === 'postgresql';
};
if (dbConfig.isPostgreSQL()) {
try {
pg = require('pg');
} catch (e) {
pg = require('pg.js');
}
// By default PostgreSQL returns data as strings along with an OID that identifies
// its type. We're setting the parser to convert OID 20 (int8) into a javascript
// integer.
pg.types.setTypeParser(20, function (val) {
return val === null ? null : parseInt(val, 10);
});
// https://github.com/tgriesser/knex/issues/97
// this sets the timezone to UTC only for the connection!
dbConfig.pool = {
afterCreate: function (connection, callback) {
connection.query('set timezone=\'UTC\'', function (err) {
callback(err, connection);
});
}
};
}
var client = dbConfig.client;
if (client === 'sqlite3') {
dbConfig.useNullAsDefault = dbConfig.useNullAsDefault || false;
@ -45,13 +15,14 @@ function configure(dbConfig) {
if (client === 'mysql') {
dbConfig.connection.timezone = 'UTC';
dbConfig.connection.charset = 'utf8mb4';
}
return dbConfig;
}
if (!knexInstance && config.database && config.database.client) {
knexInstance = knex(configure(config.database));
if (!knexInstance && config.get('database') && config.get('database').client) {
knexInstance = knex(configure(config.get('database')));
}
module.exports = knexInstance;

View file

@ -2,20 +2,21 @@ var _ = require('lodash'),
Promise = require('bluebird'),
path = require('path'),
config = require('../../../config'),
utils = require('../../../utils'),
storage = require('../../../storage'),
ImageHandler;
ImageHandler = {
type: 'images',
extensions: config.uploads.images.extensions,
contentTypes: config.uploads.images.contentTypes,
extensions: config.get('uploads').images.extensions,
contentTypes: config.get('uploads').images.contentTypes,
directories: ['images', 'content'],
loadFile: function (files, baseDir) {
var store = storage.getStorage(),
baseDirRegex = baseDir ? new RegExp('^' + baseDir + '/') : new RegExp(''),
imageFolderRegexes = _.map(config.paths.imagesRelPath.split('/'), function (dir) {
imageFolderRegexes = _.map(config.get('paths').imagesRelPath.split('/'), function (dir) {
return new RegExp('^' + dir + '/');
});
@ -30,15 +31,16 @@ ImageHandler = {
file.originalPath = noBaseDir;
file.name = noGhostDirs;
file.targetDir = path.join(config.paths.imagesPath, path.dirname(noGhostDirs));
file.targetDir = path.join(config.getContentPath('images'), path.dirname(noGhostDirs));
return file;
});
return Promise.map(files, function (image) {
return store.getUniqueFileName(store, image, image.targetDir).then(function (targetFilename) {
image.newPath = (config.paths.subdir + '/' +
config.paths.imagesRelPath + '/' + path.relative(config.paths.imagesPath, targetFilename))
image.newPath = (utils.url.getSubdir() + '/' +
config.get('paths').imagesRelPath + '/' + path.relative(config.getContentPath('images'), targetFilename))
.replace(new RegExp('\\' + path.sep, 'g'), '/');
return image;
});
});

View file

@ -1,4 +1,4 @@
var config = require('../../config'),
var utils = require('../../utils'),
getUrl = require('./url'),
_ = require('lodash');
@ -6,7 +6,7 @@ function getAmplUrl(data) {
var context = data.context ? data.context : null;
if (_.includes(context, 'post') && !_.includes(context, 'amp')) {
return config.urlJoin(config.getBaseUrl(false),
return utils.url.urlJoin(utils.url.getBaseUrl(false),
getUrl(data, false)) + 'amp/';
}
return null;

View file

@ -1,9 +1,11 @@
var config = require('../../config');
var config = require('../../config'),
utils = require('../../utils'),
crypto = require('crypto');
function getAssetUrl(path, isAdmin, minify) {
var output = '';
output += config.paths.subdir + '/';
output += utils.url.getSubdir() + '/';
if (!path.match(/^favicon\.ico$/) && !path.match(/^shared/) && !path.match(/^asset/)) {
if (isAdmin) {
@ -24,7 +26,11 @@ function getAssetUrl(path, isAdmin, minify) {
output += path;
if (!path.match(/^favicon\.ico$/)) {
output = output + '?v=' + config.assetHash;
if (!config.get('assetHash')) {
config.set('assetHash', (crypto.createHash('md5').update(config.get('ghostVersion') + Date.now()).digest('hex')).substring(0, 10));
}
output = output + '?v=' + config.get('assetHash');
}
return output;

View file

@ -1,4 +1,4 @@
var config = require('../../config'),
var utils = require('../../utils'),
getContextObject = require('./context_object.js'),
_ = require('lodash');
@ -7,7 +7,7 @@ function getAuthorImage(data, absolute) {
contextObject = getContextObject(data, context);
if ((_.includes(context, 'post') || _.includes(context, 'page')) && contextObject.author && contextObject.author.image) {
return config.urlFor('image', {image: contextObject.author.image}, absolute);
return utils.url.urlFor('image', {image: contextObject.author.image}, absolute);
}
return null;
}

View file

@ -1,4 +1,4 @@
var config = require('../../config');
var utils = require('../../utils');
function getAuthorUrl(data, absolute) {
var context = data.context ? data.context[0] : null;
@ -6,10 +6,10 @@ function getAuthorUrl(data, absolute) {
context = context === 'amp' ? 'post' : context;
if (data.author) {
return config.urlFor('author', {author: data.author}, absolute);
return utils.url.urlFor('author', {author: data.author}, absolute);
}
if (data[context] && data[context].author) {
return config.urlFor('author', {author: data[context].author}, absolute);
return utils.url.urlFor('author', {author: data[context].author}, absolute);
}
return null;
}

View file

@ -1,8 +1,8 @@
var config = require('../../config'),
var utils = require('../../utils'),
getUrl = require('./url');
function getCanonicalUrl(data) {
var url = config.urlJoin(config.getBaseUrl(false),
var url = utils.url.urlJoin(utils.url.getBaseUrl(false),
getUrl(data, false));
if (url.indexOf('/amp/')) {

View file

@ -2,7 +2,7 @@ var config = require('../../config'),
_ = require('lodash');
function getContextObject(data, context) {
var blog = config.theme,
var blog = config.get('theme'),
contextObject;
context = _.includes(context, 'page') || _.includes(context, 'amp') ? 'post' : context;

View file

@ -1,4 +1,4 @@
var config = require('../../config'),
var utils = require('../../utils'),
getContextObject = require('./context_object.js'),
_ = require('lodash');
@ -8,11 +8,11 @@ function getCoverImage(data) {
if (_.includes(context, 'home') || _.includes(context, 'author')) {
if (contextObject.cover) {
return config.urlFor('image', {image: contextObject.cover}, true);
return utils.url.urlFor('image', {image: contextObject.cover}, true);
}
} else {
if (contextObject.image) {
return config.urlFor('image', {image: contextObject.image}, true);
return utils.url.urlFor('image', {image: contextObject.image}, true);
}
}
return null;

View file

@ -10,7 +10,7 @@ function getDescription(data, root) {
} else if (_.includes(context, 'paged')) {
description = '';
} else if (_.includes(context, 'home')) {
description = config.theme.description;
description = config.get('theme').description;
} else if (_.includes(context, 'author') && data.author) {
description = data.author.meta_description || data.author.bio;
} else if (_.includes(context, 'tag') && data.tag) {

View file

@ -1,6 +1,7 @@
var _ = require('lodash'),
Promise = require('bluebird'),
config = require('../../config'),
utils = require('../../utils'),
getUrl = require('./url'),
getImageDimensions = require('./image-dimensions'),
getCanonicalUrl = require('./canonical_url'),
@ -45,12 +46,12 @@ function getMetaData(data, root) {
publishedDate: getPublishedDate(data),
modifiedDate: getModifiedDate(data),
ogType: getOgType(data),
blog: _.cloneDeep(config.theme)
blog: _.cloneDeep(config.get('theme'))
};
metaData.blog.logo = {};
metaData.blog.logo.url = config.theme.logo ?
config.urlFor('image', {image: config.theme.logo}, true) : config.urlFor({relativeUrl: '/ghost/img/ghosticon.jpg'}, {}, true);
metaData.blog.logo.url = config.get('theme').logo ?
utils.url.urlFor('image', {image: config.get('theme').logo}, true) : utils.url.urlFor({relativeUrl: '/ghost/img/ghosticon.jpg'}, {}, true);
// TODO: cleanup these if statements
if (data.post && data.post.html) {

View file

@ -1,5 +1,6 @@
var _ = require('lodash'),
config = require('../../config');
config = require('../../config'),
utils = require('../../utils');
function getPaginatedUrl(page, data, absolute) {
// If we don't have enough information, return null right away
@ -7,9 +8,9 @@ function getPaginatedUrl(page, data, absolute) {
return null;
}
var pagePath = '/' + config.routeKeywords.page + '/',
var pagePath = '/' + config.get('routeKeywords').page + '/',
// Try to match the base url, as whatever precedes the pagePath
baseUrlPattern = new RegExp('(.+)?(/' + config.routeKeywords.page + '/\\d+/)'),
baseUrlPattern = new RegExp('(.+)?(/' + config.get('routeKeywords').page + '/\\d+/)'),
baseUrlMatch = data.relativeUrl.match(baseUrlPattern),
// If there is no match for pagePath, use the original url, without the trailing slash
baseUrl = baseUrlMatch ? baseUrlMatch[1] : data.relativeUrl.slice(0, -1),
@ -29,7 +30,7 @@ function getPaginatedUrl(page, data, absolute) {
// baseUrl can be undefined, if there was nothing preceding the pagePath (e.g. first page of the index channel)
newRelativeUrl = baseUrl ? baseUrl + newRelativeUrl : newRelativeUrl;
return config.urlFor({relativeUrl: newRelativeUrl, secure: data.secure}, absolute);
return utils.url.urlFor({relativeUrl: newRelativeUrl, secure: data.secure}, absolute);
}
module.exports = getPaginatedUrl;

View file

@ -1,7 +1,7 @@
var config = require('../../config');
var utils = require('../../utils');
function getRssUrl(data, absolute) {
return config.urlFor('rss', {secure: data.secure}, absolute);
return utils.url.urlFor('rss', {secure: data.secure}, absolute);
}
module.exports = getRssUrl;

View file

@ -4,7 +4,7 @@ var _ = require('lodash'),
function getTitle(data, root) {
var title = '',
context = root ? root.context : null,
blog = config.theme,
blog = config.get('theme'),
pagination = root ? root.pagination : null,
pageString = '';

View file

@ -1,5 +1,5 @@
var schema = require('../schema').checks,
config = require('../../config');
utils = require('../../utils');
// This cleans the url from any `/amp` postfixes, so we'll never
// output a url with `/amp` in the end, except for the needed `amphtml`
@ -13,23 +13,23 @@ function sanitizeAmpUrl(url) {
function getUrl(data, absolute) {
if (schema.isPost(data)) {
return config.urlFor('post', {post: data, secure: data.secure}, absolute);
return utils.url.urlFor('post', {post: data, secure: data.secure}, absolute);
}
if (schema.isTag(data)) {
return config.urlFor('tag', {tag: data, secure: data.secure}, absolute);
return utils.url.urlFor('tag', {tag: data, secure: data.secure}, absolute);
}
if (schema.isUser(data)) {
return config.urlFor('author', {author: data, secure: data.secure}, absolute);
return utils.url.urlFor('author', {author: data, secure: data.secure}, absolute);
}
if (schema.isNav(data)) {
return config.urlFor('nav', {nav: data, secure: data.secure}, absolute);
return utils.url.urlFor('nav', {nav: data, secure: data.secure}, absolute);
}
// sanitize any trailing `/amp` in the url
return sanitizeAmpUrl(config.urlFor(data, {}, absolute));
return sanitizeAmpUrl(utils.url.urlFor(data, {}, absolute));
}
module.exports = getUrl;

View file

@ -11,7 +11,7 @@ var _ = require('lodash'),
backup;
writeExportFile = function writeExportFile(exportResult) {
var filename = path.resolve(config.paths.contentPath + '/data/' + exportResult.filename);
var filename = path.resolve(config.get('paths').contentPath + '/data/' + exportResult.filename);
return Promise.promisify(fs.writeFile)(filename, JSON.stringify(exportResult.data)).return(filename);
};

View file

@ -41,7 +41,7 @@ module.exports = function moveJQuery(options, logger) {
}
})
.then(function () {
if (_.isEmpty(config.privacy)) {
if (_.isEmpty(config.get('privacy'))) {
return Promise.resolve();
}

View file

@ -1,7 +1,7 @@
var config = require('../../../../config'),
models = require(config.paths.corePath + '/server/models'),
api = require(config.paths.corePath + '/server/api'),
sequence = require(config.paths.corePath + '/server/utils/sequence'),
models = require(config.get('paths').corePath + '/server/models'),
api = require(config.get('paths').corePath + '/server/api'),
sequence = require(config.get('paths').corePath + '/server/utils/sequence'),
moment = require('moment'),
_ = require('lodash'),
Promise = require('bluebird'),
@ -26,7 +26,6 @@ _private.addOffset = function addOffset(date) {
};
/**
* postgres: stores dates with offset, so it's enough to force timezone UTC in the db connection (see data/db/connection.js)
* sqlite: stores UTC timestamps, but we will normalize the format to YYYY-MM-DD HH:mm:ss
*/
module.exports = function transformDatesIntoUTC(options, logger) {
@ -39,15 +38,13 @@ module.exports = function transformDatesIntoUTC(options, logger) {
return sequence([
function databaseCheck() {
// we have to change the sqlite format, because it stores dates as integer
if (ServerTimezoneOffset === 0 && config.database.client === 'mysql') {
if (ServerTimezoneOffset === 0 && config.get('database').client === 'mysql') {
return Promise.reject(new Error('skip'));
}
if (config.database.isPostgreSQL()) {
_private.noOffset = true;
} else if (config.database.client === 'mysql') {
if (config.get('database').client === 'mysql') {
_private.noOffset = false;
} else if (config.database.client === 'sqlite3') {
} else if (config.get('database').client === 'sqlite3') {
_private.noOffset = true;
}
@ -187,7 +184,7 @@ module.exports = function transformDatesIntoUTC(options, logger) {
});
},
function setActiveTimezone() {
var timezone = config.forceTimezoneOnMigration || moment.tz.guess();
var timezone = config.get('forceTimezoneOnMigration') || moment.tz.guess();
return models.Settings.edit({
key: 'activeTimezone',
value: timezone

View file

@ -1,9 +1,9 @@
var config = require('../../../../config'),
_ = require('lodash'),
models = require(config.paths.corePath + '/server/models'),
transfomDatesIntoUTC = require(config.paths.corePath + '/server/data/migration/fixtures/006/01-transform-dates-into-utc'),
models = require(config.get('paths').corePath + '/server/models'),
transfomDatesIntoUTC = require(config.get('paths').corePath + '/server/data/migration/fixtures/006/01-transform-dates-into-utc'),
Promise = require('bluebird'),
messagePrefix = 'Fix sqlite/pg format: ',
messagePrefix = 'Fix sqlite format: ',
_private = {};
_private.rerunDateMigration = function rerunDateMigration(options, logger) {
@ -33,22 +33,16 @@ _private.rerunDateMigration = function rerunDateMigration(options, logger) {
};
/**
* this migration script is a very special one for people who run their server in UTC and use sqlite3 or run their server in any TZ and use postgres
* this migration script is a very special one for people who run their server in UTC and use sqlite3
* 006/01-transform-dates-into-utc had a bug for this case, see what happen because of this bug https://github.com/TryGhost/Ghost/issues/7192
*/
module.exports = function fixSqliteFormat(options, logger) {
// CASE: skip this script when using mysql
if (config.database.client === 'mysql') {
logger.warn(messagePrefix + 'This script only runs, when using sqlite/postgres as database.');
if (config.get('database').client === 'mysql') {
logger.warn(messagePrefix + 'This script only runs, when using sqlite as database.');
return Promise.resolve();
}
// CASE: database is postgres, server is in ANY TZ, run 006/001 again
// we can't check the format for PG somehow, so we just run the migration again
if (config.database.isPostgreSQL()) {
return _private.rerunDateMigration(options, logger);
}
// CASE: sqlite3 and server is UTC, we check if the date migration was already running
return options.transacting.raw('select created_at from users')
.then(function (users) {

View file

@ -0,0 +1,27 @@
var Promise = require('bluebird'),
versioning = require('./versioning'),
migrations = require('../migration'),
errors = require('./../../errors');
module.exports = function bootUp() {
return versioning
.getDatabaseVersion()
.then(function successHandler(result) {
if (!/^alpha/.test(result)) {
// This database was not created with Ghost alpha, and is not compatible
throw new errors.DatabaseVersion(
'Your database version is not compatible with Ghost 1.0.0 Alpha (master branch)',
'Want to keep your DB? Use Ghost < 1.0.0 or the "stable" branch. Otherwise please delete your DB and restart Ghost',
'More information on the Ghost 1.0.0 Alpha at https://support.ghost.org/v1-0-alpha'
);
}
},
// We don't use .catch here, as it would catch the error from the successHandler
function errorHandler(err) {
if (err instanceof errors.DatabaseNotPopulated) {
return migrations.populate();
}
return Promise.reject(err);
});
};

View file

@ -1,11 +1,7 @@
var sqlite3 = require('./sqlite3'),
mysql = require('./mysql'),
pg = require('./pg');
mysql = require('./mysql');
module.exports = {
sqlite3: sqlite3,
mysql: mysql,
pg: pg,
postgres: pg,
postgresql: pg
mysql: mysql
};

View file

@ -1,7 +1,7 @@
{
"core": {
"databaseVersion": {
"defaultValue": "008"
"defaultValue": "alpha.1"
},
"dbHash": {
"defaultValue": null

View file

@ -1,11 +1,6 @@
var schema = require('./schema'),
checks = require('./checks'),
commands = require('./commands'),
versioning = require('./versioning'),
defaultSettings = require('./default-settings');
module.exports.tables = schema;
module.exports.checks = checks;
module.exports.commands = commands;
module.exports.versioning = versioning;
module.exports.defaultSettings = defaultSettings;
module.exports.tables = require('./schema');
module.exports.checks = require('./checks');
module.exports.commands = require('./commands');
module.exports.versioning = require('./versioning');
module.exports.defaultSettings = require('./default-settings');
module.exports.bootUp = require('./bootup');

View file

@ -30,7 +30,7 @@ module.exports = {
name: {type: 'string', maxlength: 150, nullable: false},
slug: {type: 'string', maxlength: 150, nullable: false, unique: true},
password: {type: 'string', maxlength: 60, nullable: false},
email: {type: 'string', maxlength: 254, nullable: false, unique: true, validations: {isEmail: true}},
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
image: {type: 'text', maxlength: 2000, nullable: true},
cover: {type: 'text', maxlength: 2000, nullable: true},
bio: {type: 'string', maxlength: 200, nullable: true},
@ -188,14 +188,14 @@ module.exports = {
},
accesstokens: {
id: {type: 'increments', nullable: false, primary: true},
token: {type: 'string', nullable: false, unique: true},
token: {type: 'string', maxlength: 191, nullable: false, unique: true},
user_id: {type: 'integer', nullable: false, unsigned: true, references: 'users.id'},
client_id: {type: 'integer', nullable: false, unsigned: true, references: 'clients.id'},
expires: {type: 'bigInteger', nullable: false}
},
refreshtokens: {
id: {type: 'increments', nullable: false, primary: true},
token: {type: 'string', nullable: false, unique: true},
token: {type: 'string', maxlength: 191, nullable: false, unique: true},
user_id: {type: 'integer', nullable: false, unsigned: true, references: 'users.id'},
client_id: {type: 'integer', nullable: false, unsigned: true, references: 'clients.id'},
expires: {type: 'bigInteger', nullable: false}
@ -204,7 +204,7 @@ module.exports = {
id: {type: 'increments', nullable: false, primary: true},
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {isUUID: true}},
name: {type: 'string', maxlength: 150, nullable: true},
email: {type: 'string', maxlength: 254, nullable: false, unique: true, validations: {isEmail: true}},
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
status: {type: 'string', maxlength: 150, nullable: false, defaultTo: 'pending', validations: {isIn: [['subscribed', 'pending', 'unsubscribed']]}},
post_id: {type: 'integer', nullable: true, unsigned: true, references: 'posts.id'},
subscribed_url: {type: 'text', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},

View file

@ -31,10 +31,6 @@ function getDatabaseVersion() {
.where('key', 'databaseVersion')
.first('value')
.then(function (version) {
if (!version || isNaN(version.value)) {
return Promise.reject(new errors.DatabaseVersion(i18n.t('errors.data.versioning.index.dbVersionNotRecognized')));
}
return version.value;
});
}

View file

@ -2,7 +2,7 @@ var https = require('https'),
errors = require('../../errors'),
url = require('url'),
Promise = require('bluebird'),
config = require('../../config'),
utils = require('../../utils'),
events = require('../../events'),
api = require('../../api/settings'),
i18n = require('../../i18n'),
@ -47,7 +47,7 @@ function ping(post) {
// If this is a post, we want to send the link of the post
if (schema.isPost(post)) {
message = config.urlFor('post', {post: post}, true);
message = utils.url.urlFor('post', {post: post}, true);
} else {
message = post.message;
}
@ -72,7 +72,7 @@ function ping(post) {
slackData = {
text: message,
unfurl_links: true,
icon_url: config.urlFor({relativeUrl: '/ghost/img/ghosticon.jpg'}, {}, true),
icon_url: utils.url.urlFor({relativeUrl: '/ghost/img/ghosticon.jpg'}, {}, true),
username: 'Ghost'
};

View file

@ -133,15 +133,15 @@ validateSettings = function validateSettings(defaultSettings, model) {
validateActiveTheme = function validateActiveTheme(themeName) {
// If Ghost is running and its availableThemes collection exists
// give it priority.
if (config.paths.availableThemes && Object.keys(config.paths.availableThemes).length > 0) {
availableThemes = Promise.resolve(config.paths.availableThemes);
if (config.get('paths').availableThemes && Object.keys(config.get('paths').availableThemes).length > 0) {
availableThemes = Promise.resolve(config.get('paths').availableThemes);
}
if (!availableThemes) {
// A Promise that will resolve to an object with a property for each installed theme.
// This is necessary because certain configuration data is only available while Ghost
// is running and at times the validations are used when it's not (e.g. tests)
availableThemes = readThemes(config.paths.themePath);
availableThemes = readThemes(config.getContentPath('themes'));
}
return availableThemes.then(function then(themes) {

View file

@ -2,6 +2,7 @@ var crypto = require('crypto'),
downsize = require('downsize'),
RSS = require('rss'),
config = require('../../../config'),
utils = require('../../../utils'),
errors = require('../../../errors'),
filters = require('../../../filters'),
processUrls = require('../../../utils/make-absolute-urls'),
@ -17,11 +18,11 @@ var crypto = require('crypto'),
feedCache = {};
function isTag(req) {
return req.originalUrl.indexOf('/' + config.routeKeywords.tag + '/') !== -1;
return req.originalUrl.indexOf('/' + config.get('routeKeywords').tag + '/') !== -1;
}
function isAuthor(req) {
return req.originalUrl.indexOf('/' + config.routeKeywords.author + '/') !== -1;
return req.originalUrl.indexOf('/' + config.get('routeKeywords').author + '/') !== -1;
}
function handleError(next) {
@ -40,8 +41,8 @@ function getData(channelOpts, slugParam) {
if (result.data && result.data.tag) { titleStart = result.data.tag[0].name + ' - ' || ''; }
if (result.data && result.data.author) { titleStart = result.data.author[0].name + ' - ' || ''; }
response.title = titleStart + config.theme.title;
response.description = config.theme.description;
response.title = titleStart + config.get('theme').title;
response.description = config.get('theme').description;
response.results = {
posts: result.posts,
meta: result.meta
@ -52,12 +53,12 @@ function getData(channelOpts, slugParam) {
}
function getBaseUrl(req, slugParam) {
var baseUrl = config.paths.subdir;
var baseUrl = utils.url.getSubdir();
if (isTag(req)) {
baseUrl += '/' + config.routeKeywords.tag + '/' + slugParam + '/rss/';
baseUrl += '/' + config.get('routeKeywords').tag + '/' + slugParam + '/rss/';
} else if (isAuthor(req)) {
baseUrl += '/' + config.routeKeywords.author + '/' + slugParam + '/rss/';
baseUrl += '/' + config.get('routeKeywords').author + '/' + slugParam + '/rss/';
} else {
baseUrl += '/rss/';
}
@ -106,7 +107,7 @@ generateFeed = function generateFeed(data) {
});
data.results.posts.forEach(function forEach(post) {
var itemUrl = config.urlFor('post', {post: post, secure: data.secure}, true),
var itemUrl = utils.url.urlFor('post', {post: post, secure: data.secure}, true),
htmlContent = processUrls(post.html, data.siteUrl, itemUrl),
item = {
title: post.title,
@ -121,7 +122,7 @@ generateFeed = function generateFeed(data) {
imageUrl;
if (post.image) {
imageUrl = config.urlFor('image', {image: post.image, secure: data.secure}, true);
imageUrl = utils.url.urlFor('image', {image: post.image, secure: data.secure}, true);
// Add a media content tag
item.custom_elements.push({
@ -176,8 +177,8 @@ generate = function generate(req, res, next) {
}
data.version = res.locals.safeVersion;
data.siteUrl = config.urlFor('home', {secure: req.secure}, true);
data.feedUrl = config.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.siteUrl = utils.url.urlFor('home', {secure: req.secure}, true);
data.feedUrl = utils.url.urlFor({relativeUrl: baseUrl, secure: req.secure}, true);
data.secure = req.secure;
return getFeedXml(req.originalUrl, data).then(function then(feedXml) {

View file

@ -1,9 +1,9 @@
var _ = require('lodash'),
xml = require('xml'),
moment = require('moment'),
config = require('../../../config'),
utils = require('../../../utils'),
events = require('../../../events'),
utils = require('./utils'),
localUtils = require('./utils'),
Promise = require('bluebird'),
path = require('path'),
CHANGE_FREQ = 'weekly',
@ -92,7 +92,7 @@ _.extend(BaseSiteMapGenerator.prototype, {
};
// Return the xml
return utils.getDeclarations() + xml(data);
return localUtils.getDeclarations() + xml(data);
},
updateXmlFromNodes: function (urlElements) {
@ -133,11 +133,11 @@ _.extend(BaseSiteMapGenerator.prototype, {
},
getUrlForDatum: function () {
return config.urlFor('home', true);
return utils.url.urlFor('home', true);
},
getUrlForImage: function (image) {
return config.urlFor('image', {image: image}, true);
return utils.url.urlFor('image', {image: image}, true);
},
getPriorityForDatum: function () {

View file

@ -1,8 +1,8 @@
var _ = require('lodash'),
xml = require('xml'),
moment = require('moment'),
config = require('../../../config'),
utils = require('./utils'),
utils = require('../../../utils'),
localUtils = require('./utils'),
RESOURCES,
XMLNS_DECLS;
@ -28,14 +28,14 @@ _.extend(SiteMapIndexGenerator.prototype, {
};
// Return the xml
return utils.getDeclarations() + xml(data);
return localUtils.getDeclarations() + xml(data);
},
generateSiteMapUrlElements: function () {
var self = this;
return _.map(RESOURCES, function (resourceType) {
var url = config.urlFor({
var url = utils.url.urlFor({
relativeUrl: '/sitemap-' + resourceType + '.xml'
}, true),
lastModified = self[resourceType].lastModified;

View file

@ -1,6 +1,6 @@
var _ = require('lodash'),
api = require('../../../api'),
config = require('../../../config'),
utils = require('../../../utils'),
BaseMapGenerator = require('./base-generator');
// A class responsible for generating a sitemap from posts and keeping it updated
@ -46,10 +46,10 @@ _.extend(PageMapGenerator.prototype, {
getUrlForDatum: function (post) {
if (post.id === 0 && !_.isEmpty(post.name)) {
return config.urlFor(post.name, true);
return utils.url.urlFor(post.name, true);
}
return config.urlFor('post', {post: post}, true);
return utils.url.urlFor('post', {post: post}, true);
},
getPriorityForDatum: function (post) {

View file

@ -1,6 +1,6 @@
var _ = require('lodash'),
api = require('../../../api'),
config = require('../../../config'),
utils = require('../../../utils'),
BaseMapGenerator = require('./base-generator');
// A class responsible for generating a sitemap from posts and keeping it updated
@ -41,7 +41,7 @@ _.extend(PostMapGenerator.prototype, {
},
getUrlForDatum: function (post) {
return config.urlFor('post', {post: post}, true);
return utils.url.urlFor('post', {post: post}, true);
},
getPriorityForDatum: function (post) {

View file

@ -1,6 +1,6 @@
var _ = require('lodash'),
api = require('../../../api'),
config = require('../../../config'),
utils = require('../../../utils'),
BaseMapGenerator = require('./base-generator');
// A class responsible for generating a sitemap from posts and keeping it updated
@ -38,7 +38,7 @@ _.extend(TagsMapGenerator.prototype, {
},
getUrlForDatum: function (tag) {
return config.urlFor('tag', {tag: tag}, true);
return utils.url.urlFor('tag', {tag: tag}, true);
},
getPriorityForDatum: function () {

View file

@ -1,6 +1,6 @@
var _ = require('lodash'),
api = require('../../../api'),
config = require('../../../config'),
utils = require('../../../utils'),
validator = require('validator'),
BaseMapGenerator = require('./base-generator'),
// @TODO: figure out a way to get rid of this
@ -42,7 +42,7 @@ _.extend(UserMapGenerator.prototype, {
},
getUrlForDatum: function (user) {
return config.urlFor('author', {author: user}, true);
return utils.url.urlFor('author', {author: user}, true);
},
getPriorityForDatum: function () {

View file

@ -1,13 +1,13 @@
var config = require('../../../config'),
utils;
var utils = require('../../../utils'),
sitemapsUtils;
utils = {
sitemapsUtils = {
getDeclarations: function () {
var baseUrl = config.urlFor('sitemap_xsl', true);
var baseUrl = utils.url.urlFor('sitemap_xsl', true);
baseUrl = baseUrl.replace(/^(http:|https:)/, '');
return '<?xml version="1.0" encoding="UTF-8"?>' +
'<?xml-stylesheet type="text/xsl" href="' + baseUrl + '"?>';
}
};
module.exports = utils;
module.exports = sitemapsUtils;

View file

@ -2,6 +2,7 @@ var _ = require('lodash'),
http = require('http'),
xml = require('xml'),
config = require('../../config'),
utils = require('../../utils'),
errors = require('../../errors'),
events = require('../../events'),
i18n = require('../../i18n'),
@ -19,7 +20,7 @@ pingList = [{
function ping(post) {
var pingXML,
title = post.title,
url = config.urlFor('post', {post: post}, true);
url = utils.url.urlFor('post', {post: post}, true);
// Only ping when in production and not a page
if (process.env.NODE_ENV !== 'production' || post.page || config.isPrivacyDisabled('useRpcPing')) {

View file

@ -25,22 +25,12 @@ var _ = require('lodash'),
DatabaseNotPopulated = require('./database-not-populated'),
DatabaseVersion = require('./database-version'),
i18n = require('../i18n'),
config,
config = require('../config'),
errors,
// Paths for views
userErrorTemplateExists = false;
// Shim right now to deal with circular dependencies.
// @TODO(hswolff): remove circular dependency and lazy require.
function getConfigModule() {
if (!config) {
config = require('../config');
}
return config;
}
function isValidErrorStatus(status) {
return _.isNumber(status) && status >= 400 && status < 600;
}
@ -69,7 +59,7 @@ function getStatusCode(error) {
*/
errors = {
updateActiveTheme: function (activeTheme) {
userErrorTemplateExists = getConfigModule().paths.availableThemes[activeTheme].hasOwnProperty('error.hbs');
userErrorTemplateExists = config.get('paths').availableThemes[activeTheme].hasOwnProperty('error.hbs');
},
throwError: function (err) {
@ -135,7 +125,14 @@ errors = {
var self = this,
origArgs = _.toArray(arguments).slice(1),
stack,
msgs;
msgs,
hideStack = false;
// DatabaseVersion errors are usually fatal, we output a nice message
// And the stack is not at all useful in this case
if (err instanceof DatabaseVersion) {
hideStack = true;
}
if (_.isArray(err)) {
_.each(err, function (e) {
@ -182,7 +179,7 @@ errors = {
// add a new line
msgs.push('\n');
if (stack) {
if (stack && !hideStack) {
msgs.push(stack, '\n');
}
@ -295,7 +292,7 @@ errors = {
renderErrorPage: function (statusCode, err, req, res, next) {
/*jshint unused:false*/
var self = this,
defaultErrorTemplatePath = path.resolve(getConfigModule().paths.adminViews, 'user-error.hbs');
defaultErrorTemplatePath = path.resolve(config.get('paths').adminViews, 'user-error.hbs');
function parseStack(stack) {
if (!_.isString(stack)) {

View file

@ -3,6 +3,8 @@
var Promise = require('bluebird'),
chalk = require('chalk'),
fs = require('fs'),
path = require('path'),
_ = require('lodash'),
errors = require('./errors'),
config = require('./config'),
i18n = require('./i18n'),
@ -35,26 +37,37 @@ function GhostServer(rootApp) {
*/
GhostServer.prototype.start = function (externalApp) {
var self = this,
rootApp = externalApp ? externalApp : self.rootApp;
rootApp = externalApp ? externalApp : self.rootApp,
socketConfig, socketValues = {
path: path.join(config.get('paths').contentPath, process.env.NODE_ENV + '.socket'),
permissions: '660'
};
return new Promise(function (resolve) {
var socketConfig = config.getSocket();
if (config.get('server').hasOwnProperty('socket')) {
socketConfig = config.get('server').socket;
if (_.isString(socketConfig)) {
socketValues.path = socketConfig;
} else if (_.isObject(socketConfig)) {
socketValues.path = socketConfig.path || socketValues.path;
socketValues.permissions = socketConfig.permissions || socketValues.permissions;
}
if (socketConfig) {
// Make sure the socket is gone before trying to create another
try {
fs.unlinkSync(socketConfig.path);
fs.unlinkSync(socketValues.path);
} catch (e) {
// We can ignore this.
}
self.httpServer = rootApp.listen(socketConfig.path);
fs.chmod(socketConfig.path, socketConfig.permissions);
self.httpServer = rootApp.listen(socketValues.path);
fs.chmod(socketValues.path, socketValues.permissions);
config.set('server:socket', socketValues);
} else {
self.httpServer = rootApp.listen(
config.server.port,
config.server.host
config.get('server').port,
config.get('server').host
);
}
@ -62,7 +75,7 @@ GhostServer.prototype.start = function (externalApp) {
if (error.errno === 'EADDRINUSE') {
errors.logError(
i18n.t('errors.httpServer.addressInUse.error'),
i18n.t('errors.httpServer.addressInUse.context', {port: config.server.port}),
i18n.t('errors.httpServer.addressInUse.context', {port: config.get('server').port}),
i18n.t('errors.httpServer.addressInUse.help')
);
} else {
@ -168,16 +181,20 @@ GhostServer.prototype.logStartMessages = function () {
// Startup & Shutdown messages
if (process.env.NODE_ENV === 'production') {
console.log(
chalk.green(i18n.t('notices.httpServer.ghostIsRunningIn', {env: process.env.NODE_ENV})),
i18n.t('notices.httpServer.yourBlogIsAvailableOn', {url: config.url}),
chalk.red('Currently running Ghost 1.0.0 Alpha, this is NOT suitable for production! \n'),
chalk.white('Please switch to the stable branch. \n'),
chalk.white('More information on the Ghost 1.0.0 Alpha at: ') + chalk.cyan('https://support.ghost.org/v1-0-alpha') + '\n',
chalk.gray(i18n.t('notices.httpServer.ctrlCToShutDown'))
);
} else {
console.log(
chalk.blue('Welcome to the Ghost 1.0.0 Alpha - this version of Ghost is for development only.')
);
console.log(
chalk.green(i18n.t('notices.httpServer.ghostIsRunningIn', {env: process.env.NODE_ENV})),
i18n.t('notices.httpServer.listeningOn'),
config.getSocket() || config.server.host + ':' + config.server.port,
i18n.t('notices.httpServer.urlConfiguredAs', {url: config.url}),
config.get('server').socket || config.get('server').host + ':' + config.get('server').port,
i18n.t('notices.httpServer.urlConfiguredAs', {url: config.get('url')}),
chalk.gray(i18n.t('notices.httpServer.ctrlCToShutDown'))
);
}

View file

@ -12,8 +12,8 @@
var hbs = require('express-hbs'),
_ = require('lodash'),
config = require('../config'),
utils = require('./utils'),
utils = require('../utils'),
localUtils = require('./utils'),
author;
author = function (options) {
@ -26,8 +26,8 @@ author = function (options) {
if (this.author && this.author.name) {
if (autolink) {
output = utils.linkTemplate({
url: config.urlFor('author', {author: this.author}),
output = localUtils.linkTemplate({
url: utils.url.urlFor('author', {author: this.author}),
text: _.escape(this.author.name)
});
} else {

View file

@ -82,7 +82,7 @@ function ghost_head(options) {
context = this.context ? this.context : null,
useStructuredData = !config.isPrivacyDisabled('useStructuredData'),
safeVersion = this.safeVersion,
referrerPolicy = config.referrerPolicy ? config.referrerPolicy : 'no-referrer-when-downgrade',
referrerPolicy = config.get('referrerPolicy') ? config.get('referrerPolicy') : 'no-referrer-when-downgrade',
fetch = {
metaData: getMetaData(this, options.data.root),
client: getClient()

View file

@ -4,14 +4,14 @@
// Returns the URL for the current object scope i.e. If inside a post scope will return image permalink
// `absolute` flag outputs absolute URL, else URL is relative.
var config = require('../config'),
var utils = require('../utils'),
image;
image = function (options) {
var absolute = options && options.hash.absolute;
if (this.image) {
return config.urlFor('image', {image: this.image}, absolute);
return utils.url.urlFor('image', {image: this.image}, absolute);
}
};

View file

@ -8,9 +8,9 @@
var hbs = require('express-hbs'),
_ = require('lodash'),
config = require('../config'),
utils = require('../utils'),
labs = require('../utils/labs'),
utils = require('./utils'),
localUtils = require('./utils'),
tags;
tags = function (options) {
@ -24,7 +24,7 @@ tags = function (options) {
limit = options.hash.limit ? parseInt(options.hash.limit, 10) : undefined,
from = options.hash.from ? parseInt(options.hash.from, 10) : 1,
to = options.hash.to ? parseInt(options.hash.to, 10) : undefined,
visibility = utils.parseVisibility(options),
visibility = localUtils.parseVisibility(options),
output = '';
function createTagList(tags) {
@ -42,8 +42,8 @@ tags = function (options) {
}
}
var tagOutput = autolink ? utils.linkTemplate({
url: config.urlFor('tag', {tag: tag}),
var tagOutput = autolink ? localUtils.linkTemplate({
url: utils.url.urlFor('tag', {tag: tag}),
text: _.escape(tag.name)
}) : _.escape(tag.name);

View file

@ -14,7 +14,6 @@ require('./overrides');
// Module dependencies
var express = require('express'),
_ = require('lodash'),
uuid = require('node-uuid'),
Promise = require('bluebird'),
i18n = require('./i18n'),
@ -22,8 +21,7 @@ var express = require('express'),
config = require('./config'),
errors = require('./errors'),
middleware = require('./middleware'),
migrations = require('./data/migration'),
versioning = require('./data/schema/versioning'),
db = require('./data/schema'),
models = require('./models'),
permissions = require('./permissions'),
apps = require('./apps'),
@ -32,6 +30,8 @@ var express = require('express'),
GhostServer = require('./ghost-server'),
scheduling = require('./scheduling'),
validateThemes = require('./utils/validate-themes'),
readDirectory = require('./utils/read-directory'),
utils = require('./utils'),
dbHash;
function initDbHashAndFirstRun() {
@ -71,48 +71,13 @@ function init(options) {
// Initialize Internationalization
i18n.init();
// Load our config.js file from the local file system.
return config.load(options.config).then(function () {
return config.checkDeprecated();
return readDirectory(config.getContentPath('apps')).then(function loadThemes(result) {
config.set('paths:availableApps', result);
return api.themes.loadThemes();
}).then(function () {
models.init();
}).then(function () {
return versioning.getDatabaseVersion()
.then(function (currentVersion) {
var response = migrations.update.isDatabaseOutOfDate({
fromVersion: currentVersion,
toVersion: versioning.getNewestDatabaseVersion(),
forceMigration: process.env.FORCE_MIGRATION
}), maintenanceState;
if (response.migrate === true) {
maintenanceState = config.maintenance.enabled || false;
config.maintenance.enabled = true;
migrations.update.execute({
fromVersion: currentVersion,
toVersion: versioning.getNewestDatabaseVersion(),
forceMigration: process.env.FORCE_MIGRATION
}).then(function () {
config.maintenance.enabled = maintenanceState;
}).catch(function (err) {
if (!err) {
return;
}
errors.logErrorAndExit(err, err.context, err.help);
});
} else if (response.error) {
return Promise.reject(response.error);
}
})
.catch(function (err) {
if (err instanceof errors.DatabaseNotPopulated) {
return migrations.populate();
}
return Promise.reject(err);
});
// @TODO: this is temporary, replace migrations with a warning if a DB exists
return db.bootUp();
}).then(function () {
// Populate any missing default settings
return models.Settings.populateDefaults();
@ -142,7 +107,7 @@ function init(options) {
middleware(parentApp);
// Log all theme errors and warnings
validateThemes(config.paths.themePath)
validateThemes(config.getContentPath('themes'))
.catch(function (result) {
// TODO: change `result` to something better
result.errors.forEach(function (err) {
@ -160,7 +125,12 @@ function init(options) {
// scheduling can trigger api requests, that's why we initialize the module after the ghost server creation
// scheduling module can create x schedulers with different adapters
return scheduling.init(_.extend(config.scheduling, {apiUrl: config.apiUrl()}));
return scheduling.init({
active: config.get('scheduling').active,
apiUrl: utils.url.apiUrl(),
internalPath: config.get('paths').internalSchedulingPath,
contentPath: config.getContentPath('scheduling')
});
}).then(function () {
return ghostServer;
});

View file

@ -8,8 +8,8 @@ var _ = require('lodash'),
i18n = require('../i18n');
function GhostMailer() {
var transport = config.mail && config.mail.transport || 'direct',
options = config.mail && _.clone(config.mail.options) || {};
var transport = config.get('mail') && config.get('mail').transport || 'direct',
options = config.get('mail') && _.clone(config.get('mail').options) || {};
this.state = {};
@ -19,7 +19,7 @@ function GhostMailer() {
}
GhostMailer.prototype.from = function () {
var from = config.mail && (config.mail.from || config.mail.fromaddress),
var from = config.get('mail') && (config.get('mail').from || config.get('mail').fromaddress),
defaultBlogTitle;
// If we don't have a from address at all
@ -30,7 +30,7 @@ GhostMailer.prototype.from = function () {
// If we do have a from address, and it's just an email
if (validator.isEmail(from)) {
defaultBlogTitle = config.theme.title ? config.theme.title : i18n.t('common.mail.title', {domain: this.getDomain()});
defaultBlogTitle = config.get('theme').title ? config.get('theme').title : i18n.t('common.mail.title', {domain: this.getDomain()});
from = '"' + defaultBlogTitle + '" <' + from + '>';
}
@ -40,7 +40,7 @@ GhostMailer.prototype.from = function () {
// Moved it to its own module
GhostMailer.prototype.getDomain = function () {
var domain = config.url.match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
var domain = config.get('url').match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
return domain && domain[1];
};

View file

@ -13,7 +13,7 @@ exports.generateContent = function generateContent(options) {
data;
defaults = {
siteUrl: config.forceAdminSSL ? (config.urlSSL || config.url) : config.url
siteUrl: config.get('forceAdminSSL') ? (config.get('urlSSL') || config.get('url')) : config.get('url')
};
data = _.defaults(defaults, options.data);

View file

@ -38,12 +38,12 @@ function sslForbiddenOrRedirect(opt) {
// Check to see if we should use SSL
// and redirect if needed
checkSSL = function checkSSL(req, res, next) {
if (isSSLrequired(res.isAdmin, config.url, config.forceAdminSSL)) {
if (isSSLrequired(res.isAdmin, config.get('url'), config.get('forceAdminSSL'))) {
if (!req.secure) {
var response = sslForbiddenOrRedirect({
forceAdminSSL: config.forceAdminSSL,
configUrlSSL: config.urlSSL,
configUrl: config.url,
forceAdminSSL: config.get('forceAdminSSL'),
configUrlSSL: config.get('urlSSL'),
configUrl: config.get('url'),
reqUrl: req.url
});

View file

@ -32,10 +32,10 @@ function getIPs() {
}
function getUrls() {
var urls = [url.parse(config.url).hostname];
var urls = [url.parse(config.get('url')).hostname];
if (config.urlSSL) {
urls.push(url.parse(config.urlSSL).hostname);
if (config.get('urlSSL')) {
urls.push(url.parse(config.get('urlSSL')).hostname);
}
return urls;

View file

@ -61,15 +61,15 @@ middleware = {
};
setupMiddleware = function setupMiddleware(blogApp) {
var logging = config.logging,
corePath = config.paths.corePath,
var logging = config.get('logging'),
corePath = config.get('paths').corePath,
adminApp = express(),
adminHbs = hbs.create();
// ##Configuration
// enabled gzip compression by default
if (config.server.compress !== false) {
if (config.get('server').compress !== false) {
blogApp.use(compress());
}
@ -103,10 +103,10 @@ setupMiddleware = function setupMiddleware(blogApp) {
}
// Preload link headers
if (config.preloadHeaders) {
if (config.get('preloadHeaders')) {
blogApp.use(netjet({
cache: {
max: config.preloadHeaders
max: config.get('preloadHeaders')
}
}));
}
@ -136,7 +136,7 @@ setupMiddleware = function setupMiddleware(blogApp) {
// Admin only config
blogApp.use('/ghost', serveStatic(
config.paths.clientAssets,
config.get('paths').clientAssets,
{maxAge: utils.ONE_YEAR_MS}
));
@ -145,15 +145,15 @@ setupMiddleware = function setupMiddleware(blogApp) {
// which do not need HTTPS. In fact, if HTTPS is forced on them, then 404 page might
// not display properly when HTTPS is not available!
blogApp.use(checkSSL);
adminApp.set('views', config.paths.adminViews);
adminApp.set('views', config.get('paths').adminViews);
// Theme only config
blogApp.use(staticTheme());
// setup middleware for internal apps
// @TODO: refactor this to be a proper app middleware hook for internal & external apps
config.internalApps.forEach(function (appName) {
var app = require(path.join(config.paths.internalAppPath, appName));
config.get('internalApps').forEach(function (appName) {
var app = require(path.join(config.get('paths').internalAppPath, appName));
if (app.hasOwnProperty('setupMiddleware')) {
app.setupMiddleware(blogApp);
}

View file

@ -3,7 +3,7 @@ var config = require('../config'),
errors = require('../errors');
module.exports = function (req, res, next) {
if (config.maintenance.enabled) {
if (config.get('maintenance').enabled) {
return next(new errors.Maintenance(
i18n.t('errors.general.maintenance')
));

View file

@ -14,7 +14,7 @@ function exchangeRefreshToken(client, refreshToken, scope, done) {
return done(new errors.NoPermissionError(i18n.t('errors.middleware.oauth.invalidRefreshToken')), false);
} else {
var token = model.toJSON(),
accessToken = utils.uid(256),
accessToken = utils.uid(191),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;
@ -47,8 +47,8 @@ function exchangePassword(client, username, password, scope, done) {
// Validate the user
return models.User.check({email: username, password: password}).then(function then(user) {
// Everything validated, return the access- and refreshtoken
var accessToken = utils.uid(256),
refreshToken = utils.uid(256),
var accessToken = utils.uid(191),
refreshToken = utils.uid(191),
accessExpires = Date.now() + utils.ONE_HOUR_MS,
refreshExpires = Date.now() + utils.ONE_WEEK_MS;

View file

@ -1,11 +1,11 @@
var api = require('../api'),
config = require('../config');
utils = require('../utils');
// Redirect to setup if no user exists
function redirectToSetup(req, res, next) {
api.authentication.isSetup().then(function then(exists) {
if (!exists.setup[0].status && !req.path.match(/\/setup\//)) {
return res.redirect(config.paths.subdir + '/ghost/setup/');
return res.redirect(utils.url.getSubdir() + '/ghost/setup/');
}
next();
}).catch(function handleError(err) {

View file

@ -1,13 +1,14 @@
var crypto = require('crypto'),
fs = require('fs'),
path = require('path'),
config = require('../config');
config = require('../config'),
utils = require('../utils');
// ### ServeSharedFile Middleware
// Handles requests to robots.txt and favicon.ico (and caches them)
function serveSharedFile(file, type, maxAge) {
var content,
corePath = config.paths.corePath,
corePath = config.get('paths').corePath,
filePath,
blogRegex = /(\{\{blog-url\}\})/g,
apiRegex = /(\{\{api-url\}\})/g;
@ -26,8 +27,8 @@ function serveSharedFile(file, type, maxAge) {
}
if (type === 'text/xsl' || type === 'text/plain' || type === 'application/javascript') {
buf = buf.toString().replace(blogRegex, config.url.replace(/\/$/, ''));
buf = buf.toString().replace(apiRegex, config.apiUrl({cors: true}));
buf = buf.toString().replace(blogRegex, config.get('url').replace(/\/$/, ''));
buf = buf.toString().replace(apiRegex, utils.url.apiUrl({cors: true}));
}
content = {
headers: {

View file

@ -21,7 +21,7 @@ function forwardToExpressStatic(req, res, next) {
next();
} else {
express.static(
path.join(config.paths.themePath, req.app.get('activeTheme')),
path.join(config.getContentPath('themes'), req.app.get('activeTheme')),
{maxAge: utils.ONE_YEAR_MS}
)(req, res, next);
}

View file

@ -15,8 +15,8 @@ themeHandler = {
ghostLocals: function ghostLocals(req, res, next) {
// Make sure we have a locals value.
res.locals = res.locals || {};
res.locals.version = config.ghostVersion;
res.locals.safeVersion = config.ghostVersion.match(/^(\d+\.)?(\d+)/)[0];
res.locals.version = config.get('ghostVersion');
res.locals.safeVersion = config.get('ghostVersion').match(/^(\d+\.)?(\d+)/)[0];
// relative path from the URL
res.locals.relativeUrl = req.path;
@ -26,13 +26,13 @@ themeHandler = {
// ### configHbsForContext Middleware
// Setup handlebars for the current context (admin or theme)
configHbsForContext: function configHbsForContext(req, res, next) {
var themeData = _.cloneDeep(config.theme),
labsData = _.cloneDeep(config.labs),
var themeData = _.cloneDeep(config.get('theme')),
labsData = _.cloneDeep(config.get('labs')),
blogApp = req.app;
if (req.secure && config.urlSSL) {
if (req.secure && config.get('urlSSL')) {
// For secure requests override .url property with the SSL version
themeData.url = config.urlSSL.replace(/\/$/, '');
themeData.url = config.get('urlSSL').replace(/\/$/, '');
}
// Change camelCase to snake_case
@ -41,8 +41,8 @@ themeHandler = {
hbs.updateTemplateOptions({data: {blog: themeData, labs: labsData}});
if (config.paths.themePath && blogApp.get('activeTheme')) {
blogApp.set('views', path.join(config.paths.themePath, blogApp.get('activeTheme')));
if (config.getContentPath('themes') && blogApp.get('activeTheme')) {
blogApp.set('views', path.join(config.getContentPath('themes'), blogApp.get('activeTheme')));
}
// Pass 'secure' flag to the view engine
@ -56,14 +56,14 @@ themeHandler = {
// Helper for updateActiveTheme
activateTheme: function activateTheme(blogApp, activeTheme) {
var hbsOptions,
themePartials = path.join(config.paths.themePath, activeTheme, 'partials');
themePartials = path.join(config.getContentPath('themes'), activeTheme, 'partials');
// clear the view cache
blogApp.cache = {};
// set view engine
hbsOptions = {
partialsDir: [config.paths.helperTemplates],
partialsDir: [config.get('paths').helperTemplates],
onCompile: function onCompile(exhbs, source) {
return exhbs.handlebars.compile(source, {preventIndent: true});
}
@ -98,7 +98,7 @@ themeHandler = {
// Check if the theme changed
if (activeTheme.value !== blogApp.get('activeTheme')) {
// Change theme
if (!config.paths.availableThemes.hasOwnProperty(activeTheme.value)) {
if (!config.get('paths').availableThemes.hasOwnProperty(activeTheme.value)) {
if (!res.isAdmin) {
// Throw an error if the theme is not available, but not on the admin UI
return errors.throwError(i18n.t('errors.middleware.themehandler.missingTheme', {theme: activeTheme.value}));

View file

@ -8,8 +8,8 @@ module.exports = function upload(options) {
// if we finish the data/importer logic, we forward the request to the specified importer
return function (req, res, next) {
var extensions = (config.uploads[type] && config.uploads[type].extensions) || [],
contentTypes = (config.uploads[type] && config.uploads[type].contentTypes) || [];
var extensions = (config.get('uploads')[type] && config.get('uploads')[type].extensions) || [],
contentTypes = (config.get('uploads')[type] && config.get('uploads')[type].contentTypes) || [];
req.file = req.file || {};
req.file.name = req.file.originalname;

View file

@ -525,9 +525,9 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
}
// Check the filtered slug doesn't match any of the reserved keywords
return filters.doFilter('slug.reservedSlugs', config.slugs.reserved).then(function then(slugList) {
return filters.doFilter('slug.reservedSlugs', config.get('slugs').reserved).then(function then(slugList) {
// Some keywords cannot be changed
slugList = _.union(slugList, config.slugs.protected);
slugList = _.union(slugList, utils.url.getProtectedSlugs());
return _.includes(slugList, slug) ? slug + '-' + baseName : slug;
}).then(function then(slug) {

View file

@ -1,8 +1,8 @@
var config = require('../../config'),
events = require(config.paths.corePath + '/server/events'),
models = require(config.paths.corePath + '/server/models'),
errors = require(config.paths.corePath + '/server/errors'),
sequence = require(config.paths.corePath + '/server/utils/sequence'),
events = require(config.get('paths:corePath') + '/server/events'),
models = require(config.get('paths:corePath') + '/server/models'),
errors = require(config.get('paths:corePath') + '/server/errors'),
sequence = require(config.get('paths:corePath') + '/server/utils/sequence'),
moment = require('moment-timezone');
/**

View file

@ -10,6 +10,7 @@ var _ = require('lodash'),
ghostBookshelf = require('./base'),
events = require('../events'),
config = require('../config'),
utils = require('../utils'),
baseUtils = require('./base/utils'),
i18n = require('../i18n'),
Post,
@ -176,10 +177,10 @@ Post = ghostBookshelf.Model.extend({
i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
));
// CASE: to schedule/reschedule a post, a minimum diff of x minutes is needed (default configured is 2minutes)
} else if (publishedAtHasChanged && moment(publishedAt).isBefore(moment().add(config.times.cannotScheduleAPostBeforeInMinutes, 'minutes'))) {
} else if (publishedAtHasChanged && moment(publishedAt).isBefore(moment().add(config.get('times').cannotScheduleAPostBeforeInMinutes, 'minutes'))) {
return Promise.reject(new errors.ValidationError(
i18n.t('errors.models.post.expectedPublishedAtInFuture', {
cannotScheduleAPostBeforeInMinutes: config.times.cannotScheduleAPostBeforeInMinutes
cannotScheduleAPostBeforeInMinutes: config.get('times').cannotScheduleAPostBeforeInMinutes
})
));
}
@ -419,7 +420,7 @@ Post = ghostBookshelf.Model.extend({
}
if (!options.columns || (options.columns && options.columns.indexOf('url') > -1)) {
attrs.url = config.urlPathForPost(attrs);
attrs.url = utils.url.urlPathForPost(attrs);
}
return attrs;

View file

@ -9,8 +9,8 @@ var express = require('express'),
frontendRoutes = function frontendRoutes() {
var router = express.Router(),
subdir = config.paths.subdir,
routeKeywords = config.routeKeywords;
subdir = utils.url.getSubdir(),
routeKeywords = config.get('routeKeywords');
// ### Admin routes
router.get(/^\/(logout|signout)\/$/, function redirectToSignout(req, res) {
@ -33,8 +33,8 @@ frontendRoutes = function frontendRoutes() {
// setup routes for internal apps
// @TODO: refactor this to be a proper app route hook for internal & external apps
config.internalApps.forEach(function (appName) {
var app = require(path.join(config.paths.internalAppPath, appName));
config.get('internalApps').forEach(function (appName) {
var app = require(path.join(config.get('paths').internalAppPath, appName));
if (app.hasOwnProperty('setupRoutes')) {
app.setupRoutes(router);
}

View file

@ -1,6 +1,6 @@
var Promise = require('bluebird'),
moment = require('moment'),
utils = require(__dirname + '/../utils'),
localUtils = require(__dirname + '/../utils'),
events = require(__dirname + '/../../events'),
errors = require(__dirname + '/../../errors'),
models = require(__dirname + '/../../models'),
@ -53,7 +53,7 @@ exports.init = function init(options) {
.then(function (_client) {
client = _client;
return utils.createAdapter(config);
return localUtils.createAdapter(config);
})
.then(function (_adapter) {
adapter = _adapter;

View file

@ -8,7 +8,8 @@ exports.createAdapter = function (options) {
var adapter = null,
activeAdapter = options.active,
path = options.path;
internalPath = options.internalPath,
contentPath = options.contentPath;
if (!activeAdapter) {
return Promise.reject(new errors.IncorrectUsage('Please provide an active adapter.'));
@ -29,10 +30,27 @@ exports.createAdapter = function (options) {
* CASE: active adapter is located in specific ghost path
*/
try {
adapter = adapter || new (require(path + activeAdapter))(options);
adapter = adapter || new (require(contentPath + activeAdapter))(options);
} catch (err) {
// CASE: only throw error if module does exist
if (err.code !== 'MODULE_NOT_FOUND') {
return Promise.reject(new errors.IncorrectUsage(err.message));
}
// CASE: if module not found it can be an error within the adapter (cannot find bluebird for example)
else if (err.code === 'MODULE_NOT_FOUND' && err.message.indexOf(contentPath + activeAdapter) === -1) {
return Promise.reject(new errors.IncorrectUsage(err.message));
}
}
/**
* CASE: active adapter is located in internal ghost path
*/
try {
adapter = adapter || new (require(internalPath + activeAdapter))(options);
} catch (err) {
// CASE: only throw error if module does exist
if (err.code === 'MODULE_NOT_FOUND') {
return Promise.reject(new errors.IncorrectUsage('MODULE_NOT_FOUND', activeAdapter));
return Promise.reject(new errors.IncorrectUsage('We cannot find your adapter in: ' + contentPath + ' or: ' + internalPath));
}
return Promise.reject(new errors.IncorrectUsage(err.message));

Some files were not shown because too many files have changed in this diff Show more