diff --git a/.gitignore b/.gitignore index e61c7d609..ca3762d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ node_modules modules/downloaded/* modules/custom modules/community/* +modules/site +modules/private media/* themes/downloaded/* themes/custom/* @@ -15,3 +17,5 @@ docs/coverage.html *.sock *.gz *.sass-cache +themes/community/ +themes/private/ diff --git a/app-cluster.js b/app-cluster.js index d1c9cff27..b2626bc25 100644 --- a/app-cluster.js +++ b/app-cluster.js @@ -1,42 +1,132 @@ -// TODO - THIS NEEDS TO BE REPLACED WITH THE NEW Node 0.5+ multi process, see cliftonc/ted +/** + * Master server process + * Initialises a number of instances of the app, based on the number of CPU's it detects + * is available. + * + * First arg is the port + * Second arg is the num worker threads; + * + * e.g. node server 3000 8 + * + */ +// Dependencies +var rootpath = process.cwd() + '/', + cluster = require('cluster'), + path = require('path'), + logo = require(path.join(rootpath, 'logo')), + colors = require('colors'), + port = process.env.PORT || 3000, + restarts = 0, + totalWorkers = 0, + runningWorkers = 0; + +var argv = processArgs(); + +launchServer(); /** - * Calipso script for running in clustered mode. Usage: node app-cluster, or - * NODE_ENV=production node app-cluster + * Launch server instance, initially master, then each worker instance is forked. + * All instances share same config. */ -var cluster = require('cluster'); -var port = process.env.PORT || 3000; -var path = __dirname; -var app; +function launchServer() { + + // Check if we are the master process + if (cluster.isMaster) { + + //require('./app').boot(function (app) { + + + // Load configuration + var Config = require(rootpath + "lib/core/Configuration"), + config = new Config(); + + config.init(); + + // Print the logo + logo.print(); + + // Set the number of workers + totalWorkers = config.get('server:cluster:workers') || argv.c; + + // Fork workers based on num cpus + console.log("Loading ".green + totalWorkers + " workers, please wait ...".green); + for (var i = 0; i < totalWorkers; i++) { + forkWorker(); + } + + // Log worker death + // TODO : Auto restart with number of retries + cluster.on('death', function(worker) { + + console.error('worker ' + worker.pid + ' died ...'); + + // Manage restarting of workers + if(config.get('server:cluster:restartWorkers')) { + if(restarts > config.get('server:cluster:maximumRestarts')) { + console.error('Maximum number of restarts reached, not restarting this worker.'.red); + } else { + restarts++; + forkWorker(); + } + } + + }); + + //}); + + } else { + + // We are a child worker, so bootstrap the app. + require(rootpath + 'app').boot(true, function (app) { + + //logger.info("Worker [" + argv.m.cyan + "] with pid " + (process.pid + "").grey + " online."); + app.listen(port); + + process.send({ cmd: 'workerStarted', pid: process.pid, port: port }); + + }); + + } + +} + +/** + * Helper function to fork a worker, we need to reset the counter in the master thread + * hence the messaging back, also deal with messaging around job management from worker threads. + */ +function forkWorker() { + + var worker = cluster.fork(); + + worker.on('message', function(msg) { + + if (msg.cmd) { + + if(msg.cmd == 'workerStarted') { + + runningWorkers++; + + if(runningWorkers === parseInt(totalWorkers)) { + console.log("Calipso configured for: ".green + (global.process.env.NODE_ENV || 'development') + " environment.".green); + console.log("Calipso server running ".green + runningWorkers + " workers, listening on port: ".green + port); + } + + } + + } + + }); + +} /** - * Create an instance of calipso via the normal App, + * Process command line arguments using optimist */ -require('./app').boot(function (app) { - - /** - * TODO: Check to ensure that the logs and pids folders exist before launching - */ - - cluster(app) - .set('working directory', path) - .set('socket path', path) - .in('development') - .set('workers', 3) - .use(cluster.logger(path + '/logs', 'debug')) - .use(cluster.debug()) - .use(cluster.pidfiles(path + '/pids')) - .use(cluster.stats({ connections: true, lightRequests: true })) - .in('test') - .set('workers', 3) - .use(cluster.logger(path + '/logs', 'warning')) - .use(cluster.pidfiles(path + '/pids')) - .in('production') - .set('workers', 3) - .use(cluster.logger(path + '/logs')) - .use(cluster.pidfiles(path + '/pids')) - .in('all') - .listen(port); - - -},true); +function processArgs() { + return require('optimist') + .usage('Launch Calipso in Clustered Mode\nUsage: $0') + .describe('c', 'Number of CPUs') + .alias('c', 'cpu') + .default('c', require('os').cpus().length) + .argv; +} diff --git a/app.js b/app.js index 9f1880d9c..de2ea220b 100644 --- a/app.js +++ b/app.js @@ -8,6 +8,86 @@ * */ +var req = require('express/lib/request'); + + var flashFormatters = req.flashFormatters = { + s: function(val){ + return String(val); + } + }; + + /** + * Queue flash `msg` of the given `type`. + * + * Examples: + * + * req.flash('info', 'email sent'); + * req.flash('error', 'email delivery failed'); + * req.flash('info', 'email re-sent'); + * // => 2 + * + * req.flash('info'); + * // => ['email sent', 'email re-sent'] + * + * req.flash('info'); + * // => [] + * + * req.flash(); + * // => { error: ['email delivery failed'], info: [] } + * + * Formatting: + * + * Flash notifications also support arbitrary formatting support. + * For example you may pass variable arguments to `req.flash()` + * and use the %s specifier to be replaced by the associated argument: + * + * req.flash('info', 'email has been sent to %s.', userName); + * + * To add custom formatters use the `exports.flashFormatters` object. + * + * @param {String} type + * @param {String} msg + * @return {Array|Object|Number} + * @api public + */ + + function miniMarkdown(str){ + return String(str) + .replace(/(__|\*\*)(.*?)\1/g, '$2') + .replace(/(_|\*)(.*?)\1/g, '$2') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + }; + + req.flash = function(type, msg){ + if (this.session === undefined) throw Error('req.flash() requires sessions'); + var msgs = this.session.flash = this.session.flash || {}; + if (type && msg) { + var i = 2 + , args = arguments + , formatters = this.app.flashFormatters || {}; + formatters.__proto__ = flashFormatters; + msg = miniMarkdown(msg); + msg = msg.replace(/%([a-zA-Z])/g, function(_, format){ + var formatter = formatters[format]; + if (formatter) return formatter(utils.escape(args[i++])); + }); + return (msgs[type] = msgs[type] || []).push(msg); + } else if (type) { + var arr = msgs[type]; + delete msgs[type]; + return arr || []; + } else { + this.session.flash = {}; + return msgs; + } + }; + +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path'), @@ -40,8 +120,8 @@ var path = rootpath, function bootApplication(next) { // Create our express instance, export for later reference - var app = express.createServer(); - app.path = path; + var app = express.createServer ? express.createServer() : express(); + app.path = function() { return path }; app.isCluster = false; // Load configuration @@ -55,8 +135,10 @@ function bootApplication(next) { calipso.defaultTheme = app.config.get('themes:default'); app.use(express.bodyParser()); + // Pause requests if they were not parsed to allow PUT and POST with custom mime types + app.use(function (req, res, next) { if (!req._body) { req.pause(); } next(); }); app.use(express.methodOverride()); - app.use(express.cookieParser()); + app.use(express.cookieParser(app.config.get('session:secret'))); app.use(express.responseTime()); // Create dummy session middleware - tag it so we can later replace diff --git a/bin/calipso b/bin/calipso index c9cc1bbcc..cdbbcd26e 100644 --- a/bin/calipso +++ b/bin/calipso @@ -1,395 +1,3 @@ -#!/usr/bin/env node - -/** - * Calipso command prompt - * @Params - cmd - server | script | params - */ - -/** -* Dependencies on this script -*/ -var fs = require('fs'), - nodepath = require('path'), - exec = require('child_process').exec, - logo = require('../logo'), - colors = require('colors'); - -/** - * Optimist configuration - */ -var argv = require('optimist') - .default('src', false) - .default('port', 3000) - .alias('p', 'port') - .alias('s', 'src') - .boolean('s') - .argv; - -/** - * Paths - * path = directory script being run from - * calipsoPath = calipso library installation path - **/ -var path = fs.realpathSync('.').replace(/\s/g,'\\ '); -var calipsoPath = __dirname + "/../"; -var step = require('step'); - -/** - * Main Command Object - * Defaults to display the help script - */ -var appLauncher = { - command:argv._[0] ? argv._[0] : 'help', - server: { port:argv.port }, - src:argv.src, - script: { - name:'help', - params: argv._.splice(1) - } - }; - -runLauncher(appLauncher); - -/** - * Run the launcher - * @param appLauncher - */ -function runLauncher(appLauncher) { - - // Always use current directory? - console.log('Launching calipso from: '.cyan.bold + path.white); - console.log('Calipso directory: '.cyan.bold + calipsoPath.white); - - // Check if this is a calipso src folder - if(isLibrary() && !appLauncher.src) { - console.log('\r\nWARNING:'.yellow.bold + ' You are running this from a Calipso source folder.'.white.bold); - } - - // Check if this is a calipso site - if(!isCalipso() && appLauncher.command != 'site' && !isLibrary()) { - console.log('\x1b[1mThis is not a Calipso site - you must run:\x1b[0m calipso site SiteName\r\n'); - return; - } - - switch(appLauncher.command) { - case 'test': - runTests(appLauncher.script); - break; - case 'server': - runServer(appLauncher.server.port); - break; - case 'site': - createApplication(path,appLauncher.script.params); - break; - case 'install': - runInstall(path); - break; - case 'modules': - process.chdir(path); - require(path + '/app').boot(false, function(app) { - var modules = require(calipsoPath + '/lib/cli/Modules') - modules.moduleRouter(path,appLauncher.script.params,true,function(err) { - if(err) { - console.log("\r\n" + err.message.red.bold + "\r\n"); - } - process.exit(); - }); - }); - break; - case 'themes': - process.chdir(path); - require(path + '/app').boot(false, function(app) { - var themes = require(calipsoPath + '/lib/cli/Themes') - themes.themeRouter(path,appLauncher.script.params,true,function(err) { - if(err) { - console.log("\r\n" + err.message.red.bold + "\r\n"); - } - process.exit(); - }); - }); - break; - default: - // Default is to display help - appLauncher.command = 'script'; - runScript(appLauncher.script); - } - -} - -/** - * Check if we are running from the library folder (or something cloned out of github) - **/ -function isLibrary() { - return nodepath.existsSync(path + '/bin/calipso'); -} - -/** - * Check if .calipso exists - **/ -function isCalipso() { - return nodepath.existsSync(path + '/.calipso'); -} - -/** - * Run a script - * @param appLauncher - * Runs by default from path where calipso runs via __dirname. - */ -function runScript(scriptLauncher) { - - if(!nodepath.existsSync(path + '/scripts/'+ scriptLauncher.name)) { - scriptLauncher.name = 'help'; - scriptLauncher.params = []; - } - - var script = require(path + '/scripts/'+ scriptLauncher.name); - logo.print(); - script.execute(scriptLauncher.params, path); - - -} - -/** - * Run expresso tests - */ -function runTests(appLauncher) { - - // var test = appLauncher.name ? appLauncher.name : 'all'; - exec('make', { timeout: 60000, cwd:path }, function (error, stdout, stderr) { - console.log(stdout); - console.log(stderr); - }); - -} - -/** - * Launch a server - */ -function runServer(port) { - - logo.print(); - - // Ensure we run in the local folder of the application - process.chdir(path); - require(path + '/app').boot(false, function(app) { - app.listen(port); - console.log("Calipso server listening on port: ".green + app.address().port.toString().white.bold); - console.log("Calipso configured for ".green + (global.process.env.NODE_ENV || 'development').white.bold + " environment\r\n".green); - }); - -} - -/** - * Create application at the given directory `path`. - * - * @param {String} path - */ -function createApplicationAt(path) { - - step( - function create() { - var self = this; - - mkdir(path + '/bin',function() { - copy(calipsoPath + '/bin/*.sh',path + '/bin',self.parallel()); - }); - mkdir(path + '/conf',function() { - copy(calipsoPath + '/conf/*',path + '/conf',self.parallel()); - }); - mkdir(path + '/i18n',function() { - copy(calipsoPath + '/i18n/*',path + '/i18n',self.parallel()); - }); - mkdir(path + '/lib',function() { - copy(calipsoPath + '/lib/*',path + '/lib',self.parallel()); - }); - mkdir(path + '/modules',function() { - copy(calipsoPath + '/modules/*',path + '/modules',self.parallel()); - }); - mkdir(path + '/support',function() { - copy(calipsoPath + '/support/*',path + '/support',self.parallel()); - }); - mkdir(path + '/test',function() { - copy(calipsoPath + '/test/*',path + '/test',self.parallel()); - }); - mkdir(path + '/themes',function() { - copy(calipsoPath + '/themes/*',path + '/themes',self.parallel()); - }); - mkdir(path + '/utils',function() { - copy(calipsoPath + '/utils/*',path + '/utils',self.parallel()); - }); - mkdir(path + '/scripts',function() { - copy(calipsoPath + '/scripts/*',path + '/scripts',self.parallel()); - mkdir(path + '/scripts/templates',function() { - copy(calipsoPath + '/scripts/templates/*',path + '/scripts/templates',self.parallel()); - }); - }); - mkdir(path + '/node_modules',function() { - copy(calipsoPath + '/node_modules/*',path + '/node_modules',self.parallel()); - }); - mkdir(path + '/logs',self.parallel()); - mkdir(path + '/pids',self.parallel()); - mkdir(path + '/media',self.parallel()); - mkdir(path + '/tmp',self.parallel()); - copy(calipsoPath + '/app-cluster.js',path + '/',self.parallel()); - copy(calipsoPath + '/app.js',path + '/',self.parallel()); - copy(calipsoPath + '/package.json',path + '/',self.parallel()); - copy(calipsoPath + '/logo.js',path + '/',self.parallel()); - }, - function done() { - write(path + '/.calipso','Created @ ' + new Date()); - console.log(''); - console.log('Application created at: '.green + path.white.bold); - // CC : Disabled to test default NPM installation process - console.log('Installing any difficult application dependencies via NPM, please wait ... '.green); - runInstall(path); - } - ) - -} - -/** - * Run the install shell script - */ -function runInstall(path) { - path = path.replace(/\\\s/g, ' '); - exec('./bin/siteInstall.sh', { timeout: 60000, cwd:path }, function (error, stdout, stderr) { - console.log(stdout); - console.log(stderr); - }); - -} - -/** - * Create a site - */ -function createApplication(path,siteName) { - - var site; - if(siteName.toString().match(/^\//)) { - // site is a full path - site = siteName.toString(); - } else { - site = path + "/" + siteName; - } - - mkdir(site,function() { - emptyDirectory(site, function(empty){ - if (empty) { - createApplicationAt(site); - } else { - confirm('This will over-write the existing site, continue? '.red.bold, function(ok){ - if (ok) { - process.stdin.destroy(); - createApplicationAt(site); - } else { - abort('aborting'); - } - }); - } - }); - }); -}; - -/** - * Check if the given directory `path` is empty. - * - * @param {String} path - * @param {Function} fn - */ -function emptyDirectory(path, fn) { - fs.readdir(path, function(err, files){ - if (err && 'ENOENT' != err.code) throw err; - fn(!files || !files.length); - }); -} - -/** - * echo str > path. - * - * @param {String} path - * @param {String} str - */ - -function write(path, str) { - fs.writeFile(path, str); - console.log(' create : '.blue + path.white); -} - -/** - * Prompt confirmation with the given `msg`. - * - * @param {String} msg - * @param {Function} fn - */ - -function confirm(msg, fn) { - prompt(msg, function(val){ - fn(/^ *y(es)?/i.test(val)); - }); -} - -/** - * Prompt input with the given `msg` and callback `fn`. - * - * @param {String} msg - * @param {Function} fn - */ - -function prompt(msg, fn) { - // prompt - if (' ' == msg[msg.length - 1]) { - process.stdout.write(msg); - } else { - console.log(msg); - } - - // stdin - process.stdin.setEncoding('ascii'); - process.stdin.once('data', function(data){ - fn(data); - }).resume(); -} - -/** - * Mkdir -p. - * - * TODO - these are unix only ... - * - * @param {String} path - * @param {Function} fn - */ - -function mkdir(path, fn) { - exec('mkdir -p ' + path, function(err){ - if (err) throw err; - console.log(' create: '.blue + path.white); - fn && fn(); - }); -} - - -/** - * cp -r - * - * @param {String} path - * @param {Function} fn - */ - -function copy(from, to, fn) { - exec('cp -R ' + from + ' ' + to, function(err){ - if (err) throw err; - console.log(' Copied: '.blue + to.white); - fn && fn(); - }); -} - -/** - * Exit with the given `str`. - * - * @param {String} str - */ - -function abort(str) { - console.error(str); - process.exit(1); -} +#!/bin/sh +DIR=`dirname $0` +NODE_PATH="${DIR}/.." node "${DIR}/../lib/calipso-cli.js" $* diff --git a/lib/calipso-cli.js b/lib/calipso-cli.js new file mode 100755 index 000000000..302b8efd2 --- /dev/null +++ b/lib/calipso-cli.js @@ -0,0 +1,397 @@ +/** + * Calipso command prompt + * @Params - cmd - server | script | params + */ + +/** +* Dependencies on this script +*/ +var fs = require('fs'), + nodepath = require('path'), + exec = require('child_process').exec, + logo = require('../logo'), + colors = require('colors'); + +/** + * Optimist configuration + */ +var argv = require('optimist') + .default('src', false) + .default('port', 3000) + .alias('p', 'port') + .alias('s', 'src') + .boolean('s') + .argv; + +/** + * Paths + * path = directory script being run from + * calipsoPath = calipso library installation path + **/ +var path = fs.realpathSync('.'); +var calipsoPath = __dirname + "/../"; + +//require.paths.unshift(calipsoPath); //make local paths accessible + +var step = require('step'); + +/** + * Main Command Object + * Defaults to display the help script + */ +var appLauncher = { + command:argv._[0] ? argv._[0] : 'help', + server: { port:argv.port }, + src:argv.src, + script: { + name:'help', + params: argv._.splice(1) + } + }; + +runLauncher(appLauncher); + +/** + * Run the launcher + * @param appLauncher + */ +function runLauncher(appLauncher) { + + // Always use current directory? + console.log('Launching calipso from: '.cyan.bold + path.white); + console.log('Calipso directory: '.cyan.bold + calipsoPath.white); + + // Check if this is a calipso src folder + if(isLibrary() && !appLauncher.src) { + console.log('\r\nWARNING:'.yellow.bold + ' You are running this from a Calipso source folder.'.white.bold); + } + + // Check if this is a calipso site + if(!isCalipso() && appLauncher.command != 'site' && !isLibrary()) { + console.log('\x1b[1mThis is not a Calipso site - you must run:\x1b[0m calipso site SiteName\r\n'); + return; + } + + switch(appLauncher.command) { + case 'test': + runTests(appLauncher.script); + break; + case 'server': + runServer(appLauncher.server.port); + break; + case 'site': + createApplication(path,appLauncher.script.params); + break; + case 'install': + runInstall(path); + break; + case 'modules': + process.chdir(path); + //require.paths.unshift(path); //make local paths accessible + require(path + '/app').boot(function(app) { + var modules = require('lib/cli/Modules') + modules.moduleRouter(path,appLauncher.script.params,true,function(err) { + if(err) { + console.log("\r\n" + err.message.red.bold + "\r\n"); + } + process.exit(); + }); + }); + break; + case 'themes': + process.chdir(path); + //require.paths.unshift(path); //make local paths accessible + require(path + '/app').boot(function(app) { + var themes = require('lib/cli/Themes') + themes.themeRouter(path,appLauncher.script.params,true,function(err) { + if(err) { + console.log("\r\n" + err.message.red.bold + "\r\n"); + } + process.exit(); + }); + }); + break; + default: + // Default is to display help + appLauncher.command = 'script'; + runScript(appLauncher.script); + } + +} + +/** + * Check if we are running from the library folder (or something cloned out of github) + **/ +function isLibrary() { + return (fs.existsSync || nodepath.existsSync)(path + '/bin/calipso'); +} + +/** + * Check if .calipso exists + **/ +function isCalipso() { + return (fs.existsSync || nodepath.existsSync)(path + '/.calipso'); +} + +/** + * Run a script + * @param appLauncher + * Runs by default from path where calipso runs via __dirname. + */ +function runScript(scriptLauncher) { + + if(!(fs.existsSync || nodepath.existsSync)(path + '/scripts/'+ scriptLauncher.name)) { + scriptLauncher.name = 'help'; + scriptLauncher.params = []; + } + + var script = require(path + '/scripts/'+ scriptLauncher.name); + logo.print(); + script.execute(scriptLauncher.params, path); + + +} + +/** + * Run expresso tests + */ +function runTests(appLauncher) { + + // var test = appLauncher.name ? appLauncher.name : 'all'; + exec('make', { timeout: 60000, cwd:path }, function (error, stdout, stderr) { + console.log(stdout); + console.log(stderr); + }); + +} + +/** + * Launch a server + */ +function runServer(port) { + + logo.print(); + + // Ensure we run in the local folder of the application + process.chdir(path); + require(path + '/app').boot(function(app) { + app.listen(port); + console.log("Calipso server listening on port: ".green + app.address().port.toString().white.bold); + console.log("Calipso configured for ".green + (global.process.env.NODE_ENV || 'development').white.bold + " environment\r\n".green); + }); + +} + +/** + * Create application at the given directory `path`. + * + * @param {String} path + */ +function createApplicationAt(path) { + + step( + function create() { + var self = this; + + mkdir(path + '/bin',function() { + copy(calipsoPath + '/bin/*.sh',path + '/bin',self.parallel()); + }); + mkdir(path + '/conf',function() { + copy(calipsoPath + '/conf/*',path + '/conf',self.parallel()); + }); + mkdir(path + '/i18n',function() { + copy(calipsoPath + '/i18n/*',path + '/i18n',self.parallel()); + }); + mkdir(path + '/lib',function() { + copy(calipsoPath + '/lib/*',path + '/lib',self.parallel()); + }); + mkdir(path + '/modules',function() { + copy(calipsoPath + '/modules/*',path + '/modules',self.parallel()); + }); + mkdir(path + '/support',function() { + copy(calipsoPath + '/support/*',path + '/support',self.parallel()); + }); + mkdir(path + '/test',function() { + copy(calipsoPath + '/test/*',path + '/test',self.parallel()); + }); + mkdir(path + '/themes',function() { + copy(calipsoPath + '/themes/*',path + '/themes',self.parallel()); + }); + mkdir(path + '/utils',function() { + copy(calipsoPath + '/utils/*',path + '/utils',self.parallel()); + }); + mkdir(path + '/scripts',function() { + copy(calipsoPath + '/scripts/*',path + '/scripts',self.parallel()); + mkdir(path + '/scripts/templates',function() { + copy(calipsoPath + '/scripts/templates/*',path + '/scripts/templates',self.parallel()); + }); + }); + mkdir(path + '/node_modules',function() { + copy(calipsoPath + '/node_modules/*',path + '/node_modules',self.parallel()); + }); + mkdir(path + '/logs',self.parallel()); + mkdir(path + '/pids',self.parallel()); + mkdir(path + '/media',self.parallel()); + mkdir(path + '/tmp',self.parallel()); + copy(calipsoPath + '/app-cluster.js',path + '/',self.parallel()); + copy(calipsoPath + '/app.js',path + '/',self.parallel()); + copy(calipsoPath + '/package.json',path + '/',self.parallel()); + copy(calipsoPath + '/logo.js',path + '/',self.parallel()); + }, + function done() { + write(path + '/.calipso','Created @ ' + new Date()); + console.log(''); + console.log('Application created at: '.green + path.white.bold); + // CC : Disabled to test default NPM installation process + console.log('Installing any difficult application dependencies via NPM, please wait ... '.green); + runInstall(path); + } + ) + +} + +/** + * Run the install shell script + */ +function runInstall(path) { + exec('./bin/siteInstall.sh', { timeout: 60000, cwd:path }, function (error, stdout, stderr) { + console.log(stdout); + console.log(stderr); + }); + +} + +/** + * Create a site + */ +function createApplication(path,siteName) { + + var site; + if(siteName.toString().match(/^\//)) { + // site is a full path + site = siteName.toString(); + } else { + site = path + "/" + siteName; + } + + mkdir(site,function() { + emptyDirectory(site, function(empty){ + if (empty) { + createApplicationAt(site); + } else { + confirm('This will over-write the existing site, continue? '.red.bold, function(ok){ + if (ok) { + process.stdin.destroy(); + createApplicationAt(site); + } else { + abort('aborting'); + } + }); + } + }); + }); +}; + +/** + * Check if the given directory `path` is empty. + * + * @param {String} path + * @param {Function} fn + */ +function emptyDirectory(path, fn) { + fs.readdir(path, function(err, files){ + if (err && 'ENOENT' != err.code) throw err; + fn(!files || !files.length); + }); +} + +/** + * echo str > path. + * + * @param {String} path + * @param {String} str + */ + +function write(path, str) { + fs.writeFile(path, str); + console.log(' create : '.blue + path.white); +} + +/** + * Prompt confirmation with the given `msg`. + * + * @param {String} msg + * @param {Function} fn + */ + +function confirm(msg, fn) { + prompt(msg, function(val){ + fn(/^ *y(es)?/i.test(val)); + }); +} + +/** + * Prompt input with the given `msg` and callback `fn`. + * + * @param {String} msg + * @param {Function} fn + */ + +function prompt(msg, fn) { + // prompt + if (' ' == msg[msg.length - 1]) { + process.stdout.write(msg); + } else { + console.log(msg); + } + + // stdin + process.stdin.setEncoding('ascii'); + process.stdin.once('data', function(data){ + fn(data); + }).resume(); +} + +/** + * Mkdir -p. + * + * TODO - these are unix only ... + * + * @param {String} path + * @param {Function} fn + */ + +function mkdir(path, fn) { + exec('mkdir -p ' + path, function(err){ + if (err) throw err; + console.log(' create: '.blue + path.white); + fn && fn(); + }); +} + + +/** + * cp -r + * + * @param {String} path + * @param {Function} fn + */ + +function copy(from, to, fn) { + exec('cp -R ' + from + ' ' + to, function(err){ + if (err) throw err; + console.log(' Copied: '.blue + to.white); + fn && fn(); + }); +} + +/** + * Exit with the given `str`. + * + * @param {String} str + */ + +function abort(str) { + console.error(str); + process.exit(1); +} diff --git a/lib/cli/Download.js b/lib/cli/Download.js index b55a85504..ce9b8de61 100644 --- a/lib/cli/Download.js +++ b/lib/cli/Download.js @@ -12,6 +12,12 @@ * Module exports * */ +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path') calipso = require(path.join(rootpath, 'lib/calipso')), @@ -167,7 +173,7 @@ function downloadFile(url, fileName, toPath, next) { // Ensure we have our download folder var tmpFolder = toPath; - if(!path.existsSync(tmpFolder)) { + if(!(fs.existsSync || path.existsSync)(tmpFolder)) { fs.mkdirSync(tmpFolder, 0755); } @@ -401,7 +407,7 @@ function unzipDownload(type, file, callback) { if(tmpName) { // Make sure we delete any existing tmp folder - if(path.existsSync(tmpFolder)) { + if((fs.existsSync || path.existsSync)(tmpFolder)) { rimraf.sync(tmpFolder); } diff --git a/lib/cli/Modules.js b/lib/cli/Modules.js index 23588c7ff..19602bde0 100644 --- a/lib/cli/Modules.js +++ b/lib/cli/Modules.js @@ -16,6 +16,12 @@ * Module exports * */ +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path') calipso = require(path.join(rootpath, 'lib/calipso')), @@ -115,7 +121,7 @@ exports.findModule = findModule; * Try to download a module */ function downloadModule(options, cli, next) { - var toPath = calipso.app.path + "/modules/downloaded/"; + var toPath = calipso.app.path() + "/modules/downloaded/"; var fromUrl = options[1]; download('module', fromUrl, toPath, cli, function(err,moduleName,path) { if(err) { diff --git a/lib/cli/RepoApi.js b/lib/cli/RepoApi.js index 98231a69d..de4a3c490 100644 --- a/lib/cli/RepoApi.js +++ b/lib/cli/RepoApi.js @@ -12,6 +12,12 @@ * Module exports * */ +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path') calipso = require(path.join(rootpath, 'lib/calipso')), diff --git a/lib/cli/Themes.js b/lib/cli/Themes.js index c6ed8ba58..973a8619a 100644 --- a/lib/cli/Themes.js +++ b/lib/cli/Themes.js @@ -16,6 +16,12 @@ * Module exports * */ +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path') calipso = require(path.join(rootpath, 'lib/calipso')), @@ -94,7 +100,7 @@ exports.findTheme = findTheme; * Try to download a module */ function downloadTheme(options, cli, next) { - var toPath = calipso.app.path + "/themes/downloaded/"; + var toPath = calipso.app.path() + "/themes/downloaded/"; var fromUrl = options[1]; download('theme', fromUrl, toPath, cli, function(err, themeName, path) { if(err) { diff --git a/lib/core/Configuration.js b/lib/core/Configuration.js index 5fa94935b..9f085ea78 100644 --- a/lib/core/Configuration.js +++ b/lib/core/Configuration.js @@ -42,7 +42,7 @@ Configuration.prototype.init = function(next) { */ Configuration.prototype.check = function() { - if (!path.existsSync(this.file)) { + if (!(fs.existsSync || path.existsSync)(this.file)) { try { var defaultFile = fs.readFileSync(this.defaultConfig); // Parse it to make sure there are no errors diff --git a/lib/core/Menu.js b/lib/core/Menu.js index 78ae1f0ca..6fc8a344d 100644 --- a/lib/core/Menu.js +++ b/lib/core/Menu.js @@ -12,6 +12,12 @@ /** * Includes */ +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path'), utils = require('connect').utils, @@ -224,7 +230,8 @@ CalipsoMenu.prototype.menuStartTag = function(menu, selected) { return ""; diff --git a/lib/core/Module.js b/lib/core/Module.js index 040b0750e..50e7121d4 100644 --- a/lib/core/Module.js +++ b/lib/core/Module.js @@ -12,9 +12,10 @@ function eventRouteModules(req, res, next) { // Ignore static content // TODO : Make this more connect friendly or at least configurable - if (req.url.match(/^\/images|^\/js|^\/css|^\/favicon.ico|png$|jpg$|gif$|css$|js$/)) { - return next(); - } + // STATIC content may or may not be static. We should route it normally for now. + //if (req.url.match(/^\/images|^\/js|^\/css|^\/favicon.ico|png$|jpg$|gif$|css$|js$/)) { + // return next(); + //} req.timeStart = new Date(); @@ -31,7 +32,7 @@ function eventRouteModules(req, res, next) { }; // Route 'first' modules that fire before all others - // These first modules can stop the routing of all others + // These first modules can stop the routing of all others doFirstModules(req, res, function(err) { var iterator = function(module, cb) { @@ -54,14 +55,14 @@ function eventRouteModules(req, res, next) { function attachRequestEvents(req, res) { - // Create a request event listener for this request, pass in functions + // Create a request event listener for this request, pass in functions // to enable testing. req.event = new calipso.event.RequestEventListener({ notifyDependencyFn: notifyDependenciesOfRoute, registerDependenciesFn: registerDependencies }); - // + // var maxListeners = calipso.config.get('server:events:maxListeners'); // Attach all the modules to it @@ -82,7 +83,7 @@ function registerDependencies(moduleEmitter, moduleName) { calipso.modules[moduleName].fn.depends.forEach(function(dependentModule) { moduleEmitter.modules[moduleName].check[dependentModule] = false; }); - } + } } /** @@ -282,7 +283,8 @@ function doResponse(req, res, next) { // Render statuscodes dealt with by themeing engine // TODO - this is not very clean - if (res.statusCode === 404 || res.statusCode === 500 || res.statusCode === 200) { + calipso.silly("Responding with statusCode: " + res.statusCode); + if (res.statusCode === 404 || res.statusCode === 500 || res.statusCode === 200 || res.statusCode === 403) { calipso.theme.render(req, res, function(err, content) { @@ -300,7 +302,8 @@ function doResponse(req, res, next) { res.setHeader('X-Powered-By', 'Calipso'); // render - res.send(content); + calipso.silly("Responding with rendered content"); + res.write(content); // Callback req.routeComplete(res); @@ -437,13 +440,15 @@ function loadModules(next) { // Read the modules in from the file system, sync is fine as we do it once on load. calipso.lib.fs.readdirSync(moduleBasePath).forEach(function(type) { - if (type != "README" && type != '.DS_Store') { // Ignore the readme file and .DS_Store file for Macs + // Check for all files or folder starting with "." so that we can handle ".svn", ".git" and so on without problems. + + if (type != "README" && type[0] != '.') { // Ignore the readme file and .DS_Store file for Macs calipso.lib.fs.readdirSync(path.join(moduleBasePath, type)).forEach(function(moduleFolderName) { - if (moduleFolderName != "README" && moduleFolderName != '.DS_Store') { // Ignore the readme file and .DS_Store file for Macs - + if (moduleFolderName != "README" && moduleFolderName[0] != '.') { // Ignore the readme file and .DS_Store file for Macs + var modulePath = path.join(moduleBasePath, type, moduleFolderName); - + var module = { name: moduleFolderName, folder: moduleFolderName, @@ -458,7 +463,7 @@ function loadModules(next) { loadAbout(module, modulePath, 'package.json'); // Set the module name to what is in the package.json, default to folder name - module.name = module.about.name ? module.about.name : moduleFoldername; + module.name = (module.about && module.about.name) ? module.about.name : moduleFolderName; // Now set the module calipso.modules[module.name] = module; @@ -505,7 +510,7 @@ function loadAbout(obj, fromPath, file) { var packageFile = calipso.lib.path.join(fromPath, file); - if (path.existsSync(packageFile)) { + if ((fs.existsSync || path.existsSync)(packageFile)) { var json = fs.readFileSync(packageFile); try { obj.about = JSON.parse(json.toString()); @@ -536,7 +541,7 @@ function attachModuleEventsAndDependencies() { // Register dependencies registerModuleDependencies(calipso.modules[module]); - + // Attach event listener calipso.event.addModuleEventListener(calipso.modules[module], options); @@ -765,7 +770,7 @@ function loadModuleTemplates(module, moduleTemplatePath) { // Default the template to any loaded in the theme (overrides) var fs = calipso.lib.fs; - if (!calipso.lib.path.existsSync(moduleTemplatePath)) { + if (!(fs.existsSync || calipso.lib.path.existsSync)(moduleTemplatePath)) { return null; } diff --git a/lib/core/Themes.js b/lib/core/Themes.js index 86dc53a79..b26b8ca20 100644 --- a/lib/core/Themes.js +++ b/lib/core/Themes.js @@ -195,6 +195,15 @@ function processSection(req, res, section, sectionPath, layoutConfig, theme, nex sectionCache = theme.cache["404"]; } + // Override with a 403 (no permissions) page + if(section === "body" && res.statusCode === 403) { + if(!theme.cache.hasOwnProperty("403")) { + localNext(new Error("You must define a 403 template in the error folder e.g. error/403.html")); + return; + } + sectionCache = theme.cache["403"]; + } + // Override with a 500 (error) page if (section === "body" && res.statusCode === 500) { if (!theme.cache.hasOwnProperty("500")) { @@ -392,8 +401,8 @@ function loadTheme(theme, themePath, next) { var themeFile = calipso.lib.path.join(themePath, "theme.json"); - path.exists(themeFile, function(exists) { - if (exists) { + (fs.exists || path.exists)(themeFile, function(exists) { + if(exists) { fs.readFile(themeFile, 'utf8', function(err, data) { if (!err) { var jsonData; @@ -529,7 +538,7 @@ function loadTemplate(templateCache, template, themePath, next) { templateExtension = template.templatePath.match(/([^\.]+)$/)[0], templateFnPath = calipso.lib.path.join(themePath, template.templatePath.replace("." + templateExtension, ".js")); - path.exists(templatePath, function(exists) { + (fs.exists || path.exists)(templatePath,function(exists) { if (exists) { @@ -563,7 +572,7 @@ function loadTemplate(templateCache, template, themePath, next) { templateCache[template.name].template = compileTemplate(templateData, templatePath, templateExtension); // See if we have a template fn - if (path.existsSync(templateFnPath)) { + if ((fs.existsSync || path.existsSync)(templateFnPath)) { if (exists) { try { @@ -571,6 +580,7 @@ function loadTemplate(templateCache, template, themePath, next) { } catch (ex) { calipso.error(ex); } + } } diff --git a/modules/core/content/content.js b/modules/core/content/content.js index ddd4baafa..55d2b73f4 100644 --- a/modules/core/content/content.js +++ b/modules/core/content/content.js @@ -650,7 +650,7 @@ function updateContent(req,res,template,block,next) { function showAliasedContent(req, res, template, block, next) { var allowedFormats = ["html","json"]; - var format = req.moduleParams.format; + var format = req.moduleParams.format ? req.moduleParams.format : 'html'; var alias = req.moduleParams.alias; // Check type @@ -704,7 +704,6 @@ function showContentByID(req,res,template,block,next) { var format = req.moduleParams.format ? req.moduleParams.format : 'html'; Content.findById(id, function(err, content) { - // Error locating content if(err) { res.statusCode = 500; @@ -715,7 +714,6 @@ function showContentByID(req,res,template,block,next) { // Content found if(content) { - calipso.modules.user.fn.userDisplay(req,content.author,function(err, userDetails) { if(err) { next(err); @@ -742,7 +740,6 @@ function showContentByID(req,res,template,block,next) { * Show content - called by ID or Alias functions preceeding */ function showContent(req,res,template,block,next,err,content,format) { - if(err || !content) { content = {title:"Not Found!",content:"Sorry, I couldn't find that content!",displayAuthor:{name:"Unknown"}}; @@ -774,7 +771,6 @@ function showContent(req,res,template,block,next,err,content,format) { template = calipso.theme.cache.contentTypes[content.contentType] && calipso.theme.cache.contentTypes[content.contentType].view ? calipso.theme.cache.contentTypes[content.contentType].view : template; } - calipso.theme.renderItem(req,res,template,block,{content:content.toObject()},next); } diff --git a/modules/core/scheduler/scheduler.cron.js b/modules/core/scheduler/scheduler.cron.js index 33e7656d6..34f71f559 100644 --- a/modules/core/scheduler/scheduler.cron.js +++ b/modules/core/scheduler/scheduler.cron.js @@ -16,11 +16,15 @@ * - http://www.opensource.org/licenses/mit-license.php * - http://www.gnu.org/copyleft/gpl.html */ - +var sys; +try { + sys = require('util'); +} catch (e) { + sys = require('sys'); +} var rootpath = process.cwd() + '/', path = require('path'), - calipso = require(path.join(rootpath, 'lib/calipso')), - sys = require("sys"); + calipso = require(path.join(rootpath, 'lib/calipso')); function CronTime(time) { diff --git a/package.json b/package.json index 68f157cca..86c3470ee 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "node": "0.6.x || 0.7.x" }, "dependencies": { - "express": "2.5.x", - "connect": "1.8.x", + "express": "3.0.x", + "connect": "2.0.x", "ejs": "0.6.x", "jade": "0.20.x", "stylus": "0.23.x", @@ -34,7 +34,8 @@ "zipfile":"0.3.x", "rimraf":"2.0.x", "nconf":"0.5.x", - "async": "0.1.x" + "async": "0.1.x", + "hook.io": "0.8.x" }, "devDependencies": { "mocha": "*", diff --git a/test/config.test.js b/test/config.test.js new file mode 100644 index 000000000..175f38845 --- /dev/null +++ b/test/config.test.js @@ -0,0 +1,92 @@ +/** + * Sanity test + * Test must be executed serially as it runs + * against an app instance, this is typically executed by the make file + **/ + +var rootpath = process.cwd() + '/', + fs = require('fs'), + exec = require('child_process').exec, + path = require('path'), + assert = require('assert'), + sys = require('sys'), + should = require('should'), + Config = require(rootpath + 'lib/Config').Config; + +var defaultConfig = { + "test":"test" +}; + +/** + * Tests + */ +exports['I can create a development configuration'] = function() { + + mkdir(path.join(rootpath,'tmp'), function() { + fs.writeFileSync(path.join(rootpath,'tmp','default.json'),JSON.stringify(defaultConfig)); + var conf = new Config({env:'development',path:path.join(rootpath,'tmp')}); + conf.type.should.equal('file'); + conf.file.should.equal(path.join(rootpath,'tmp','development.json')); + }); + +}; + +exports['I can add and retrieve configuration'] = function() { + + var conf = new Config({env:'development',path:path.join(rootpath,'tmp')}); + conf.set('test:v1','v1'); + conf.get('test:v1').should.equal('v1'); + conf.set('test:v2','v2'); + conf.get('test:v2').should.equal('v2'); + + var test = conf.get('test'); + test.v1.should.equal('v1'); + test.v2.should.equal('v2'); + +}; + +exports['I can use different environments'] = function() { + + var confDev = new Config({env:'development',path:path.join(rootpath,'tmp')}); + var confTest = new Config({env:'test',path:path.join(rootpath,'tmp')}); + + confDev.set('test:v1','v1'); + confDev.get('test:v1').should.equal('v1'); + confDev.save(function(err) { + (fs.existsSync || path.existsSync)(confDev.file); + }); + + confTest.set('test:v1','v1'); + confTest.get('test:v1').should.equal('v1'); + confTest.save(function(err) { + (fs.existsSync || path.existsSync)(confTest.file); + }); + +}; + +exports['I can use the setSave shortcut'] = function() { + + var conf = new Config({path:path.join(rootpath,'tmp')}); + conf.setSave('test:setsave','Yah!',function(err) { + var confTest = new Config({path:path.join(rootpath,'tmp')}); + confTest.get('test:setsave').should.equal('Yah!'); + }); + +}; + + +/** + * Mkdir -p. + * + * TODO - these are unix only ... + * + * @param {String} path + * @param {Function} fn + */ + +function mkdir(path, fn) { + exec('mkdir -p ' + path, function(err){ + if (err) throw err; + fn && fn(); + }); +} diff --git a/test/lib.calipso.js b/test/lib.calipso.js index 74b9e2df9..d5bb20e01 100644 --- a/test/lib.calipso.js +++ b/test/lib.calipso.js @@ -21,15 +21,15 @@ describe('Calipso', function(){ }); describe('Basic loading and initialisation', function(){ - - it('Calipso has loaded successfully', function(done) { + + it('Calipso has loaded successfully', function(done) { calipso.loaded.should.equal(true); calipso.modules.should.exist; done(); }); - it('I can send a mock request through.', function(done) { - + it('I can send a mock request through.', function(done) { + var req = calipsoHelper.requests.testUser, res = calipsoHelper.response, response = 0, @@ -51,8 +51,8 @@ describe('Calipso', function(){ }); - it('I can send a mock request through to an invalid route and get a 404', function(done) { - + it('I can send a mock request through to an invalid route and get a 404', function(done) { + var req = calipsoHelper.requests.testUser, res = calipsoHelper.response, response = 0, @@ -75,8 +75,8 @@ describe('Calipso', function(){ }); - it('I can send a mock admin request through as an admin user', function(done) { - + it('I can send a mock admin request through as an admin user', function(done) { + var req = calipsoHelper.requests.adminUser, res = calipsoHelper.response, response = 0, @@ -89,20 +89,18 @@ describe('Calipso', function(){ // Over ride the res.end and increment our counter res.send = function(content) { responseContent = content; - response++; + response++; } - - routeFn(req, res, function(err) { + routeFn(req, res, function(err) { response.should.equal(1); res.outputStack.should.eql(['module_first','module_a','module_last']); - responseContent.should.include('world'); + responseContent.should.match(/world/); done(); }) - }); - it('I can send a mock admin request through and fail as a test user.', function(done) { - + it('I can send a mock admin request through and fail as a test user.', function(done) { + var req = calipsoHelper.requests.anonUser, res = calipsoHelper.response, response = 0, @@ -111,12 +109,12 @@ describe('Calipso', function(){ req.url = '/secured'; res.outputStack = []; - routeFn(req, res, function(err) { + routeFn(req, res, function(err) { res.outputStack.should.eql(['module_first','module_last']); req.flashMsgs[0].type.should.equal('error'); res.redirectQueue.length.should.equal(1); res.redirectQueue[0].should.equal('/'); - response.should.equal(0); // Don't match + response.should.equal(0); // Don't match done(); }) @@ -129,7 +127,7 @@ describe('Calipso', function(){ }); }); - }); + }); after(function() { try { fs.unlinkSync(mochaConfig); } catch(ex) { /** ignore **/ } diff --git a/test/lib.client.client.js b/test/lib.client.client.js index eee76029d..b72a0eff7 100644 --- a/test/lib.client.client.js +++ b/test/lib.client.client.js @@ -12,12 +12,12 @@ var should = require('should'), describe('Client', function(){ before(function(){ - // + // }); describe('Adding scripts', function(){ - - it('I can add a basic client script', function(done){ + + it('I can add a basic client script', function(done){ var client = new Client(), script = {name:'test',url:'/test.js',weight:10}; client.addScript(script); client.scripts.length.should.equal(1); @@ -25,7 +25,7 @@ describe('Client', function(){ done(); }); - it('If there is no name, it assigns the url as the name', function(done){ + it('If there is no name, it assigns the url as the name', function(done){ var client = new Client(), script = {url:'/test.js', weight:10}, scriptWithName = {url:'/test.js',weight:10, name:'/test.js'}; client.addScript(script); client.scripts.length.should.equal(1); @@ -33,7 +33,7 @@ describe('Client', function(){ done(); }); - it('I can add core scripts just by name', function(done){ + it('I can add core scripts just by name', function(done){ var client = new Client(), script = 'calipso', scriptWithName = {key:'calipso', url:'calipso.js',weight:-50, name:'calipso.js'}; client.addScript(script); client.scripts.length.should.equal(1); @@ -41,7 +41,7 @@ describe('Client', function(){ done(); }); - it('I can add a script just by name', function(done){ + it('I can add a script just by name', function(done){ var client = new Client(), script = '/myscript.js', scriptWithName = {url:'/myscript.js',weight:0, name:'/myscript.js'}; client.addScript(script); client.scripts.length.should.equal(1); @@ -50,9 +50,9 @@ describe('Client', function(){ }); - it('Adding a script twice does not duplicate it', function(done){ + it('Adding a script twice does not duplicate it', function(done){ var client = new Client(), script = {name:'test',url:'/test.js',weight:10}; - + client.addScript(script); client.addScript(script); @@ -63,11 +63,11 @@ describe('Client', function(){ }); - }); + }); describe('Adding styles', function(){ - - it('I can add a basic client style', function(done){ + + it('I can add a basic client style', function(done){ var client = new Client(), style = {name:'test',url:'/test.css',weight:10}; client.addStyle(style); client.styles.length.should.equal(1); @@ -75,7 +75,7 @@ describe('Client', function(){ done(); }); - it('If there is no name, it assigns the url as the name', function(done){ + it('If there is no name, it assigns the url as the name', function(done){ var client = new Client(), style = {url:'/test.css', weight:10}, styleWithName = {url:'/test.css',weight:10, name:'/test.css'}; client.addStyle(style); client.styles.length.should.equal(1); @@ -83,7 +83,7 @@ describe('Client', function(){ done(); }); - it('I can add a style by just passing in a url', function(done){ + it('I can add a style by just passing in a url', function(done){ var client = new Client(), style = '/test.css', styleWithName = {url:'/test.css',weight:0, name:'/test.css'}; client.addStyle(style); client.styles.length.should.equal(1); @@ -92,37 +92,37 @@ describe('Client', function(){ }); - }); + }); describe('Listing scripts and styles', function(){ - it('Listing scripts satisfies the ordering defined', function(done){ - var client = new Client(), + it('Listing scripts satisfies the ordering defined', function(done){ + var client = new Client(), s1 = {url:'/test1.js', weight:10}, s2 = {url:'/test2.js', weight:-10}; - + client.addScript(s1); client.addScript(s2); client.listScripts(function(err, scripts) { should.not.exist(err); - scripts.should.include('\r\n