Просмотр исходного кода

Merge pull request #32 from crowi/v1.2.0

V1.2.0
Sotaro KARASAWA 10 лет назад
Родитель
Сommit
4c9684a3c9

+ 5 - 0
CHANGES.md

@@ -1,6 +1,11 @@
 CHANGES
 ========
 
+## 1.2.0
+
+* Re-writed application structure.
+* Add unit test for page model. (Thank you: @shinnya)
+
 ## 1.1.3
 
 * Fix: Error occured on editting when the path includes multibyte string. (Thanks @shinnya)

+ 0 - 148
Gruntfile.js

@@ -1,148 +0,0 @@
-/*
- * @package Crowi
- */
-
-module.exports = function(grunt) {
-
-  var paths = {
-        scripts: ['Gruntfile.js', 'app.js', 'lib/**/*.js', 'resource/js/**/*.js'],
-        tests: ['test/**/*.test.js'],
-        styles: ['resource/css/*.scss'],
-        all: []
-      };
-
-  Object.keys(paths).forEach(function(name) {
-    paths[name].forEach(function(path) {
-      paths.all[paths.all.length] = path;
-    });
-  });
-
-  // Project configuration.
-  grunt.initConfig({
-    pkg: grunt.file.readJSON('package.json'),
-    dirs: {
-      js: 'resource/js',
-      jsDest: 'public/js',
-      css: 'resource/css',
-      cssDest: 'public/css',
-      web: 'public/'
-    },
-    sass: {
-      dev: {
-        options: {
-          outputStyle: 'nested',
-          includePaths: [
-            'bower_components/bootstrap-sass-official/assets/stylesheets',
-            'bower_components/fontawesome/scss',
-            'bower_components/reveal.js/css'
-          ]
-        },
-        files: {
-          '<%= dirs.cssDest %>/<%= pkg.name %>-main.css': '<%= dirs.css %>/<%= pkg.name %>.scss',
-          '<%= dirs.cssDest %>/<%= pkg.name %>-reveal.css': '<%= dirs.css %>/<%= pkg.name %>-reveal.scss'
-        }
-      },
-      default: {
-        options: {
-          outputStyle: 'compressed',
-          includePaths: [
-            'bower_components/bootstrap-sass-official/assets/stylesheets',
-            'bower_components/fontawesome/scss',
-            'bower_components/reveal.js/css'
-          ]
-        },
-        files: {
-          '<%= dirs.cssDest %>/<%= pkg.name %>-main.min.css': '<%= dirs.css %>/<%= pkg.name %>.scss',
-          '<%= dirs.cssDest %>/<%= pkg.name %>-reveal.min.css': '<%= dirs.css %>/<%= pkg.name %>-reveal.scss'
-        }
-      }
-    },
-    concat: {
-      dist: {
-        files: {
-          '<%= dirs.cssDest %>/<%= pkg.name %>.css': [
-            'bower_components/highlightjs/styles/tomorrow-night.css',
-            '<%= dirs.cssDest %>/<%= pkg.name %>-main.css',
-          ],
-          '<%= dirs.cssDest %>/<%= pkg.name %>.min.css': [
-            'bower_components/highlightjs/styles/tomorrow-night.css', // TODO minimize
-            '<%= dirs.cssDest %>/<%= pkg.name %>-main.min.css',
-          ],
-          '<%= dirs.jsDest %>/<%= pkg.name %>.js': [
-            'bower_components/jquery/dist/jquery.js',
-            'bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js',
-            'node_modules/socket.io-client/socket.io.js',
-            'bower_components/marked/lib/marked.js',
-            'bower_components/jquery.cookie/jquery.cookie.js',
-            'bower_components/highlightjs/highlight.pack.js',
-            'resource/js/crowi.js'
-          ],
-          '<%= dirs.jsDest %>/<%= pkg.name %>-reveal.js': [
-            'bower_components/jquery/dist/jquery.js',
-            'bower_components/reveal.js/lib/js/head.min.js',
-            'bower_components/reveal.js/lib/js/html5shiv.js',
-            'bower_components/reveal.js/js/reveal.js'
-          ],
-        }
-      },
-    },
-    uglify: {
-      build: {
-        files: {
-          '<%= dirs.jsDest %>/<%= pkg.name %>.min.js': '<%= dirs.jsDest %>/<%= pkg.name %>.js',
-          '<%= dirs.jsDest %>/<%= pkg.name %>-reveal.min.js': '<%= dirs.jsDest %>/<%= pkg.name %>-reveal.js'
-        }
-      }
-    },
-    jshint: {
-      options: {
-        jshintrc: true
-      },
-      all: paths.scripts
-    },
-    mochaTest: {
-      all: {
-        src: ['test/**/*.test.js'],
-        options: {
-          globals: ['chai'],
-          require: ['test/bootstrap.js'],
-          timeout: 3000,
-          quiet: false,
-          clearRequireCache: true,
-        },
-      }
-    },
-    watch: {
-      css: {
-        files: paths.styles,
-        tasks: ['sass'],
-      },
-      dev: {
-        files: paths.all,
-        tasks: ['dev'],
-      },
-      test: {
-        files: paths.all,
-        tasks: ['test'],
-      },
-      default: {
-        files: paths.all,
-        tasks: ['default'],
-      },
-    },
-  });
-
-
-  grunt.loadNpmTasks('grunt-contrib-uglify');
-  grunt.loadNpmTasks('grunt-contrib-watch');
-  grunt.loadNpmTasks('grunt-contrib-concat');
-  grunt.loadNpmTasks('grunt-contrib-jshint');
-  grunt.loadNpmTasks('grunt-sass');
-  grunt.loadNpmTasks('grunt-mocha-test');
-
-  // grunt watch dev
-  grunt.registerTask('default', ['sass', 'concat', 'uglify']);
-  grunt.registerTask('dev', ['sass:dev', 'concat', 'jshint']);
-  grunt.registerTask('test', ['mochaTest']);
-
-};

+ 5 - 198
app.js

@@ -5,203 +5,10 @@
  * @author  Sotaro KARASAWA <sotarok@crocos.co.jp>
  */
 
-var express  = require('express')
-  , bodyParser = require('body-parser')
-  , multer   = require('multer')
-  , morgan   = require('morgan')
-  , cookieParser = require('cookie-parser')
-  , methodOverride = require('method-override')
-  , errorHandler = require('errorhandler')
-  , cons     = require('consolidate')
-  , swig     = require('swig')
-  , flash    = require('connect-flash')
-  , http     = require('http')
-  , facebook = require('facebook-node-sdk')
-  , mongo    = require('mongoose')
-  , middleware = require('./lib/util/middlewares')
-  , time     = require('time')
-  , async    = require('async')
-  , session  = require('express-session')
-  , basicAuth = require('basic-auth-connect')
-  , debug    = require('debug')('crowi:appjs')
-  , models
-  , config
-  , configModel
-  , server
-  , sessionConfig
-  , RedisStore
-  ;
+var crowi = new (require('./lib/crowi'))(__dirname, process.env);
 
