2015-05-28 18:16:09 +03:00
|
|
|
// # Bootup
|
|
|
|
// This file needs serious love & refactoring
|
|
|
|
|
2016-09-14 17:50:17 +03:00
|
|
|
/**
|
|
|
|
* make sure overrides get's called first!
|
|
|
|
* - keeping the overrides require here works for installing Ghost as npm!
|
|
|
|
*
|
|
|
|
* the call order is the following:
|
|
|
|
* - root index requires core module
|
|
|
|
* - core index requires server
|
|
|
|
* - overrides is the first package to load
|
|
|
|
*/
|
|
|
|
require('./overrides');
|
|
|
|
|
2013-09-06 19:54:50 +04:00
|
|
|
// Module dependencies
|
2017-08-15 14:29:27 +03:00
|
|
|
var debug = require('ghost-ignition').debug('boot:init'),
|
2016-10-17 15:50:29 +03:00
|
|
|
config = require('./config'),
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
Promise = require('bluebird'),
|
2017-12-12 00:47:46 +03:00
|
|
|
common = require('./lib/common'),
|
2016-07-15 19:22:41 +03:00
|
|
|
models = require('./models'),
|
2017-12-14 05:01:23 +03:00
|
|
|
permissions = require('./services/permissions'),
|
|
|
|
auth = require('./services/auth'),
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
dbHealth = require('./data/db/health'),
|
2014-09-19 20:17:58 +04:00
|
|
|
GhostServer = require('./ghost-server'),
|
2017-05-15 13:52:01 +03:00
|
|
|
scheduling = require('./adapters/scheduling'),
|
2017-12-14 05:01:23 +03:00
|
|
|
settings = require('./services/settings'),
|
|
|
|
themes = require('./services/themes'),
|
2017-12-11 21:14:05 +03:00
|
|
|
urlService = require('./services/url'),
|
2017-10-30 15:31:04 +03:00
|
|
|
|
|
|
|
// Services that need initialisation
|
|
|
|
apps = require('./services/apps'),
|
|
|
|
xmlrpc = require('./services/xmlrpc'),
|
2017-11-21 18:43:14 +03:00
|
|
|
slack = require('./services/slack'),
|
|
|
|
webhooks = require('./services/webhooks');
|
2013-12-06 18:13:15 +04:00
|
|
|
|
2015-05-28 18:16:09 +03:00
|
|
|
// ## Initialise Ghost
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
function init() {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Init Start...');
|
2016-07-15 19:22:41 +03:00
|
|
|
|
2016-09-30 14:45:59 +03:00
|
|
|
var ghostServer, parentApp;
|
2016-05-19 14:49:22 +03:00
|
|
|
|
2018-01-09 16:50:57 +03:00
|
|
|
// Initialize default internationalization, just for core now
|
|
|
|
// (settings for language and theme not yet available here)
|
2017-12-12 00:47:46 +03:00
|
|
|
common.i18n.init();
|
2018-01-09 16:50:57 +03:00
|
|
|
debug('Default i18n done for core');
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
models.init();
|
|
|
|
debug('models done');
|
2015-11-12 15:29:45 +03:00
|
|
|
|
2017-02-22 02:26:19 +03:00
|
|
|
return dbHealth.check().then(function () {
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
debug('DB health check done');
|
2014-05-16 06:29:42 +04:00
|
|
|
// Populate any missing default settings
|
🎨 ⏱ Cleanup / optimise the server.init() function (#7985)
refs #2182
* 🔥 Remove unused options from server init
- this is left over from old code and is now unused
* 🎨 Move knex-migrator check to db health
- Move complex check function into own module
- Call module from server/index.js
- This just improves the readability of server/index.js
* 🔥 Remove old comments
- These comments all make no sense now!
* 🎨 ⏱ Move model init out of promise chain
- Model.init() does not return a promise
- Therefore, we can move it to the top of the init function, outside of the promise change
- This should be a minor optimisation, and again improves readability /clarity of what's happening
* ✨ ⁉️ Move DBHash init / first run to Settings model
- this structure is left over from when we had code we executed on the first run of Ghost
- the implementation used the API to initialise one setting before populateDefaults is called
- this had lots of dependencies - the whole model, API, and permissions structure had to be initialised for it to work
- the new implementation is simpler, it captures the dbHash getting initialised during populateDefaults()
- it also adds an event, so we can do first-run code later if we really want to (or maybe apps can?!)
- perhaps this is hiding behaviour, and there's a nicer way to do it, but populateDefaults seems like a sane place to populate a default setting 😁
* ⏱ Optimise require order so config is first
- the first require to config will cause the files to be read etc
- this ensures that it happens early, and isn't confusingly timed as part of loading a different module
* 🎨 Simplify settings model changes
2017-02-17 19:44:34 +03:00
|
|
|
// Refresh the API settings cache
|
2017-02-27 18:53:04 +03:00
|
|
|
return settings.init();
|
2014-05-07 04:49:25 +04:00
|
|
|
}).then(function () {
|
2016-11-08 16:37:19 +03:00
|
|
|
debug('Update settings cache done');
|
2018-04-16 00:53:18 +03:00
|
|
|
|
|
|
|
common.events.emit('db.ready');
|
|
|
|
|
2018-01-09 16:50:57 +03:00
|
|
|
// Full internationalization for core could be here
|
|
|
|
// in a future version with backend translations
|
|
|
|
// (settings for language and theme available here;
|
|
|
|
// internationalization for theme is done
|
|
|
|
// shortly after, when activating the theme)
|
|
|
|
//
|
2014-05-07 04:49:25 +04:00
|
|
|
// Initialize the permissions actions and objects
|
|
|
|
return permissions.init();
|
2013-12-06 18:13:15 +04:00
|
|
|
}).then(function () {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Permissions done');
|
2014-08-17 10:17:23 +04:00
|
|
|
return Promise.join(
|
2017-02-22 02:26:19 +03:00
|
|
|
themes.init(),
|
2015-03-24 23:23:23 +03:00
|
|
|
// Initialize xmrpc ping
|
2016-06-05 14:22:11 +03:00
|
|
|
xmlrpc.listen(),
|
2016-03-29 11:40:44 +03:00
|
|
|
// Initialize slack ping
|
2017-11-16 16:03:24 +03:00
|
|
|
slack.listen(),
|
2017-11-21 18:43:14 +03:00
|
|
|
// Initialize webhook pings
|
2017-12-11 18:56:04 +03:00
|
|
|
webhooks.listen()
|
2013-12-06 18:13:15 +04:00
|
|
|
);
|
2014-01-15 03:14:44 +04:00
|
|
|
}).then(function () {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Apps, XMLRPC, Slack done');
|
2016-10-06 15:27:35 +03:00
|
|
|
|
🎉 🎨 ✨ Remove middleware/index.js (#7548)
closes #4172, closes #6948, refs #7491, refs #7488, refs #7542, refs #7484
* 🎨 Co-locate all admin-related code in /admin
- move all the admin related code from controllers, routes and helpers into a single location
- add error handling middleware explicitly to adminApp
- re-order blogApp middleware to ensure the shared middleware is mounted after the adminApp
- TODO: rethink the structure of /admin, this should probably be an internal app
* 💄 Group global middleware together
- There are only a few pieces of middleware which are "global"
- These are needed for the admin, blog and api
- Everything else is only needed in one or two places
* ✨ Introduce a separate blogApp
- create a brand-new blogApp
- mount all blog/theme only middleware etc onto blogApp
- mount error handling on blogApp only
* 🎨 Separate error handling for HTML & API JSON
- split JSON and HTML error handling into separate functions
- re-introduce a way to not output the stack for certain errors
- add more tests around errors & an assertion framework for checking JSON Errors
- TODO: better 404 handling for static assets
Rationale:
The API is very different to the blog/admin panel:
- It is intended to only ever serve JSON, never HTML responses
- It is intended to always serve JSON
Meanwhile the blog and admin panel have no need for JSON errors,
when an error happens on those pages, we should serve HTML pages
which are nicely formatted with the error & using the correct template
* 🐛 Fix checkSSL to work for subapps
- in order to make this work on a sub app we need to use the pattern `req.originalUrl || req.url`
* 🔥 Get rid of decide-is-admin (part 1/2)
- delete decide-is-admin & tests
- add two small functions to apiApp and adminApp to set res.isAdmin
- mount checkSSL on all the apps
- TODO: deduplicate the calls to checkSSL by making blogApp a subApp :D
- PART 2/2: finish cleaning this up by removing it from where it's not needed and giving it a more specific name
Rationale:
Now that we have both an adminApp and an apiApp,
we can temporarily replace this weird path-matching middleware
with middleware that sets res.isAdmin for api & admin
* 🎨 Wire up prettyURLs on all Apps
- prettyURLs is needed for all requests
- it cannot be global because it has to live after asset middleware, and before routing
- this does not result in duplicate redirects, but does result in duplicate checks
- TODO: resolve extra middleware in stack by making blogApp a sub app
* ⏱ Add debug to API setup
* 🎨 Rename blogApp -> parentApp in middleware
* 🎨 Co-locate all blog-related code in /blog
- Move all of the blogApp code from middleware/index.js to blog/app.js
- Move routes/frontend.js to blog/routes.js
- Remove the routes/index.js and routes folder, this is empty now!
- @TODO is blog the best name for this? 🤔
- @TODO sort out the big hunk of asset-related mess
- @TODO also separate out the concept of theme from blog
* 🎉 Replace middleware index with server/app.js
- The final piece of the puzzle! 🎉 🎈 🎂
- We no longer have our horrendous middleware/index.js
- Instead, we have a set of app.js files, which all use a familiar pattern
* 💄 Error handling fixups
2016-10-13 18:24:09 +03:00
|
|
|
// Setup our collection of express apps
|
2017-12-06 19:37:54 +03:00
|
|
|
parentApp = require('./web/parent-app')();
|
2016-10-06 15:27:35 +03:00
|
|
|
|
2017-08-22 14:23:23 +03:00
|
|
|
// Initialise analytics events
|
|
|
|
if (config.get('segment:key')) {
|
|
|
|
require('./analytics-events').init();
|
|
|
|
}
|
|
|
|
|
🎉 🎨 ✨ Remove middleware/index.js (#7548)
closes #4172, closes #6948, refs #7491, refs #7488, refs #7542, refs #7484
* 🎨 Co-locate all admin-related code in /admin
- move all the admin related code from controllers, routes and helpers into a single location
- add error handling middleware explicitly to adminApp
- re-order blogApp middleware to ensure the shared middleware is mounted after the adminApp
- TODO: rethink the structure of /admin, this should probably be an internal app
* 💄 Group global middleware together
- There are only a few pieces of middleware which are "global"
- These are needed for the admin, blog and api
- Everything else is only needed in one or two places
* ✨ Introduce a separate blogApp
- create a brand-new blogApp
- mount all blog/theme only middleware etc onto blogApp
- mount error handling on blogApp only
* 🎨 Separate error handling for HTML & API JSON
- split JSON and HTML error handling into separate functions
- re-introduce a way to not output the stack for certain errors
- add more tests around errors & an assertion framework for checking JSON Errors
- TODO: better 404 handling for static assets
Rationale:
The API is very different to the blog/admin panel:
- It is intended to only ever serve JSON, never HTML responses
- It is intended to always serve JSON
Meanwhile the blog and admin panel have no need for JSON errors,
when an error happens on those pages, we should serve HTML pages
which are nicely formatted with the error & using the correct template
* 🐛 Fix checkSSL to work for subapps
- in order to make this work on a sub app we need to use the pattern `req.originalUrl || req.url`
* 🔥 Get rid of decide-is-admin (part 1/2)
- delete decide-is-admin & tests
- add two small functions to apiApp and adminApp to set res.isAdmin
- mount checkSSL on all the apps
- TODO: deduplicate the calls to checkSSL by making blogApp a subApp :D
- PART 2/2: finish cleaning this up by removing it from where it's not needed and giving it a more specific name
Rationale:
Now that we have both an adminApp and an apiApp,
we can temporarily replace this weird path-matching middleware
with middleware that sets res.isAdmin for api & admin
* 🎨 Wire up prettyURLs on all Apps
- prettyURLs is needed for all requests
- it cannot be global because it has to live after asset middleware, and before routing
- this does not result in duplicate redirects, but does result in duplicate checks
- TODO: resolve extra middleware in stack by making blogApp a sub app
* ⏱ Add debug to API setup
* 🎨 Rename blogApp -> parentApp in middleware
* 🎨 Co-locate all blog-related code in /blog
- Move all of the blogApp code from middleware/index.js to blog/app.js
- Move routes/frontend.js to blog/routes.js
- Remove the routes/index.js and routes folder, this is empty now!
- @TODO is blog the best name for this? 🤔
- @TODO sort out the big hunk of asset-related mess
- @TODO also separate out the concept of theme from blog
* 🎉 Replace middleware index with server/app.js
- The final piece of the puzzle! 🎉 🎈 🎂
- We no longer have our horrendous middleware/index.js
- Instead, we have a set of app.js files, which all use a familiar pattern
* 💄 Error handling fixups
2016-10-13 18:24:09 +03:00
|
|
|
debug('Express Apps done');
|
✨Dynamic Routing Beta (#9596)
refs #9601
### Dynamic Routing
This is the beta version of dynamic routing.
- we had a initial implementation of "channels" available in the codebase
- we have removed and moved this implementation
- there is now a centralised place for dynamic routing - server/services/routing
- each routing component is represented by a router type e.g. collections, routes, static pages, taxonomies, rss, preview of posts
- keep as much as possible logic of routing helpers, middlewares and controllers
- ensure test coverage
- connect all the things together
- yaml file + validation
- routing + routers
- url service
- sitemaps
- url access
- deeper implementation of yaml validations
- e.g. hard require slashes
- ensure routing hierarchy/order
- e.g. you enable the subscriber app
- you have a custom static page, which lives under the same slug /subscribe
- static pages are stronger than apps
- e.g. the first collection owns the post it has filtered
- a post cannot live in two collections
- ensure apps are still working and hook into the routers layer (or better said: and register in the routing service)
- put as much as possible comments to the code base for better understanding
- ensure a clean debug log
- ensure we can unmount routes
- e.g. you have a collection permalink of /:slug/ represented by {globals.permalink}
- and you change the permalink in the admin to dated permalink
- the express route get's refreshed from /:slug/ to /:year/:month/:day/:slug/
- unmount without server restart, yey
- ensure we are backwards compatible
- e.g. render home.hbs for collection index if collection route is /
- ensure you can access your configured permalink from the settings table with {globals.permalink}
### Render 503 if url service did not finish
- return 503 if the url service has not finished generating the resource urls
### Rewrite sitemaps
- we have rewritten the sitemaps "service", because the url generator does no longer happen on runtime
- we generate all urls on bootstrap
- the sitemaps service will consume created resource and router urls
- these urls will be shown on the xml pages
- we listen on url events
- we listen on router events
- we no longer have to fetch the resources, which is nice
- the urlservice pre-fetches resources and emits their urls
- the urlservice is the only component who knows which urls are valid
- i made some ES6 adaptions
- we keep the caching logic -> only regenerate xml if there is a change
- updated tests
- checked test coverage (100%)
### Re-work usage of Url utility
- replace all usages of `urlService.utils.urlFor` by `urlService.getByResourceId`
- only for resources e.g. post, author, tag
- this is important, because with dynamic routing we no longer create static urls based on the settings permalink on runtime
- adapt url utility
- adapt tests
2018-06-05 20:02:20 +03:00
|
|
|
}).then(function () {
|
|
|
|
/**
|
|
|
|
* @NOTE:
|
|
|
|
*
|
|
|
|
* Must happen after express app bootstrapping, because we need to ensure that all
|
|
|
|
* routers are created and are now ready to register additional routes. In this specific case, we
|
|
|
|
* are waiting that the AppRouter was instantiated. And then we can register e.g. amp if enabled.
|
|
|
|
*
|
|
|
|
* If you create a published post, the url is always stronger than any app url, which is equal.
|
|
|
|
*/
|
|
|
|
return apps.init();
|
2017-03-02 22:50:58 +03:00
|
|
|
}).then(function () {
|
2017-09-18 15:01:58 +03:00
|
|
|
parentApp.use(auth.init());
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Auth done');
|
2017-09-18 15:01:58 +03:00
|
|
|
|
2016-05-14 21:02:45 +03:00
|
|
|
return new GhostServer(parentApp);
|
2016-05-19 14:49:22 +03:00
|
|
|
}).then(function (_ghostServer) {
|
|
|
|
ghostServer = _ghostServer;
|
|
|
|
|
|
|
|
// 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
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Server done');
|
2016-09-13 22:38:16 +03:00
|
|
|
return scheduling.init({
|
2016-10-25 14:19:22 +03:00
|
|
|
schedulerUrl: config.get('scheduling').schedulerUrl,
|
2016-09-13 22:38:16 +03:00
|
|
|
active: config.get('scheduling').active,
|
2017-12-11 21:14:05 +03:00
|
|
|
apiUrl: urlService.utils.urlFor('api', true),
|
2016-09-13 23:24:57 +03:00
|
|
|
internalPath: config.get('paths').internalSchedulingPath,
|
|
|
|
contentPath: config.getContentPath('scheduling')
|
2016-09-13 22:38:16 +03:00
|
|
|
});
|
2016-05-19 14:49:22 +03:00
|
|
|
}).then(function () {
|
2016-10-03 11:33:14 +03:00
|
|
|
debug('Scheduling done');
|
|
|
|
debug('...Init End');
|
2016-05-19 14:49:22 +03:00
|
|
|
return ghostServer;
|
2014-02-20 07:22:02 +04:00
|
|
|
});
|
2013-11-23 21:54:47 +04:00
|
|
|
}
|
|
|
|
|
2013-11-17 22:40:26 +04:00
|
|
|
module.exports = init;
|