-time.tzset('Asia/Tokyo');
-
-var app = express();
-var env = app.get('env');
-var days = (1000*3600*24*30);
-
-// mongoUri = mongodb://user:password@host/dbname
-var mongoUri = process.env.MONGOLAB_URI ||
-  process.env.MONGOHQ_URL ||
-  process.env.MONGO_URI ||
-  'mongodb://localhost/crowi'
-  ;
-
-mongo.connect(mongoUri);
-
-sessionConfig = {
-  rolling: true,
-  secret: process.env.SECRET_TOKEN || 'this is default session secret',
-  resave: false,
-  saveUninitialized: true,
-  cookie: {
-    maxAge: days,
-  },
-};
-var redisUrl = process.env.REDISTOGO_URL ||
-  process.env.REDIS_URL ||
-  null;
-
-if (redisUrl) {
-  var ru   = require('url').parse(redisUrl);
-  var redis = require('redis');
-  var redisClient = redis.createClient(ru.port, ru.hostname);
-  if (ru.auth) {
-    redisClient.auth(ru.auth.split(':')[1]);
-  }
-
-  RedisStore = require('connect-redis')(session);
-  sessionConfig.store = new RedisStore({
-    prefix: 'crowi:sess:',
-    client: redisClient,
-  });
-}
-
-app.set('port', process.env.PORT || 3000);
-app.use(express.static(__dirname + '/public'));
-app.engine('html', cons.swig);
-app.set('view cache', false);
-app.set('view engine', 'html');
-app.set('views', __dirname + '/lib/views');
-app.use(methodOverride());
-app.use(bodyParser.urlencoded({ extended: true }));
-app.use(bodyParser.json());
-app.use(cookieParser());
-app.use(multer());
-app.use(session(sessionConfig));
-app.use(flash());
-
-configModel = require('./lib/models/config')(app);
-
-async.series([
-  function (next) {
-    configModel.loadAllConfig(function(err, doc) {
-      app.set('config', doc);
-
-      return next();
-    });
-  }, function (next) {
-    var config = app.set('config')
-      , tzoffset
-      ;
-
-
-    app.set('mailer', require('./lib/util/mailer')(app));
-
-    models = require('./lib/models')(app);
-    models.Config = configModel;
-
-    // configure application
-    app.use(function(req, res, next) {
-      var now = new Date()
-        , fbparams = {}
-        , baseUrl
-        , config = app.set('config');
-
-      tzoffset = -(config.crowi['app:timezone'] || 9) * 60; // for datez
-      app.set('tzoffset', tzoffset);
-
-      req.config = config;
-
-      config.crowi['app:url'] = baseUrl = (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');
-
-      res.locals.req      = req;
-      res.locals.baseUrl  = baseUrl;
-      res.locals.config   = config;
-      res.locals.env      = app.get('env');
-      res.locals.now      = now;
-      res.locals.tzoffset = tzoffset;
-      res.locals.facebook = {appId: config.crowi['facebook:appId'] || ''};
-      res.locals.consts   = {
-          pageGrants: models.Page.getGrantLabels(),
-          userStatus: models.User.getUserStatusLabels(),
-          registrationMode: models.Config.getRegistrationModeLabels(),
-      };
-
-      next();
-    });
-
-    app.use(function(req, res, next) {
-      if (config.crowi['security:basicName'] && config.crowi['security:basicSecret']) {
-        return basicAuth(
-          config.crowi['security:basicName'],
-          config.crowi['security:basicSecret'])(req, res, next);
-      } else {
-        next();
-      }
-    });
-
-    app.use(function(req, res, next) {
-      var config = app.set('config');
-      if (config.crowi['facebook:appId'] && config.crowi['facebook:secret']) {
-        return facebook.middleware({
-          appId: config.crowi['facebook:appId'],
-          secret: config.crowi['facebook:secret']
-        })(req, res, next);
-      } else {
-        return next();
-      }
-    });
-
-    // register swig function
-    app.use(middleware.swigFilters(app, swig));
-    app.use(middleware.swigFunctions(app));
-
-    app.use(middleware.loginChecker(app, models));
-
-    next();
-  }, function(next) {
-
-    if (env == 'development') {
-      swig.setDefaults({ cache: false });
-      app.use(errorHandler({ dumpExceptions: true, showStack: true }));
-      app.use(morgan('dev'));
-
-      server = http.createServer(app).listen(app.get('port'), function(){
-        console.log('[' + app.get('env') + '] Express server listening on port ' + app.get('port'));
-      });
-    }
-
-    if (env == 'production') {
-      var oneYear = 31557600000;
-      app.use(morgan('combined'));
-      app.use(function (err, req, res, next) {
-        res.status(500);
-        res.render('500', { error: err });
-      });
-
-      server = http.createServer(app).listen(app.get('port'), function(){
-        console.log('[' + app.get('env') + '] Express server listening on port ' + app.get('port'));
-      });
-    }
-
-    require('./lib/routes')(app);
-
-    var io = require('socket.io')(server);
-    io.sockets.on('connection', function (socket) {
-    });
-
-    app.set('io', io);
-
-    next();
-  }
-]);
+crowi.init()
+  .then(function(app) {
+    crowi.start(app);
+  }).catch(crowi.exitOnError);
 

+ 1 - 1
bower.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi",
-  "version": "1.1.3",
+  "version": "1.2.0",
   "description": "Crocos' Wiki implementation in node.js",
   "authors": [
     "Sotaro KARASAWA <sotarok@crocos.co.jp>",

+ 141 - 0
gulpfile.js

@@ -0,0 +1,141 @@
+'use strict';
+
+var gulp   = require('gulp');
+var sass   = require('gulp-sass');
+var cssmin = require('gulp-cssmin');
+var mocha  = require('gulp-spawn-mocha');
+var concat = require('gulp-concat');
+var rename = require('gulp-rename');
+var uglify = require('gulp-uglify');
+var jshint = require('gulp-jshint');
+
+var stylish = require('jshint-stylish');
+
+var pkg = require('./package.json');
+
+
+var dirs = {
+  cssSrc: './resource/css',
+  cssDist: './public/css',
+  jsSrc: './resource/js',
+  jsDist: './public/js',
+};
+
+var tests = {
+  watch: ['test/**/*.test.js'],
+}
+
+var css = {
+  src: dirs.cssSrc + '/' + pkg.name + '.scss',
+  main: dirs.cssDist + '/crowi-main.css',
+  dist: dirs.cssDist + '/crowi.css',
+  watch: ['resource/css/*.scss'],
+};
+
+var js = {
+  src: [
+    'bower_components/jquery/dist/jquery.js',
+    'bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js',
+    'node_modules/socket.io-client/socket.io.js',
+    'bower_components/marked/lib/marked.js',
+    'bower_components/jquery.cookie/jquery.cookie.js',
+    'bower_components/highlightjs/highlight.pack.js',
+    'resource/js/crowi.js'
+  ],
+  dist: dirs.jsDist + '/crowi.js',
+  revealSrc: [
+    'bower_components/reveal.js/lib/js/head.min.js',
+    'bower_components/reveal.js/lib/js/html5shiv.js',
+    'bower_components/reveal.js/js/reveal.js'
+  ],
+  revealDist: dirs.jsDist + '/crowi-reveal.js',
+  clientWatch: ['resource/js/**/*.js'],
+  watch: ['test/**/*.test.js', 'app.js', 'lib/**/*.js'],
+  lint: ['app.js', 'lib/**/*.js'],
+  tests: tests.watch,
+};
+
+var cssIncludePaths = [
+  'bower_components/bootstrap-sass-official/assets/stylesheets',
+  'bower_components/fontawesome/scss',
+  'bower_components/reveal.js/css'
+];
+
+gulp.task('js:concat', function() {
+  gulp.src(js.revealSrc)
+    .pipe(concat('crowi-reveal.js'))
+    .pipe(gulp.dest(dirs.jsDist));
+
+  return gulp.src(js.src)
+    .pipe(concat('crowi.js'))
+    .pipe(gulp.dest(dirs.jsDist));
+});
+
+gulp.task('js:min', ['js:concat'], function() {
+  gulp.src(js.revealDist)
+    .pipe(uglify())
+    .pipe(rename({suffix: '.min'}))
+    .pipe(gulp.dest(dirs.jsDist));
+
+  return gulp.src(js.dist)
+    .pipe(uglify())
+    .pipe(rename({suffix: '.min'}))
+    .pipe(gulp.dest(dirs.jsDist));
+});
+
+gulp.task('jshint', function() {
+  return gulp.src(js.lint)
+    .pipe(jshint())
+    .pipe(jshint.reporter(stylish));
+});
+
+gulp.task('test', function() {
+  return gulp.src(js.tests)
+    .pipe(mocha({
+      r: 'test/bootstrap.js',
+      globals: ['chai'],
+      R: 'dot',
+    }));
+});
+
+gulp.task('css:sass', function() {
+  return gulp.src(css.src)
+    .pipe(sass({
+        outputStyle: 'nesed',
+        sourceComments: 'map',
+        includePaths: cssIncludePaths
+    }).on('error', sass.logError))
+    .pipe(rename({suffix: '-main'}))
+    .pipe(gulp.dest(dirs.cssDist));
+});
+
+gulp.task('css:concat', ['css:sass'], function() {
+  return gulp.src([css.main, 'bower_components/highlightjs/styles/tomorrow-night.css'])
+    .pipe(concat('crowi.css'))
+    .pipe(gulp.dest(dirs.cssDist))
+});
+
+gulp.task('css:min', ['css:concat'], function() {
+  return gulp.src(css.dist)
+    .pipe(cssmin())
+    .pipe(rename({suffix: '.min'}))
+    .pipe(gulp.dest(dirs.cssDist));
+});
+
+gulp.task('watch', function() {
+  var watchLogger = function(event) {
+    console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
+  };
+
+  var cssWatcher = gulp.watch(css.watch, ['css:concat']);
+  cssWatcher.on('change', watchLogger);
+  var jsWatcher = gulp.watch(js.clientWatch, ['js:concat']);
+  jsWatcher.on('change', watchLogger);
+  var testWatcher = gulp.watch(js.watch, ['test']);
+  testWatcher.on('change', watchLogger);
+});
+
+gulp.task('css', ['css:sass', 'css:concat',]);
+gulp.task('default', ['css:min', 'js:min',]);
+gulp.task('dev', ['css:concat', 'js:concat','jshint', 'test']);
+

+ 115 - 0
lib/crowi/express-init.js

@@ -0,0 +1,115 @@
+'use strict';
+
+module.exports = function(crowi, app) {
+  var express        = require('express')
+    , bodyParser     = require('body-parser')
+    , multer         = require('multer')
+    , morgan         = require('morgan')
+    , cookieParser   = require('cookie-parser')
+    , methodOverride = require('method-override')
+    , errorHandler   = require('errorhandler')
+    , session        = require('express-session')
+    , basicAuth      = require('basic-auth-connect')
+    , flash          = require('connect-flash')
+    , cons           = require('consolidate')
+    , swig           = require('swig')
+    , env            = crowi.node_env
+    , middleware     = require('../util/middlewares')
+    ;
+
+  app.use(function(req, res, next) {
+    var now = new Date()
+      , fbparams = {}
+      , baseUrl
+      , config = crowi.getConfig()
+      , tzoffset = -(config.crowi['app:timezone'] || 9) * 60 // for datez
+      , Page = crowi.model('Page')
+      , User = crowi.model('User')
+      , Config = crowi.model('Config')
+      ;
+
+    app.set('tzoffset', tzoffset);
+
+    req.config = config;
+
+    config.crowi['app:url'] = baseUrl = (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');
+
+    res.locals.req      = req;
+    res.locals.baseUrl  = baseUrl;
+    res.locals.config   = config;
+    res.locals.env      = env;
+    res.locals.now      = now;
+    res.locals.tzoffset = tzoffset;
+    res.locals.facebook = {appId: config.crowi['facebook:appId'] || ''};
+    res.locals.consts   = {
+        pageGrants: Page.getGrantLabels(),
+        userStatus: User.getUserStatusLabels(),
+        registrationMode: Config.getRegistrationModeLabels(),
+    };
+
+    next();
+  });
+
+  // Set basic auth middleware
+  app.use(function(req, res, next) {
+    var config = crowi.getConfig();
+
+    if (config.crowi['security:basicName'] && config.crowi['security:basicSecret']) {
+      return basicAuth(
+        config.crowi['security:basicName'],
+        config.crowi['security:basicSecret'])(req, res, next);
+    } else {
+      next();
+    }
+  });
+
+  // Register Facebook middleware
+  app.use(function(req, res, next) {
+    var config = crowi.getConfig()
+      , facebook = require('facebook-node-sdk')
+      ;
+
+    if (config.crowi['facebook:appId'] && config.crowi['facebook:secret']) {
+      return facebook.middleware({
+        appId: config.crowi['facebook:appId'],
+        secret: config.crowi['facebook:secret']
+      })(req, res, next);
+    } else {
+      return next();
+    }
+  });
+
+  app.set('port', crowi.port);
+  app.use(express.static(crowi.publicDir));
+  app.engine('html', cons.swig);
+  app.set('view cache', false);
+  app.set('view engine', 'html');
+  app.set('views', crowi.viewsDir);
+  app.use(methodOverride());
+  app.use(bodyParser.urlencoded({ extended: true }));
+  app.use(bodyParser.json());
+  app.use(cookieParser());
+  app.use(multer());
+  app.use(session(crowi.sessionConfig));
+  app.use(flash());
+
+  app.use(middleware.swigFilters(app, swig));
+  app.use(middleware.swigFunctions(crowi, app));
+
+  app.use(middleware.loginChecker(crowi, app));
+
+  if (env == 'development') {
+    swig.setDefaults({ cache: false });
+    app.use(errorHandler({ dumpExceptions: true, showStack: true }));
+    app.use(morgan('dev'));
+  }
+
+  if (env == 'production') {
+    var oneYear = 31557600000;
+    app.use(morgan('combined'));
+    app.use(function (err, req, res, next) {
+      res.status(500);
+      res.render('500', { error: err });
+    });
+  }
+};

+ 218 - 0
lib/crowi/index.js

@@ -0,0 +1,218 @@
+'use strict';
+
+
+var debug = require('debug')('crowi:crowi')
+  , pkg = require('../../package.json')
+  , path = require('path')
+  , sep = path.sep
+  , Promise = require('bluebird')
+
+  , http     = require('http')
+  , express  = require('express')
+
+  , mongoose    = require('mongoose')
+
+  , models = require('../models')
+  ;
+
+function Crowi (rootdir, env)
+{
+  var self = this;
+
+  this.version = pkg.version;
+
+  this.rootDir   = rootdir;
+  this.pluginDir = path.join(this.rootDir, 'node_modules') + sep;
+  this.publicDir = path.join(this.rootDir, 'public') + sep;
+  this.libDir    = path.join(this.rootDir, 'lib') + sep;
+  this.viewsDir  = path.join(this.libDir, 'views') + sep;
+  this.mailDir   = path.join(this.viewsDir, 'mail') + sep;
+
+  this.config = {};
+  this.mailer = {};
+
+  this.models = {};
+
+  this.env = env;
+  this.node_env = this.env.NODE_ENV || 'development';
+  this.port = this.env.PORT || 3000;
+
+  if (this.node_env == 'development') {
+    Promise.longStackTraces();
+  }
+
+  //time.tzset('Asia/Tokyo');
+};
+
+Crowi.prototype.init = function() {
+  var self = this;
+
+  return new Promise.resolve()
+  .then(function() {
+    // setup database server and load all modesl
+    return self.setupDatabase();
+  }).then(function() {
+    return self.setupModels();
+  }).then(function() {
+    return self.setupSessionConfig();
+  }).then(function() {
+    return new Promise(function(resolve, reject) {
+      self.model('Config', require('../models/config')(self));
+      var Config = self.model('Config');
+      Config.loadAllConfig(function(err, doc) {
+        if (err) {
+          return reject();
+        }
+        self.setConfig(doc);
+        return resolve();
+      });
+    });
+  }).then(function() {
+    return self.setupMailer();
+  }).then(function() {
+    return self.buildServer();
+  });
+}
+
+Crowi.prototype.setConfig = function(config) {
+  this.config = config;
+};
+
+Crowi.prototype.getConfig = function() {
+  return this.config;
+};
+
+// getter/setter of model instance
+//
+Crowi.prototype.model = function(name, model) {
+  if (model) {
+    return this.models[name] = model;
+  }
+
+  return this.models[name];
+};
+
+Crowi.prototype.setupDatabase = function() {
+  // mongoUri = mongodb://user:password@host/dbname
+  var mongoUri = this.env.MONGOLAB_URI ||
+    this.env.MONGOHQ_URL ||
+    this.env.MONGO_URI ||
+    'mongodb://localhost/crowi'
+    ;
+
+  return new Promise(function(resolve, reject) {
+    mongoose.connect(mongoUri, function(e) {
+      if (e) {
+        debug('DB Connect Error: ', mongoUri);
+        return reject(new Error('Cann\'t connect to Database Server.'));
+      }
+      return resolve();
+    });
+  });
+};
+
+Crowi.prototype.setupSessionConfig = function() {
+  var self = this
+    , session  = require('express-session')
+    , sessionConfig
+    , sessionAge = (1000*3600*24*30)
+    , redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URL || null
+    , RedisStore
+    ;
+
+  return new Promise(function(resolve, reject) {
+    sessionConfig = {
+      rolling: true,
+      secret: self.env.SECRET_TOKEN || 'this is default session secret',
+      resave: false,
+      saveUninitialized: true,
+      cookie: {
+        maxAge: sessionAge,
+      },
+    };
+
+    if (redisUrl) {
+      var ru   = require('url').parse(redisUrl);
+      var redis = require('redis');
+      var redisClient = redis.createClient(ru.port, ru.hostname);
+      if (ru.auth) {
+        redisClient.auth(ru.auth.split(':')[1]);
+      }
+
+      RedisStore = require('connect-redis')(session);
+      sessionConfig.store = new RedisStore({
+        prefix: 'crowi:sess:',
+        client: redisClient,
+      });
+    }
+
+    self.sessionConfig = sessionConfig;
+    resolve();
+  });
+};
+
+Crowi.prototype.setupModels = function() {
+  var self = this
+    ;
+
+  return new Promise(function(resolve, reject) {
+    Object.keys(models).forEach(function(key) {
+      self.model(key, models[key](self));
+    });
+    resolve();
+  });
+};
+
+Crowi.prototype.getIo = function() {
+  return this.io;
+};
+
+Crowi.prototype.getMailer = function() {
+  return this.mailer;
+};
+
+Crowi.prototype.setupMailer = function() {
+  var self = this;
+  return new Promise(function(resolve, reject) {
+    self.mailer = require('../util/mailer')(self);
+    resolve();
+  });
+};
+
+
+Crowi.prototype.start = function(app) {
+  var self = this
+    , server
+    , io;
+
+  server = http.createServer(app).listen(self.port, function() {
+    console.log('[' + self.node_env + '] Express server listening on port ' + self.port);
+  });
+
+  io = require('socket.io')(server);
+  io.sockets.on('connection', function (socket) {
+  });
+  this.io = io;
+};
+
+Crowi.prototype.buildServer = function() {
+  var app            = express()
+    , env            = this.node_env
+    , sessionConfig  = this.setupSessionConfig();
+    ;
+
+  require('./express-init')(this, app);
+  require('../routes')(this, app);
+
+  return new Promise.resolve(app);
+};
+
+Crowi.prototype.exitOnError = function(err) {
+  debug('Critical error occured.');
+  console.error(err);
+  console.error(err.stack);
+  process.exit(1);
+};
+
+
+module.exports = Crowi;

+ 4 - 6
lib/models/bookmark.js

@@ -1,6 +1,6 @@
-module.exports = function(app, models) {
-  var mongoose = require('mongoose')
-    , debug = require('debug')('crowi:models:bookmark')
+module.exports = function(crowi) {
+  var debug = require('debug')('crowi:models:bookmark')
+    , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , bookmarkSchema;
 
@@ -52,7 +52,5 @@ module.exports = function(app, models) {
     // To be implemented ...
   };
 
-  models.Bookmark = mongoose.model('Bookmark', bookmarkSchema);
-
-  return models.Bookmark;
+  return mongoose.model('Bookmark', bookmarkSchema);
 };

+ 3 - 3
lib/models/config.js

@@ -1,4 +1,4 @@
-module.exports = function(app) {
+module.exports = function(crowi) {
   var mongoose = require('mongoose')
     , debug = require('debug')('crowi:models:config')
     , ObjectId = mongoose.Schema.Types.ObjectId
@@ -58,7 +58,7 @@ module.exports = function(app) {
 
   configSchema.statics.updateConfigCache = function(ns, config)
   {
-    var originalConfig = app.set('config');
+    var originalConfig = crowi.getConfig();
     var newNSConfig = originalConfig[ns] || {};
     Object.keys(config).forEach(function (key) {
       if (config[key] || config[key] === '') {
@@ -67,7 +67,7 @@ module.exports = function(app) {
     });
 
     originalConfig[ns] = newNSConfig;
-    app.set('config', originalConfig);
+    crowi.setConfig(originalConfig);
   };
 
   // Execute only once for installing application

+ 5 - 11
lib/models/index.js

@@ -1,12 +1,6 @@
-module.exports = function(app) {
-  var models = {};
-
-  require('./page')(app, models);
-  require('./user')(app, models);
-  require('./revision')(app, models);
-  require('./bookmark')(app, models);
-
-  app.set('models', models);
-
-  return models;
+module.exports = {
+  Page: require('./page'),
+  User: require('./user'),
+  Revision: require('./revision'),
+  Bookmark: require('./bookmark'),
 };

+ 24 - 17
lib/models/page.js

@@ -1,6 +1,6 @@
-module.exports = function(app, models) {
-  var mongoose = require('mongoose')
-    , debug = require('debug')('crowi:models:page')
+module.exports = function(crowi) {
+  var debug = require('debug')('crowi:models:page')
+    , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , GRANT_PUBLIC = 1
     , GRANT_RESTRICTED = 2
@@ -10,11 +10,12 @@ module.exports = function(app, models) {
     , pageSchema;
 
   function populatePageData(pageData, revisionId, callback) {
+    var Page = crowi.model('Page');
+
+    pageData.latestRevision = pageData.revision;
     if (revisionId) {
       pageData.revision = revisionId;
     }
-
-    pageData.latestRevision = pageData.revision;
     pageData.likerCount = pageData.liker.length || 0;
     pageData.seenUsersCount = pageData.seenUsers.length || 0;
 
@@ -24,7 +25,7 @@ module.exports = function(app, models) {
       {path: 'liker', options: { limit: 11 }},
       {path: 'seenUsers', options: { limit: 11 }},
     ], function (err, pageData) {
-      models.Page.populate(pageData, {path: 'revision.author', model: 'User'}, callback);
+      Page.populate(pageData, {path: 'revision.author', model: 'User'}, callback);
     });
   }
 
@@ -92,7 +93,9 @@ module.exports = function(app, models) {
   };
 
   pageSchema.methods.like = function(userData, callback) {
-    var self = this;
+    var self = this,
+      Page = self;
+
     if (undefined === this.populated('liker')) {
       var added = this.liker.addToSet(userData._id);
       if (added.length > 0) {
@@ -105,7 +108,7 @@ module.exports = function(app, models) {
         return callback(null, this);
       }
     } else {
-      models.Page.update(
+      Page.update(
         {_id: self._id},
         { $addToSet: { liker:  userData._id }},
         function(err, numAffected, raw) {
@@ -117,7 +120,9 @@ module.exports = function(app, models) {
   };
 
   pageSchema.methods.unlike = function(userData, callback) {
-    var self = this;
+    var self = this,
+      Page = self;
+
     if (undefined === this.populated('liker')) {
       var removed = this.liker.pull(userData._id);
       if (removed.length > 0) {
@@ -130,7 +135,7 @@ module.exports = function(app, models) {
         callback(null, this);
       }
     } else {
-      models.Page.update(
+      Page.update(
         {_id: self._id},
         { $pull: { liker:  userData._id }},
         function(err, numAffected, raw) {
@@ -142,7 +147,9 @@ module.exports = function(app, models) {
   };
 
   pageSchema.methods.seen = function(userData, callback) {
-    var self = this;
+    var self = this,
+      Page = self;
+
     if (undefined === this.populated('seenUsers')) {
       var added = this.seenUsers.addToSet(userData._id);
       if (added.length > 0) {
@@ -155,7 +162,7 @@ module.exports = function(app, models) {
         return callback(null, this);
       }
     } else {
-      models.Page.update(
+      Page.update(
         {_id: self._id},
         { $addToSet: { seenUsers:  userData._id }},
         function(err, numAffected, raw) {
@@ -356,6 +363,7 @@ module.exports = function(app, models) {
 
   pageSchema.statics.create = function(path, body, user, options, cb) {
     var Page = this
+      , Revision = crowi.model('Revision')
       , format = options.format || 'markdown'
       , redirectTo = options.redirectTo || null;
 
@@ -373,7 +381,7 @@ module.exports = function(app, models) {
       newPage.redirectTo = redirectTo;
       newPage.save(function (err, newPage) {
 
-        var newRevision = models.Revision.prepareRevision(newPage, body, user, {format: format});
+        var newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
         Page.pushRevision(newPage, newRevision, user, function(err, data) {
           if (err) {
             console.log('Push Revision Error on create page', err);
@@ -387,6 +395,7 @@ module.exports = function(app, models) {
 
   pageSchema.statics.rename = function(pageData, newPageName, user, options, cb) {
     var Page = this
+      , Revision = crowi.model('Revision')
       , path = pageData.path
       , createRedirectPage = options.createRedirectPage || 0
       , moveUnderTrees     = options.moveUnderTrees || 0;
@@ -398,7 +407,7 @@ module.exports = function(app, models) {
       }
 
       // reivisions の path を変更
-      models.Revision.updateRevisionListByPath(path, {path: newPageName}, {}, function(err, data) {
+      Revision.updateRevisionListByPath(path, {path: newPageName}, {}, function(err, data) {
         if (err) {
           return cb(err, null);
         }
@@ -427,7 +436,5 @@ module.exports = function(app, models) {
   pageSchema.statics.GRANT_OWNER = GRANT_OWNER;
   pageSchema.statics.PAGE_GRANT_ERROR = PAGE_GRANT_ERROR;
 
-  models.Page = mongoose.model('Page', pageSchema);
-
-  return models.Page;
+  return mongoose.model('Page', pageSchema);
 };

+ 4 - 5
lib/models/revision.js

@@ -1,5 +1,6 @@
-module.exports = function(app, models) {
-  var mongoose = require('mongoose')
+module.exports = function(crowi) {
+  var debug = require('debug')('crowi:models:revision')
+    , mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , revisionSchema;
 
@@ -68,7 +69,5 @@ module.exports = function(app, models) {
   revisionSchema.statics.updatePath = function(pathName) {
   };
 
-  models.Revision = mongoose.model('Revision', revisionSchema);
-
-  return models.Revision;
+  return mongoose.model('Revision', revisionSchema);
 };

+ 13 - 11
lib/models/user.js

@@ -1,12 +1,10 @@
-module.exports = function(app, models) {
-  var mongoose = require('mongoose')
+module.exports = function(crowi) {
+  var debug = require('debug')('crowi:models:user')
+    , mongoose = require('mongoose')
     , mongoosePaginate = require('mongoose-paginate')
-    , debug = require('debug')('crowi:models:user')
     , crypto = require('crypto')
     , async = require('async')
-    , config = app.set('config')
     , ObjectId = mongoose.Schema.Types.ObjectId
-    , mailer = app.set('mailer')
 
     , STATUS_REGISTERED = 1
     , STATUS_ACTIVE     = 2
@@ -34,7 +32,8 @@ module.exports = function(app, models) {
   userSchema.plugin(mongoosePaginate);
 
   function decideUserStatusOnRegistration () {
-    var Config = models.Config;
+    var Config = crowi.model('Config'),
+      config = crowi.getConfig();
 
     // status decided depends on registrationMode
     switch (config.crowi['security:registrationMode']) {
@@ -214,7 +213,9 @@ module.exports = function(app, models) {
   };
 
   userSchema.statics.isEmailValid = function(email, callback) {
-    var whitelist = config.crowi['security:registrationWhiteList'];
+    var config = crowi.getConfig()
+      , whitelist = config.crowi['security:registrationWhiteList'];
+
     if (Array.isArray(whitelist) && whitelist.length > 0) {
       return config.crowi['security:registrationWhiteList'].some(function(allowedEmail) {
         var re = new RegExp(allowedEmail + '$');
@@ -348,7 +349,10 @@ module.exports = function(app, models) {
 
   userSchema.statics.createUsersByInvitation = function(emailList, toSendEmail, callback) {
     var User = this
-      , createdUserList = [];
+      , createdUserList = []
+      , config = crowi.getConfig()
+      , mailer = crowi.getMailer()
+      ;
 
     if (!Array.isArray(emailList)) {
       debug('emailList is not array');
@@ -493,7 +497,5 @@ module.exports = function(app, models) {
   userSchema.statics.STATUS_DELETED = STATUS_DELETED;
   userSchema.statics.STATUS_INVITED = STATUS_INVITED;
 
-  models.User = mongoose.model('User', userSchema);
-
-  return models.User;
+  return mongoose.model('User', userSchema);
 };

+ 9 - 6
lib/routes/admin.js

@@ -1,8 +1,8 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:admin')
-    , models = app.set('models')
+    , models = crowi.models
     , Page = models.Page
     , User = models.User
     , Config = models.Config
@@ -203,6 +203,7 @@ module.exports = function(app) {
       // mail setting ならここで validation
       if (form['mail:from']) {
         validateMailSetting(req, form, function(err, data) {
+          debug('Error validate mail setting: ', err, data);
           if (err) {
             req.form.errors.push('SMTPを利用したテストメール送信に失敗しました。設定をみなおしてください。');
             return res.json({status: false, message: req.form.errors.join('\n')});
@@ -228,15 +229,17 @@ module.exports = function(app) {
 
   function validateMailSetting(req, form, callback)
   {
-    var mailer = app.set('mailer');
+    var mailer = crowi.mailer;
     var option = {
       host: form['mail:smtpHost'],
       port: form['mail:smtpPort'],
-      auth: {
+    };
+    if (form['mail:smtpUser'] && form['mail:smtpPassword']) {
+      option.auth = {
         user: form['mail:smtpUser'],
         pass: form['mail:smtpPassword'],
-      }
-    };
+      };
+    }
     if (option.port === 465) {
       option.secure = true;
     }

+ 47 - 45
lib/routes/index.js

@@ -1,15 +1,17 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   var middleware = require('../util/middlewares')
     , form      = require('../form')
-    , page      = require('./page')(app)
-    , login     = require('./login')(app)
-    , logout    = require('./logout')(app)
-    , me        = require('./me')(app)
-    , admin     = require('./admin')(app)
-    , installer = require('./installer')(app)
-    , user      = require('./user')(app);
+    , page      = require('./page')(crowi, app)
+    , login     = require('./login')(crowi, app)
+    , logout    = require('./logout')(crowi, app)
+    , me        = require('./me')(crowi, app)
+    , admin     = require('./admin')(crowi, app)
+    , installer = require('./installer')(crowi, app)
+    , user      = require('./user')(crowi, app)
+    , loginRequired = middleware.loginRequired
+    ;
 
-  app.get('/'                        , middleware.loginRequired(app) , page.pageListShow);
+  app.get('/'                        , loginRequired(crowi, app) , page.pageListShow);
 
   app.get('/installer'               , middleware.applicationNotInstalled() , installer.index);
   app.post('/installer/createAdmin'  , middleware.applicationNotInstalled() , form.register , installer.createAdmin);
@@ -28,50 +30,50 @@ module.exports = function(app) {
   app.get('/login/facebook'          , login.loginFacebook);
   app.get('/logout'                  , logout.logout);
 
-  app.get('/admin'                      , middleware.loginRequired(app) , middleware.adminRequired() , admin.index);
-  app.get('/admin/app'                  , middleware.loginRequired(app) , middleware.adminRequired() , admin.app.index);
-  app.post('/_api/admin/settings/app'   , middleware.loginRequired(app) , middleware.adminRequired() , form.admin.app, admin.api.appSetting);
-  app.post('/_api/admin/settings/sec'   , middleware.loginRequired(app) , middleware.adminRequired() , form.admin.sec, admin.api.appSetting);
-  app.post('/_api/admin/settings/mail'  , middleware.loginRequired(app) , middleware.adminRequired() , form.admin.mail, admin.api.appSetting);
-  app.post('/_api/admin/settings/aws'   , middleware.loginRequired(app) , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
-  app.post('/_api/admin/settings/google', middleware.loginRequired(app) , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
-  app.post('/_api/admin/settings/fb'    , middleware.loginRequired(app) , middleware.adminRequired() , form.admin.fb
+  app.get('/admin'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
+  app.get('/admin/app'                  , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
+  app.post('/_api/admin/settings/app'   , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.app, admin.api.appSetting);
+  app.post('/_api/admin/settings/sec'   , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.sec, admin.api.appSetting);
+  app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.mail, admin.api.appSetting);
+  app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
+  app.post('/_api/admin/settings/google', loginRequired(crowi, app) , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
+  app.post('/_api/admin/settings/fb'    , loginRequired(crowi, app) , middleware.adminRequired() , form.admin.fb
   , admin.api.appSetting);
 
-  app.get('/admin/users'                , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.index);
-  app.post('/admin/user/invite'         , form.admin.userInvite ,  middleware.loginRequired(app) , middleware.adminRequired() , admin.user.invite);
-  app.post('/admin/user/:id/makeAdmin'  , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.makeAdmin);
-  app.post('/admin/user/:id/removeFromAdmin', middleware.loginRequired(app) , middleware.adminRequired() , admin.user.removeFromAdmin);
-  app.post('/admin/user/:id/activate'   , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.activate);
-  app.post('/admin/user/:id/suspend'    , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.suspend);
-  app.post('/admin/user/:id/remove'     , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.remove);
-  app.post('/admin/user/:id/removeCompletely' , middleware.loginRequired(app) , middleware.adminRequired() , admin.user.removeCompletely);
+  app.get('/admin/users'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.index);
+  app.post('/admin/user/invite'         , form.admin.userInvite ,  loginRequired(crowi, app) , middleware.adminRequired() , admin.user.invite);
+  app.post('/admin/user/:id/makeAdmin'  , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.makeAdmin);
+  app.post('/admin/user/:id/removeFromAdmin', loginRequired(crowi, app) , middleware.adminRequired() , admin.user.removeFromAdmin);
+  app.post('/admin/user/:id/activate'   , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.activate);
+  app.post('/admin/user/:id/suspend'    , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.suspend);
+  app.post('/admin/user/:id/remove'     , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.remove);
+  app.post('/admin/user/:id/removeCompletely' , loginRequired(crowi, app) , middleware.adminRequired() , admin.user.removeCompletely);
 
-  app.get('/me'                      , middleware.loginRequired(app) , me.index);
-  app.get('/me/password'             , middleware.loginRequired(app) , me.password);
-  app.post('/me'                     , form.me.user               , middleware.loginRequired(app) , me.index);
-  app.post('/me/password'            , form.me.password           , middleware.loginRequired(app) , me.password);
-  app.post('/me/picture/delete'      , middleware.loginRequired(app) , me.deletePicture);
-  app.post('/me/auth/facebook'       , middleware.loginRequired(app) , me.authFacebook);
-  app.post('/me/auth/google'         , middleware.loginRequired(app) , me.authGoogle);
-  app.get('/me/auth/google/callback' , middleware.loginRequired(app) , me.authGoogleCallback);
+  app.get('/me'                      , loginRequired(crowi, app) , me.index);
+  app.get('/me/password'             , loginRequired(crowi, app) , me.password);
+  app.post('/me'                     , form.me.user               , loginRequired(crowi, app) , me.index);
+  app.post('/me/password'            , form.me.password           , loginRequired(crowi, app) , me.password);
+  app.post('/me/picture/delete'      , loginRequired(crowi, app) , me.deletePicture);
+  app.post('/me/auth/facebook'       , loginRequired(crowi, app) , me.authFacebook);
+  app.post('/me/auth/google'         , loginRequired(crowi, app) , me.authGoogle);
+  app.get('/me/auth/google/callback' , loginRequired(crowi, app) , me.authGoogleCallback);
 
-  app.get('/:id([0-9a-z]{24})'       , middleware.loginRequired(app) , page.api.redirector);
-  app.get('/_r/:id([0-9a-z]{24})'    , middleware.loginRequired(app) , page.api.redirector); // alias
+  app.get('/:id([0-9a-z]{24})'       , loginRequired(crowi, app) , page.api.redirector);
+  app.get('/_r/:id([0-9a-z]{24})'    , loginRequired(crowi, app) , page.api.redirector); // alias
   app.get('/_api/check_username'     , user.api.checkUsername);
-  app.post('/_api/me/picture/upload' , middleware.loginRequired(app) , me.api.uploadPicture);
-  app.get('/_api/user/bookmarks'     , middleware.loginRequired(app) , user.api.bookmarks);
-  app.post('/_api/page_rename/*'     , middleware.loginRequired(app) , page.api.rename);
-  app.post('/_api/page/:id/like'     , middleware.loginRequired(app) , page.api.like);
-  app.post('/_api/page/:id/unlike'   , middleware.loginRequired(app) , page.api.unlike);
-  app.get('/_api/page/:id/bookmark'  , middleware.loginRequired(app) , page.api.isBookmarked);
-  app.post('/_api/page/:id/bookmark' , middleware.loginRequired(app) , page.api.bookmark);
+  app.post('/_api/me/picture/upload' , loginRequired(crowi, app) , me.api.uploadPicture);
+  app.get('/_api/user/bookmarks'     , loginRequired(crowi, app) , user.api.bookmarks);
+  app.post('/_api/page_rename/*'     , loginRequired(crowi, app) , page.api.rename);
+  app.post('/_api/page/:id/like'     , loginRequired(crowi, app) , page.api.like);
+  app.post('/_api/page/:id/unlike'   , loginRequired(crowi, app) , page.api.unlike);
+  app.get('/_api/page/:id/bookmark'  , loginRequired(crowi, app) , page.api.isBookmarked);
+  app.post('/_api/page/:id/bookmark' , loginRequired(crowi, app) , page.api.bookmark);
   //app.get('/_api/page/*'           , user.useUserData()         , page.api.get);
   //app.get('/_api/revision/:id'     , user.useUserData()         , revision.api.get);
   //app.get('/_api/r/:revisionId'    , user.useUserData()         , page.api.get);
 
-  app.post('/*/edit'                 , form.revision              , middleware.loginRequired(app) , page.pageEdit);
-  app.get('/*/$'                     , middleware.loginRequired(app) , page.pageListShow);
-  app.get('/*'                       , middleware.loginRequired(app) , page.pageShow);
+  app.post('/*/edit'                 , form.revision              , loginRequired(crowi, app) , page.pageEdit);
+  app.get('/*/$'                     , loginRequired(crowi, app) , page.pageListShow);
+  app.get('/*'                       , loginRequired(crowi, app) , page.pageShow);
   //app.get('/*/edit'                , routes.edit);
 };

+ 2 - 2
lib/routes/installer.js

@@ -1,8 +1,8 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:installer')
-    , models = app.set('models')
+    , models = crowi.models
     , Config = models.Config
     , User = models.User
 

+ 10 - 11
lib/routes/login.js

@@ -1,16 +1,15 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var googleapis = require('googleapis')
     , debug = require('debug')('crowi:routes:login')
     , async    = require('async')
-    , models = app.set('models')
-    , config = app.set('config')
-    , mailer = app.set('mailer')
-    , Page = models.Page
-    , User = models.User
-    , Config = models.Config
-    , Revision = models.Revision
+    , config = crowi.getConfig()
+    , mailer = crowi.getMailer()
+    , Page = crowi.model('Page')
+    , User = crowi.model('User')
+    , Config = crowi.model('Config')
+    , Revision = crowi.model('Revision')
     , actions = {};
 
   var loginSuccess = function(req, res, userData) {
@@ -83,7 +82,7 @@ module.exports = function(app) {
   };
 
   actions.loginGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(app);
+    var googleAuth = require('../util/googleAuth')(config);
     var code = req.session.googleAuthCode || null;
 
     if (!code) {
@@ -135,7 +134,7 @@ module.exports = function(app) {
   };
 
   actions.register = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(app);
+    var googleAuth = require('../util/googleAuth')(config);
 
     // ログイン済みならさようなら
     if (req.user) {
@@ -270,7 +269,7 @@ module.exports = function(app) {
   };
 
   actions.registerGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(app);
+    var googleAuth = require('../util/googleAuth')(config);
     googleAuth.createAuthUrl(req, function(err, redirectUrl) {
       if (err) {
         // TODO

+ 1 - 1
lib/routes/logout.js

@@ -1,4 +1,4 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   return {
     logout: function(req, res) {
 

+ 20 - 6
lib/routes/me.js

@@ -1,19 +1,25 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:me')
     , fs = require('fs')
-    , models = app.set('models')
+    , models = crowi.models
+    , config = crowi.getConfig()
     , Page = models.Page
     , User = models.User
     , Revision = models.Revision
+    //, pluginService = require('../service/plugin')
     , actions = {}
-    , api = {};
+    , api = {}
+    ;
 
   actions.api = api;
 
   api.uploadPicture = function (req, res) {
-    var fileUploader = require('../util/fileUploader')(app);
+    var fileUploader = require('../util/fileUploader')(crowi, app);
+    //var storagePlugin = new pluginService('storage');
+    //var storage = require('../service/storage').StorageService(config);
+
     var tmpFile = req.files.userPicture || null;
     if (!tmpFile) {
       return res.json({
@@ -33,6 +39,14 @@ module.exports = function(app) {
       });
     }
 
+    //debug('tmpFile Is', tmpFile, tmpFile.constructor, tmpFile.prototype);
+    //var imageUrl = storage.writeSync(storage.tofs(tmpFile), filePath, {mime: tmpFile.mimetype});
+    //return return res.json({
+    //  'status': true,
+    //  'url': imageUrl,
+    //  'message': '',
+    //});
+
     fileUploader.uploadFile(
       filePath,
       tmpFile.mimetype,
@@ -165,7 +179,7 @@ module.exports = function(app) {
   };
 
   actions.authGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(app);
+    var googleAuth = require('../util/googleAuth')(config);
 
     var userData = req.user;
 
@@ -192,7 +206,7 @@ module.exports = function(app) {
   };
 
   actions.authGoogleCallback = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(app);
+    var googleAuth = require('../util/googleAuth')(config);
     var userData = req.user;
 
     googleAuth.handleCallback(req, function(err, tokenInfo) {

+ 11 - 12
lib/routes/page.js

@@ -1,12 +1,11 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var debug = require('debug')('crowi:routes:page')
-    , models = app.set('models')
-    , Page = models.Page
-    , User = models.User
-    , Revision = models.Revision
-    , Bookmark = models.Bookmark
+    , Page = crowi.model('Page')
+    , User = crowi.model('User')
+    , Revision = crowi.model('Revision')
+    , Bookmark = crowi.model('Bookmark')
     , actions = {};
 
   function getPathFromRequest(req) {
@@ -70,7 +69,7 @@ module.exports = function(app) {
     }
 
     if (pageData.redirectTo) {
-      return res.redirect(pageData.redirectTo + '?renamed=' + pageData.path);
+      return res.redirect(encodeURI(pageData.redirectTo + '?renamed=' + pageData.path));
     }
 
     Revision.findRevisionList(pageData.path, {}, function(err, tree) {
@@ -93,7 +92,7 @@ module.exports = function(app) {
     // pageShow は /* にマッチしてる最後の砦なので、creatableName でない routing は
     // これ以前に定義されているはずなので、こうしてしまって問題ない。
     if (!Page.isCreatableName(path)) {
-      debug('Page is not creatable name.');
+      debug('Page is not creatable name.', path);
       res.redirect('/');
       return ;
     }
@@ -105,7 +104,7 @@ module.exports = function(app) {
 
     Page.findPage(path, req.user, req.query.revision, options, function(err, pageData) {
       if (req.query.revision && err) {
-        res.redirect(path);
+        res.redirect(encodeURI(path));
         return ;
       }
 
@@ -135,7 +134,7 @@ module.exports = function(app) {
     var grant = pageForm.grant;
 
     if (!Page.isCreatableName(path)) {
-      res.redirect(path);
+      res.redirect(encodeURI(path));
       return ;
     }
 
@@ -154,7 +153,7 @@ module.exports = function(app) {
         if (err) {
           console.log('Page save error:', err);
         }
-        app.set('io').sockets.emit('page edited', {page: data, user: req.user});
+        crowi.getIo().sockets.emit('page edited', {page: data, user: req.user});
 
         var redirectPath = encodeURI(path);
         if (grant != data.grant) {
@@ -186,7 +185,7 @@ module.exports = function(app) {
       if (err) {
         return res.redirect('/');
       }
-      return res.redirect(d.path);
+      return res.redirect(encodeURI(d.path));
     };
 
     Page.findPageById(id, function(err, pageData) {

+ 5 - 6
lib/routes/user.js

@@ -1,11 +1,10 @@
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
-   var models = app.set('models')
-    , Page = models.Page
-    , User = models.User
-    , Revision = models.Revision
-    , Bookmark = models.Bookmark
+   var Page = crowi.model('Page')
+    , User = crowi.model('User')
+    , Revision = crowi.model('Revision')
+    , Bookmark = crowi.model('Bookmark')
     , actions = {}
     , api = {};
 

+ 2 - 2
lib/util/fileUploader.js

@@ -3,12 +3,12 @@
  */
 
 
-module.exports = function(app) {
+module.exports = function(crowi, app) {
   'use strict';
 
   var aws = require('aws-sdk')
     , debug = require('debug')('crowi:lib:fileUploader')
-    , config = app.set('config')
+    , config = crowi.getConfig()
     , lib = {}
     ;
 

+ 1 - 2
lib/util/googleAuth.js

@@ -2,12 +2,11 @@
  * googleAuth utility
  */
 
-module.exports = function(app) {
+module.exports = function(config) {
   'use strict';
 
   var googleapis = require('googleapis')
     , debug = require('debug')('crowi:lib:googleAuth')
-    , config = app.set('config')
     , lib = {}
     ;
 

+ 12 - 8
lib/util/mailer.js

@@ -2,16 +2,16 @@
  * mailer
  */
 
-module.exports = function(app) {
+module.exports = function(crowi) {
   'use strict';
 
   var debug = require('debug')('crowi:lib:mailer')
     , nodemailer = require('nodemailer')
     , swig = require('swig')
-    , config = app.set('config')
+    , config = crowi.getConfig()
     , mailConfig = {}
     , mailer = {}
-    , MAIL_TEMPLATE_DIR = app.set('views') + '/mail/'
+    , MAIL_TEMPLATE_DIR = crowi.mailDir
     ;
 
 
@@ -24,19 +24,23 @@ module.exports = function(app) {
       option = {
         host: config.crowi['mail:smtpHost'],
         port: config.crowi['mail:smtpPort'],
-        auth: {
+      };
+
+      if (config.crowi['mail:smtpUser'] && config.crowi['mail:smtpPassword']) {
+        option.auth =  {
           user: config.crowi['mail:smtpUser'],
           pass: config.crowi['mail:smtpPassword']
-        }
-      };
+        };
+      }
       if (option.port === 465) {
         option.secure = true;
       }
     }
+    option.tls = {rejectUnauthorized: false};
 
     client = nodemailer.createTransport(option);
 
-    debug('mailer setted up for SMTP', client);
+    debug('mailer set up for SMTP', client);
     return client;
   }
 
@@ -54,7 +58,7 @@ module.exports = function(app) {
     var ses = require('nodemailer-ses-transport');
     client = nodemailer.createTransport(ses(option));
 
-    debug('mailer setted up for SES', client);
+    debug('mailer set up for SES', client);
     return client;
   }
 

+ 12 - 10
lib/util/middlewares.js

@@ -1,10 +1,12 @@
 var debug = require('debug')('crowi:lib:middlewares');
 
-exports.loginChecker = function(app, models) {
+exports.loginChecker = function(crowi, app) {
   return function(req, res, next) {
+    var User = crowi.model('User');
+
     // session に user object が入ってる
     if (req.session.user && '_id' in req.session.user) {
-      models.User.findById(req.session.user._id, function(err, userData) {
+      User.findById(req.session.user._id, function(err, userData) {
         if (err) {
           next();
         } else {
@@ -21,9 +23,9 @@ exports.loginChecker = function(app, models) {
   };
 };
 
-exports.swigFunctions = function(app) {
+exports.swigFunctions = function(crowi, app) {
   return function(req, res, next) {
-    require('../util/swigFunctions')(app, res.locals);
+    require('../util/swigFunctions')(crowi, app, res.locals);
     next();
   };
 };
@@ -80,19 +82,19 @@ exports.adminRequired = function() {
   };
 };
 
-exports.loginRequired = function(app) {
+exports.loginRequired = function(crowi, app) {
   return function(req, res, next) {
-    var models = app.set('models');
+    var User = crowi.model('User')
 
     if (req.user && '_id' in req.user) {
-      if (req.user.status === models.User.STATUS_ACTIVE) {
+      if (req.user.status === User.STATUS_ACTIVE) {
         // Active の人だけ先に進める
         return next();
-      } else if (req.user.status === models.User.STATUS_REGISTERED) {
+      } else if (req.user.status === User.STATUS_REGISTERED) {
         return res.redirect('/login/error/registered');
-      } else if (req.user.status === models.User.STATUS_SUSPENDED) {
+      } else if (req.user.status === User.STATUS_SUSPENDED) {
         return res.redirect('/login/error/suspended');
-      } else if (req.user.status === models.User.STATUS_INVITED) {
+      } else if (req.user.status === User.STATUS_INVITED) {
         return res.redirect('/login/invited');
       }
     }

+ 5 - 5
lib/util/swigFunctions.js

@@ -1,16 +1,16 @@
-module.exports = function(app, locals) {
+module.exports = function(crowi, app, locals) {
   var debug = require('debug')('crowi:lib:swigFunctions')
-    , models = app.set('models')
-    , Page = models.Page
-    , User = models.User
-    , config = app.set('config')
+    , Page = crowi.model('Page')
+    , User = crowi.model('User')
   ;
 
   locals.facebookLoginEnabled = function() {
+    var config = crowi.getConfig()
     return config.crowi['facebook:appId'] && config.crowi['facebook:secret'];
   };
 
   locals.googleLoginEnabled = function() {
+    var config = crowi.getConfig()
     return config.crowi['google:clientId'] && config.crowi['google:clientSecret'];
   };
 

+ 16 - 12
package.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi",
-  "version": "1.1.3",
+  "version": "1.2.0",
   "description": "The simple & powerful Wiki",
   "tags": [
     "wiki",
@@ -10,7 +10,8 @@
   ],
   "author": "Sotaro KARASAWA <sotaro.k@gmail.com>",
   "contributors": [
-    "Keisuke SATO <riaf@me.com> (http://riaf.jp)"
+    "Keisuke SATO <riaf@me.com> (http://riaf.jp)",
+    "Shinya Yamaoka <contact@mail.libmacro.com>"
   ],
   "config": {
     "blanket": {
@@ -29,6 +30,7 @@
     "async": "~0.9.0",
     "aws-sdk": "~2.0.0-rc.19",
     "basic-auth-connect": "~1.0.0",
+    "bluebird": "^2.9.12",
     "body-parser": "~1.12.0",
     "bower": "~1.4.0",
     "cli": "~0.6.0",
@@ -43,14 +45,16 @@
     "express-session": "~1.10.0",
     "facebook-node-sdk": "=0.1.10",
     "googleapis": "=0.4.7",
-    "grunt": "~0.4.5",
-    "grunt-cli": "~0.1.13",
-    "grunt-contrib-concat": "~0.5.0",
-    "grunt-contrib-jshint": "~0.11.0",
-    "grunt-contrib-uglify": "~0.8.0",
-    "grunt-contrib-watch": "~0.6.1",
-    "grunt-mocha-test": "~0.12.7",
-    "grunt-sass": "~0.18.0",
+    "gulp": "~3.8.11",
+    "gulp-concat": "^2.5.2",
+    "gulp-cssmin": "^0.1.7",
+    "gulp-jshint": "~1.10.0",
+    "gulp-rename": "^1.2.2",
+    "gulp-sass": "~2.0.4",
+    "gulp-spawn-mocha": "^2.2.1",
+    "gulp-uglify": "~1.2.0",
+    "gulp-watch": "~4.2.4",
+    "jshint-stylish": "^2.0.0",
     "method-override": "~2.3.1",
     "mongoose": "~3.8.0",
     "mongoose-paginate": "~3.1.0",
@@ -79,8 +83,8 @@
   ],
   "scripts": {
     "start": "node app.js",
-    "test": "grunt test",
-    "postinstall": "bower cache clean && bower install && grunt"
+    "test": "gulp test",
+    "postinstall": "bower cache clean && bower install && gulp"
   },
   "env": {
     "NODE_ENV": "production"

+ 671 - 0
resource/css/_utilities.scss

@@ -0,0 +1,671 @@
+// crowi
+$brand-primary:         #43676b;
+
+$crowiHeaderBackground: darken($brand-primary, 15%);
+$crowiHeaderHeight: 50px;
+
+// $crowiAsideBackground:   darken($crowiHeaderBackground, 5%);
+$crowiAsideBackground: #fcfcfc;
+
+$crowiFooterBackground:  $crowiHeaderBackground;
+$crowiFooterHeight: 34px;
+
+
+
+//
+// Variables
+// --------------------------------------------------
+
+
+// Global values
+// --------------------------------------------------
+
+// Grays
+// -------------------------
+
+$gray-darker:            lighten(#000, 13.5%); // #222
+$gray-dark:              lighten(#000, 20%);   // #333
+// $gray:                   lighten(#000, 33.5%); // #555
+// $gray-light:             lighten(#000, 60%);   // #999
+// $gray-lighter:           lighten(#000, 93.5%); // #eee
+//
+// // Brand colors
+// // -------------------------
+//
+//$brand-primary: #4d4398;
+//$brand-primary: #622d18;
+//$brand-primary: #e5a323;
+// $brand-success:         #5cb85c;
+// $brand-warning:         #f0ad4e;
+// $brand-danger:          #d9534f;
+// $brand-info:            #5bc0de;
+
+//
+// // Scaffolding
+// // -------------------------
+//
+//$body-bg:              $crowiHeaderBackground;
+// $text-color:            $gray-dark;
+//
+// // Links
+// // -------------------------
+//
+$link-color:            $brand-primary;
+$link-hover-color:      darken($link-color, 15%);
+//
+// // Typography
+// // -------------------------
+//
+// $font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif;
+// $font-family-serif:       Georgia, "Times New Roman", Times, serif;
+// $font-family-monospace:   Monaco, Menlo, Consolas, "Courier New", monospace;
+// $font-family-base:        $font-family-sans-serif;
+//
+// $font-size-base:          14px;
+// $font-size-large:         ceil($font-size-base * 1.25); // ~18px
+// $font-size-small:         ceil($font-size-base * 0.85); // ~12px
+//
+// $font-size-h1:            floor($font-size-base * 2.6); // ~36px
+// $font-size-h2:            floor($font-size-base * 2.15); // ~30px
+// $font-size-h3:            ceil($font-size-base * 1.7); // ~24px
+// $font-size-h4:            ceil($font-size-base * 1.25); // ~18px
+// $font-size-h5:            $font-size-base;
+// $font-size-h6:            ceil($font-size-base * 0.85); // ~12px
+//
+// $line-height-base:        1.428571429; // 20/14
+// $line-height-computed:    floor($font-size-base * $line-height-base); // ~20px
+//
+// $headings-font-family:    $font-family-base;
+// $headings-font-weight:    500;
+// $headings-line-height:    1.1;
+// $headings-color:          inherit;
+//
+//
+// // Iconography
+// // -------------------------
+//
+// $icon-font-path:          "../fonts/";
+// $icon-font-name:          "glyphicons-halflings-regular";
+//
+//
+// // Components
+// // -------------------------
+// // Based on 14px font-size and 1.428 line-height (~20px to start)
+//
+// $padding-base-vertical:          6px;
+// $padding-base-horizontal:        12px;
+//
+// $padding-large-vertical:         10px;
+// $padding-large-horizontal:       16px;
+//
+// $padding-small-vertical:         5px;
+// $padding-small-horizontal:       10px;
+//
+// $line-height-large:              1.33;
+// $line-height-small:              1.5;
+//
+// $border-radius-base:             4px;
+// $border-radius-large:            6px;
+// $border-radius-small:            3px;
+//
+// $component-active-color:         #fff;
+// $component-active-bg:            $brand-primary;
+//
+// $caret-width-base:               4px;
+// $caret-width-large:              5px;
+//
+// // Tables
+// // -------------------------
+//
+// $table-cell-padding:                 8px;
+// $table-condensed-cell-padding:       5px;
+//
+// $table-bg:                           transparent; // overall background-color
+// $table-bg-accent:                    #f9f9f9; // for striping
+// $table-bg-hover:                     #f5f5f5;
+// $table-bg-active:                    $table-bg-hover;
+//
+// $table-border-color:                 #ddd; // table and cell border
+//
+//
+// // Buttons
+// // -------------------------
+//
+// $btn-font-weight:                normal;
+//
+// $btn-default-color:              #333;
+// $btn-default-bg:                 #fff;
+// $btn-default-border:             #ccc;
+//
+// $btn-primary-color:              #fff;
+// $btn-primary-bg:                 $brand-primary;
+// $btn-primary-border:             darken($btn-primary-bg, 5%);
+//
+// $btn-success-color:              #fff;
+// $btn-success-bg:                 $brand-success;
+// $btn-success-border:             darken($btn-success-bg, 5%);
+//
+// $btn-warning-color:              #fff;
+// $btn-warning-bg:                 $brand-warning;
+// $btn-warning-border:             darken($btn-warning-bg, 5%);
+//
+// $btn-danger-color:               #fff;
+// $btn-danger-bg:                  $brand-danger;
+// $btn-danger-border:              darken($btn-danger-bg, 5%);
+//
+// $btn-info-color:                 #fff;
+// $btn-info-bg:                    $brand-info;
+// $btn-info-border:                darken($btn-info-bg, 5%);
+//
+// $btn-link-disabled-color:        $gray-light;
+//
+//
+// // Forms
+// // -------------------------
+//
+// $input-bg:                       #fff;
+// $input-bg-disabled:              $gray-lighter;
+//
+// $input-color:                    $gray;
+// $input-border:                   #ccc;
+// $input-border-radius:            $border-radius-base;
+// $input-border-focus:             #66afe9;
+//
+// $input-color-placeholder:        $gray-light;
+//
+// $input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2);
+// $input-height-large:             (floor($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2);
+// $input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2);
+//
+// $legend-color:                   $gray-dark;
+// $legend-border-color:            #e5e5e5;
+//
+// $input-group-addon-bg:           $gray-lighter;
+// $input-group-addon-border-color: $input-border;
+//
+//
+// // Dropdowns
+// // -------------------------
+//
+// $dropdown-bg:                    #fff;
+// $dropdown-border:                rgba(0,0,0,.15);
+// $dropdown-fallback-border:       #ccc;
+// $dropdown-divider-bg:            #e5e5e5;
+//
+$dropdown-link-color:            $gray-dark;
+$dropdown-link-hover-color:      darken($gray-dark, 5%);
+$dropdown-link-hover-bg:         #f5f5f5;
+//
+// $dropdown-link-active-color:     $component-active-color;
+// $dropdown-link-active-bg:        $component-active-bg;
+//
+// $dropdown-link-disabled-color:   $gray-light;
+//
+// $dropdown-header-color:          $gray-light;
+//
+// $dropdown-caret-color:           #000;
+//
+//
+// // COMPONENT VARIABLES
+// // --------------------------------------------------
+//
+//
+// // Z-index master list
+// // -------------------------
+// // Used for a bird's eye view of components dependent on the z-axis
+// // Try to avoid customizing these :)
+//
+// $zindex-navbar:            1000;
+// $zindex-dropdown:          1000;
+// $zindex-popover:           1010;
+// $zindex-tooltip:           1030;
+// $zindex-navbar-fixed:      1030;
+// $zindex-modal-background:  1040;
+// $zindex-modal:             1050;
+//
+// // Media queries breakpoints
+// // --------------------------------------------------
+//
+// // Extra small screen / phone
+// // Note: Deprecated $screen-xs and $screen-phone as of v3.0.1
+// $screen-xs:                  480px;
+// $screen-xs-min:              $screen-xs;
+// $screen-phone:               $screen-xs-min;
+//
+// // Small screen / tablet
+// // Note: Deprecated $screen-sm and $screen-tablet as of v3.0.1
+// $screen-sm:                  768px;
+// $screen-sm-min:              $screen-sm;
+// $screen-tablet:              $screen-sm-min;
+//
+// // Medium screen / desktop
+// // Note: Deprecated $screen-md and $screen-desktop as of v3.0.1
+// $screen-md:                  992px;
+// $screen-md-min:              $screen-md;
+// $screen-desktop:             $screen-md-min;
+//
+// // Large screen / wide desktop
+// // Note: Deprecated $screen-lg and $screen-lg-desktop as of v3.0.1
+// $screen-lg:                  1200px;
+// $screen-lg-min:              $screen-lg;
+// $screen-lg-desktop:          $screen-lg-min;
+//
+// // So media queries don't overlap when required, provide a maximum
+// $screen-xs-max:              ($screen-sm-min - 1);
+// $screen-sm-max:              ($screen-md-min - 1);
+// $screen-md-max:              ($screen-lg-min - 1);
+//
+//
+// // Grid system
+// // --------------------------------------------------
+//
+// // Number of columns in the grid system
+// $grid-columns:              12;
+// // Padding, to be divided by two and applied to the left and right of all columns
+// $grid-gutter-width:         30px;
+// // Point at which the navbar stops collapsing
+// $grid-float-breakpoint:     $screen-sm-min;
+//
+//
+// // Navbar
+// // -------------------------
+//
+// // Basics of a navbar
+// $navbar-height:                    50px;
+// $navbar-margin-bottom:             $line-height-computed;
+// $navbar-border-radius:             $border-radius-base;
+// $navbar-padding-horizontal:        floor($grid-gutter-width / 2);
+// $navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2);
+//
+// $navbar-default-color:             #777;
+// $navbar-default-bg:                #f8f8f8;
+// $navbar-default-border:            darken($navbar-default-bg, 6.5%);
+//
+// // Navbar links
+// $navbar-default-link-color:                #777;
+// $navbar-default-link-hover-color:          #333;
+// $navbar-default-link-hover-bg:             transparent;
+// $navbar-default-link-active-color:         #555;
+// $navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%);
+// $navbar-default-link-disabled-color:       #ccc;
+// $navbar-default-link-disabled-bg:          transparent;
+//
+// // Navbar brand label
+// $navbar-default-brand-color:               $navbar-default-link-color;
+// $navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%);
+// $navbar-default-brand-hover-bg:            transparent;
+//
+// // Navbar toggle
+// $navbar-default-toggle-hover-bg:           #ddd;
+// $navbar-default-toggle-icon-bar-bg:        #ccc;
+// $navbar-default-toggle-border-color:       #ddd;
+//
+//
+// // Inverted navbar
+// //
+// // Reset inverted navbar basics
+// $navbar-inverse-color:                      $gray-light;
+// $navbar-inverse-bg:                         #222;
+// $navbar-inverse-border:                     darken($navbar-inverse-bg, 10%);
+//
+// // Inverted navbar links
+// $navbar-inverse-link-color:                 $gray-light;
+// $navbar-inverse-link-hover-color:           #fff;
+// $navbar-inverse-link-hover-bg:              transparent;
+// $navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color;
+// $navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%);
+// $navbar-inverse-link-disabled-color:        #444;
+// $navbar-inverse-link-disabled-bg:           transparent;
+//
+// // Inverted navbar brand label
+// $navbar-inverse-brand-color:                $navbar-inverse-link-color;
+// $navbar-inverse-brand-hover-color:          #fff;
+// $navbar-inverse-brand-hover-bg:             transparent;
+//
+// // Inverted navbar toggle
+// $navbar-inverse-toggle-hover-bg:            #333;
+// $navbar-inverse-toggle-icon-bar-bg:         #fff;
+// $navbar-inverse-toggle-border-color:        #333;
+//
+//
+// // Navs
+// // -------------------------
+//
+// $nav-link-padding:                          10px 15px;
+// $nav-link-hover-bg:                         $gray-lighter;
+//
+// $nav-disabled-link-color:                   $gray-light;
+// $nav-disabled-link-hover-color:             $gray-light;
+//
+// $nav-open-link-hover-color:                 #fff;
+// $nav-open-caret-border-color:               #fff;
+//
+// // Tabs
+// $nav-tabs-border-color:                     #ddd;
+//
+// $nav-tabs-link-hover-border-color:          $gray-lighter;
+//
+// $nav-tabs-active-link-hover-bg:             $body-bg;
+// $nav-tabs-active-link-hover-color:          $gray;
+// $nav-tabs-active-link-hover-border-color:   #ddd;
+//
+// $nav-tabs-justified-link-border-color:            #ddd;
+// $nav-tabs-justified-active-link-border-color:     $body-bg;
+//
+// // Pills
+// $nav-pills-border-radius:                   $border-radius-base;
+// $nav-pills-active-link-hover-bg:            $component-active-bg;
+// $nav-pills-active-link-hover-color:         $component-active-color;
+//
+//
+// // Pagination
+// // -------------------------
+//
+// $pagination-bg:                        #fff;
+// $pagination-border:                    #ddd;
+//
+// $pagination-hover-bg:                  $gray-lighter;
+//
+// $pagination-active-bg:                 $brand-primary;
+// $pagination-active-color:              #fff;
+//
+// $pagination-disabled-color:            $gray-light;
+//
+//
+// // Pager
+// // -------------------------
+//
+// $pager-border-radius:                  15px;
+// $pager-disabled-color:                 $gray-light;
+//
+//
+// // Jumbotron
+// // -------------------------
+//
+// $jumbotron-padding:              30px;
+// $jumbotron-color:                inherit;
+// $jumbotron-bg:                   $gray-lighter;
+// $jumbotron-heading-color:        inherit;
+// $jumbotron-font-size:            ceil($font-size-base * 1.5);
+//
+//
+// // Form states and alerts
+// // -------------------------
+//
+// $state-success-text:             #468847;
+// $state-success-bg:               #dff0d8;
+// $state-success-border:           darken(spin($state-success-bg, -10), 5%);
+//
+// $state-info-text:                #3a87ad;
+// $state-info-bg:                  #d9edf7;
+// $state-info-border:              darken(spin($state-info-bg, -10), 7%);
+//
+// $state-warning-text:             #c09853;
+// $state-warning-bg:               #fcf8e3;
+// $state-warning-border:           darken(spin($state-warning-bg, -10), 5%);
+//
+// $state-danger-text:              #b94a48;
+// $state-danger-bg:                #f2dede;
+// $state-danger-border:            darken(spin($state-danger-bg, -10), 5%);
+//
+//
+// // Tooltips
+// // -------------------------
+// $tooltip-max-width:           200px;
+// $tooltip-color:               #fff;
+// $tooltip-bg:                  #000;
+//
+// $tooltip-arrow-width:         5px;
+// $tooltip-arrow-color:         $tooltip-bg;
+//
+//
+// // Popovers
+// // -------------------------
+// $popover-bg:                          #fff;
+// $popover-max-width:                   276px;
+// $popover-border-color:                rgba(0,0,0,.2);
+// $popover-fallback-border-color:       #ccc;
+//
+// $popover-title-bg:                    darken($popover-bg, 3%);
+//
+// $popover-arrow-width:                 10px;
+// $popover-arrow-color:                 #fff;
+//
+// $popover-arrow-outer-width:           ($popover-arrow-width + 1);
+// $popover-arrow-outer-color:           rgba(0,0,0,.25);
+// $popover-arrow-outer-fallback-color:  #999;
+//
+//
+// // Labels
+// // -------------------------
+//
+// $label-default-bg:            $gray-light;
+// $label-primary-bg:            $brand-primary;
+// $label-success-bg:            $brand-success;
+// $label-info-bg:               $brand-info;
+// $label-warning-bg:            $brand-warning;
+// $label-danger-bg:             $brand-danger;
+//
+// $label-color:                 #fff;
+// $label-link-hover-color:      #fff;
+//
+//
+// // Modals
+// // -------------------------
+// $modal-inner-padding:         20px;
+//
+// $modal-title-padding:         15px;
+// $modal-title-line-height:     $line-height-base;
+//
+// $modal-content-bg:                             #fff;
+// $modal-content-border-color:                   rgba(0,0,0,.2);
+// $modal-content-fallback-border-color:          #999;
+//
+// $modal-backdrop-bg:           #000;
+// $modal-header-border-color:   #e5e5e5;
+// $modal-footer-border-color:   $modal-header-border-color;
+//
+//
+// // Alerts
+// // -------------------------
+// $alert-padding:               15px;
+// $alert-border-radius:         $border-radius-base;
+// $alert-link-font-weight:      bold;
+//
+// $alert-success-bg:            $state-success-bg;
+// $alert-success-text:          $state-success-text;
+// $alert-success-border:        $state-success-border;
+//
+// $alert-info-bg:               $state-info-bg;
+// $alert-info-text:             $state-info-text;
+// $alert-info-border:           $state-info-border;
+//
+// $alert-warning-bg:            $state-warning-bg;
+// $alert-warning-text:          $state-warning-text;
+// $alert-warning-border:        $state-warning-border;
+//
+// $alert-danger-bg:             $state-danger-bg;
+// $alert-danger-text:           $state-danger-text;
+// $alert-danger-border:         $state-danger-border;
+//
+//
+// // Progress bars
+// // -------------------------
+// $progress-bg:                 #f5f5f5;
+// $progress-bar-color:          #fff;
+//
+// $progress-bar-bg:             $brand-primary;
+// $progress-bar-success-bg:     $brand-success;
+// $progress-bar-warning-bg:     $brand-warning;
+// $progress-bar-danger-bg:      $brand-danger;
+// $progress-bar-info-bg:        $brand-info;
+//
+//
+// // List group
+// // -------------------------
+// $list-group-bg:               #fff;
+// $list-group-border:           #ddd;
+// $list-group-border-radius:    $border-radius-base;
+//
+// $list-group-hover-bg:         #f5f5f5;
+// $list-group-active-color:     $component-active-color;
+// $list-group-active-bg:        $component-active-bg;
+// $list-group-active-border:    $list-group-active-bg;
+//
+// $list-group-link-color:          #555;
+// $list-group-link-heading-color:  #333;
+//
+//
+// // Panels
+// // -------------------------
+// $panel-bg:                    #fff;
+// $panel-inner-border:          #ddd;
+// $panel-border-radius:         $border-radius-base;
+// $panel-footer-bg:             #f5f5f5;
+//
+// $panel-default-text:          $gray-dark;
+// $panel-default-border:        #ddd;
+// $panel-default-heading-bg:    #f5f5f5;
+//
+// $panel-primary-text:          #fff;
+// $panel-primary-border:        $brand-primary;
+// $panel-primary-heading-bg:    $brand-primary;
+//
+// $panel-success-text:          $state-success-text;
+// $panel-success-border:        $state-success-border;
+// $panel-success-heading-bg:    $state-success-bg;
+//
+// $panel-warning-text:          $state-warning-text;
+// $panel-warning-border:        $state-warning-border;
+// $panel-warning-heading-bg:    $state-warning-bg;
+//
+// $panel-danger-text:           $state-danger-text;
+// $panel-danger-border:         $state-danger-border;
+// $panel-danger-heading-bg:     $state-danger-bg;
+//
+// $panel-info-text:             $state-info-text;
+// $panel-info-border:           $state-info-border;
+// $panel-info-heading-bg:       $state-info-bg;
+//
+//
+// // Thumbnails
+// // -------------------------
+// $thumbnail-padding:           4px;
+// $thumbnail-bg:                $body-bg;
+// $thumbnail-border:            #ddd;
+// $thumbnail-border-radius:     $border-radius-base;
+//
+// $thumbnail-caption-color:     $text-color;
+// $thumbnail-caption-padding:   9px;
+//
+//
+// // Wells
+// // -------------------------
+// $well-bg:                     #f5f5f5;
+//
+//
+// // Badges
+// // -------------------------
+// $badge-color:                 #fff;
+// $badge-link-hover-color:      #fff;
+// $badge-bg:                    $gray-light;
+//
+// $badge-active-color:          $link-color;
+// $badge-active-bg:             #fff;
+//
+// $badge-font-weight:           bold;
+// $badge-line-height:           1;
+// $badge-border-radius:         10px;
+//
+//
+// // Breadcrumbs
+// // -------------------------
+// $breadcrumb-bg:               #f5f5f5;
+// $breadcrumb-color:            #ccc;
+// $breadcrumb-active-color:     $gray-light;
+// $breadcrumb-separator:        "/";
+//
+//
+// // Carousel
+// // ------------------------
+//
+// $carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+//
+// $carousel-control-color:                      #fff;
+// $carousel-control-width:                      15%;
+// $carousel-control-opacity:                    .5;
+// $carousel-control-font-size:                  20px;
+//
+// $carousel-indicator-active-bg:                #fff;
+// $carousel-indicator-border-color:             #fff;
+//
+// $carousel-caption-color:                      #fff;
+//
+//
+// // Close
+// // ------------------------
+// $close-font-weight:           bold;
+// $close-color:                 #000;
+// $close-text-shadow:           0 1px 0 #fff;
+//
+//
+// // Code
+// // ------------------------
+// $code-color:                  #c7254e;
+// $code-bg:                     #f9f2f4;
+//
+// $pre-bg:                      #f5f5f5;
+// $pre-color:                   $gray-dark;
+// $pre-border-color:            #ccc;
+// $pre-scrollable-max-height:   340px;
+//
+// // Type
+// // ------------------------
+// $text-muted:                  $gray-light;
+// $abbr-border-color:           $gray-light;
+// $headings-small-color:        $gray-light;
+// $blockquote-small-color:      $gray-light;
+// $blockquote-border-color:     $gray-lighter;
+// $page-header-border-color:    $gray-lighter;
+//
+// // Miscellaneous
+// // -------------------------
+//
+// // Hr border color
+// $hr-border:                   $gray-lighter;
+//
+// // Horizontal forms & lists
+// $component-offset-horizontal: 180px;
+//
+//
+// // Container sizes
+// // --------------------------------------------------
+//
+// // Small screen / tablet
+// $container-tablet:             ((720px + $grid-gutter-width));
+// $container-sm:                 $container-tablet;
+//
+// // Medium screen / desktop
+// $container-desktop:            ((940px + $grid-gutter-width));
+// $container-md:                 $container-desktop;
+//
+// // Large screen / wide desktop
+// $container-large-desktop:      ((1140px + $grid-gutter-width));
+// $container-lg:                 $container-large-desktop;
+
+
+//
+// mixins
+//
+
+// Badget
+@mixin badge-variant($color) {
+  background-color: $color;
+
+  &[href] {
+    &:hover,
+    &:focus {
+      background-color: darken($color, 10%);
+    }
+  }
+}

+ 1 - 4
resource/css/crowi.scss

@@ -1,15 +1,12 @@
 // import crowi variable
-@import 'variables';
+@import 'utilities';
 
 // import bootstrap
 @import 'bootstrap';
 
 @import 'font-awesome';
 
-@import 'mixins';
-
 // crowi component
-@import 'mixins';
 @import 'layout';
 @import 'form';
 @import 'wiki';

+ 2 - 0
test/bootstrap.js

@@ -3,6 +3,7 @@
 var express = require('express')
   , async = require('async')
   , mongoose= require('mongoose')
+  , ROOT_DIR = __dirname + '/..'
   , MODEL_DIR = __dirname + '/../lib/models'
   , mongoUri
   , testDBUtil
@@ -39,5 +40,6 @@ testDBUtil = {
 global.express = express;
 global.mongoose = mongoose;
 global.mongoUri = mongoUri;
+global.ROOT_DIR = ROOT_DIR;
 global.MODEL_DIR = MODEL_DIR;
 global.testDBUtil = testDBUtil;

+ 59 - 0
test/crowi/crowi.test.js

@@ -0,0 +1,59 @@
+var chai = require('chai')
+  , expect = chai.expect
+  , sinon = require('sinon')
+  , sinonChai = require('sinon-chai')
+  , proxyquire = require('proxyquire')
+
+  , path = require('path')
+  , Promise = require('bluebird')
+  ;
+chai.use(sinonChai);
+
+describe('Test for Crowi application context', function () {
+  var Crowi = require('../../lib/crowi')
+    , mongoose = require('mongoose')
+    ;
+
+  describe('construction', function() {
+    it('initialize crowi context', function() {
+      var crowi = new Crowi(path.normalize(__dirname + '/../../'), process.env);
+      expect(crowi).to.be.instanceof(Crowi);
+      expect(crowi.version).to.equal(require('../../package.json').version);
+      expect(crowi.env).to.be.an('Object');
+    });
+
+    it('config getter, setter', function() {
+      var crowi = new Crowi(path.normalize(__dirname + '/../../'), process.env);
+      expect(crowi.getConfig()).to.deep.equals({});
+      crowi.setConfig({test: 1});
+      expect(crowi.getConfig()).to.deep.equals({test: 1});
+    });
+
+    it('model getter, setter', function() {
+      var crowi = new Crowi(path.normalize(__dirname + '/../../'), process.env);
+      // set
+      crowi.model('hoge', { fuga: 1 });
+      expect(crowi.model('hoge')).to.deep.equals({ fuga: 1 });
+    });
+  });
+
+  describe('.setupDatabase', function() {
+    it('setup completed', function(done) {
+      var crowi = new Crowi(path.normalize(__dirname + '/../../'), process.env);
+      // set
+      var p = crowi.setupDatabase()
+      expect(p).to.instanceof(Promise);
+      if (mongoUri) {
+        p.then(function() {
+          expect(mongoose.connection.readyState).to.equals(1);
+          done();
+        });
+      } else {
+        p.catch(function() {
+          expect(mongoose.connection.readyState).to.equals(1);
+          done();
+        });
+      }
+    });
+  });
+});

+ 4 - 5
test/models/config.test.js

@@ -8,8 +8,8 @@ chai.use(sinonChai);
 
 describe('Config model test', function () {
   var conn
-    , app = new express()
-    , Config = proxyquire(MODEL_DIR + '/config.js', {mongoose: mongoose})(app)
+    , crowi = new (require(ROOT_DIR + '/lib/crowi'))(ROOT_DIR, process.env)
+    , Config = proxyquire(MODEL_DIR + '/config.js', {mongoose: mongoose})(crowi)
     ;
 
   before(function (done) {
@@ -38,9 +38,8 @@ describe('Config model test', function () {
 
   after(function (done) {
     if (mongoUri) {
-      testDBUtil.cleanUpDb(conn, 'Config', function(err, doc) {
-        conn.close();
-        done();
+      return testDBUtil.cleanUpDb(conn, 'Config', function(err, doc) {
+        return conn.close(done);
       });
     }
   });

+ 143 - 0
test/models/page.test.js

@@ -0,0 +1,143 @@
+var chai = require('chai')
+  , expect = chai.expect
+  , sinon = require('sinon')
+  , sinonChai = require('sinon-chai')
+  , proxyquire = require('proxyquire')
+  ;
+chai.use(sinonChai);
+
+describe('Page', function () {
+  var conn
+    , crowi = new (require(ROOT_DIR + '/lib/crowi'))(ROOT_DIR, process.env)
+    , Page = proxyquire(MODEL_DIR + '/page.js', {mongoose: mongoose})(crowi)
+    , User = proxyquire(MODEL_DIR + '/user.js', {mongoose: mongoose})(crowi)
+    ;
+
+  if (!mongoUri) {
+    return;
+  }
+
+  before(function (done) {
+    conn = mongoose.createConnection(mongoUri, function(err) {
+      if (err) {
+        done();
+      }
+
+      Page = conn.model('Page');
+      User = conn.model('User');
+
+      var fixture = [
+        {
+          path: '/user/anonymous/memo',
+          grant: Page.GRANT_RESTRICTED,
+          grantedUsers: []
+        },
+        {
+          path: '/grant/public',
+          grant: Page.GRANT_PUBLIC
+        },
+        {
+          path: '/grant/restricted',
+          grant: Page.GRANT_RESTRICTED
+        },
+        {
+          path: '/grant/specified',
+          grant: Page.GRANT_SPECIFIED
+        },
+        {
+          path: '/grant/owner',
+          grant: Page.GRANT_OWNER
+        }
+      ];
+      var userFixture = [
+        {userId: 'anonymous', email: 'anonymous@gmail.com'}
+      ];
+
+      testDBUtil.generateFixture(conn, 'Page', fixture, function() {});
+      testDBUtil.generateFixture(conn, 'User', userFixture, done);
+    });
+  });
+
+  after(function (done) {
+    return testDBUtil.cleanUpDb(conn, 'Page', function(err, doc) {
+      return conn.close(done);
+    });
+  });
+
+  describe('.isPublic', function () {
+    context('with a public page', function() {
+      it('should return true', function(done) {
+        Page.findOne({path: '/grant/public'}, function(err, page) {
+          expect(err).to.be.null;
+          expect(page.isPublic()).to.be.equal(true);
+          done();
+        });
+      });
+    });
+
+    ['restricted', 'specified', 'owner'].forEach(function(grant) {
+      context('with a ' + grant + ' page', function() {
+        it('should return false', function(done) {
+          Page.findOne({path: '/grant/' + grant}, function(err, page) {
+            expect(err).to.be.null;
+            expect(page.isPublic()).to.be.equal(false);
+            done();
+          });
+        });
+      });
+    });
+  });
+
+  describe('.isGrantedFor', function() {
+    context('with a granted user', function() {
+      it('should return true', function(done) {
+        User.find({userId: 'anonymous'}, function(err, user) {
+          if (err) { done(err); }
+
+          Page.findOne({path: '/user/anonymous/memo'}, function(err, page) {
+            if (err) { done(err); }
+
+            page.grantedUsers.push(user.id);
+
+            page.save(function(err, newPage) {
+              if (err) { done(err); }
+
+              expect(newPage.isGrantedFor(user)).to.be.equal(true);
+              done();
+            });
+          });
+        });
+      });
+    });
+
+    context('with a public page', function() {
+      it('should return true', function(done) {
+        User.find({userId: 'anonymous'}, function(err, user) {
+          if (err) { done(err); }
+
+          Page.findOne({path: '/user/anonymous/memo'}, function(err, page) {
+            if (err) { done(err); }
+
+            expect(page.isGrantedFor(user)).to.be.equal(true);
+            done();
+          });
+        });
+      });
+    });
+
+    context('with a restricted page and an user who has no grant', function() {
+      it('should return false', function(done) {
+        User.find({userId: 'anonymous'}, function(err, user) {
+          if (err) { done(err); }
+
+          Page.findOne({path: '/grant/restricted'}, function(err, page) {
+            if (err) { done(err); }
+
+            expect(page.isGrantedFor(user)).to.be.equal(false);
+            done();
+          });
+        });
+      });
+    });
+  });
+});