Răsfoiți Sursa

Merge pull request #1 from crowi/feature/config

Config を DB 化
Sotaro KARASAWA 11 ani în urmă
părinte
comite
43ee7668e1

+ 17 - 5
README.md

@@ -1,6 +1,10 @@
-Crowi - The Simple and Powerful Communication Tool Based on Wiki
+Crowi - The Simple & Powerful Communication Tool Based on Wiki
 ================================================================
 ================================================================
 
 
+
+[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
+
+
 Crowi is:
 Crowi is:
 
 
 * Easy to edit and share,
 * Easy to edit and share,
@@ -8,6 +12,7 @@ Crowi is:
 * Useful timeline list view,
 * Useful timeline list view,
 * Fast.
 * Fast.
 
 
+
 Install
 Install
 ---------
 ---------
 
 
@@ -17,12 +22,9 @@ Install dependencies and build CSS and JavaScript:
     $ bower install
     $ bower install
     $ grunt
     $ grunt
 
 
-Configuration:
-
-    $ cp config/default.js.dist config/default.js
-
 More info are [here](doc/index.md).
 More info are [here](doc/index.md).
 
 
+
 Dependencies
 Dependencies
 -------------
 -------------
 
 
@@ -32,6 +34,16 @@ Dependencies
 * Facebook Application (optional)
 * Facebook Application (optional)
 * Google Project (optional)
 * Google Project (optional)
 
 
+
+Start Up on Local
+-------------------
+
+Crowi is designed setting up to Heroku or some PaaS, but you can start up Crowi with ENV parameter on your local.
+
+```
+$ MONGOLAB_URI=mongodb://username:password@localhost/crowi node app.js
+```
+
 License
 License
 ---------
 ---------
 
 

+ 133 - 146
app.js

@@ -1,160 +1,147 @@
 /**
 /**
- * Fakie::app.js
+ * Crowi::app.js
  *
  *
- * @package Fakie
+ * @package Crowi
  * @author  Sotaro KARASAWA <sotarok@crocos.co.jp>
  * @author  Sotaro KARASAWA <sotarok@crocos.co.jp>
  */
  */
 
 
-var express  = require('express');
-var cons     = require('consolidate');
-var swig     = require('swig');
-var flash    = require('connect-flash');
-var config   = require('config');
-var http     = require('http');
-var facebook = require('facebook-node-sdk');
-var mongo    = require('mongoose');
-var socketio = require('socket.io');
-
-var time     = require('time');
+var express  = require('express')
+  , cons     = require('consolidate')
+  , swig     = require('swig')
+  , flash    = require('connect-flash')
+  , http     = require('http')
+  , facebook = require('facebook-node-sdk')
+  , mongo    = require('mongoose')
+  , socketio = require('socket.io')
+  , middleware = require('./lib/middlewares')
+  , time     = require('time')
+  , async    = require('async')
+  , models
+  , config
+  , server
+  ;
+
+
 time.tzset('Asia/Tokyo');
 time.tzset('Asia/Tokyo');
-tzoffset = -(config.app.timezone || 9) * 60; // for datez
 
 
 var app = express();
 var app = express();
+var env = app.get('env');
+
+// 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);
+
+app.set('port', process.env.SERVER_PORT || 3000);
+app.engine('html', cons.swig);
+app.set('view cache', false);
+app.set('view engine', 'html');
+app.set('views', __dirname + '/views');
+app.use(express.methodOverride());
+app.use(express.bodyParser());
+app.use(express.cookieParser());
+app.use(express.session({
+  rolling: true,
+  secret: process.env.SECRET_TOKEN || 'this is default session secret',
+}));
+app.use(flash());
+app.use(express.static(__dirname + '/public'));
+
+config = require('./models/config')(app);
+
+async.series([
+  function (next) {
+    config.getConfigArray(function(err, doc) {
+      app.set('config', doc);
+
+      return next();
+    });
+  }, function (next) {
+    models = require('./models')(app);
+    models.Config = config;
+
+    // configure application
+    app.use(function(req, res, next) {
+      var days = (1000*3600*24*30)
+        , now = new Date()
+        , fbparams = {}
+        , config = app.set('config');
+
+      tzoffset = -(config.crowi['app:timezone'] || 9) * 60; // for datez
+      app.set('tzoffset', tzoffset);
+
+      req.config = config;
+
+      req.session.cookie.expires = new Date(Date.now() + days);
+      req.session.cookie.maxAge = days;
+
+      req.baseUrl = (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + "://" + req.get('host');
+      res.locals({
+        req: req,
+        baseUrl: req.baseUrl,
+        config: config,
+        env: app.get('env'),
+        now: now,
+        tzoffset: tzoffset,
+        facebook: {appId: config.crowi['facebook:appId'] || ''},
+        consts: {
+          pageGrants: models.Page.getGrantLabels(),
+          userStatus: models.User.getUserStatusLabels(),
+        },
+      });
 
 
-mongo.connect('mongodb://' + config.mongodb.user + ':' + config.mongodb.password + '@' + config.mongodb.host + '/' + config.mongodb.dbname);
-
-
-// swig
-// TODO どっかに移す
-swig.setFilter('path2name', function(string) {
-  return string.replace(/.+\/(.+)?$/, "$1");
-});
-swig.setFilter('datetz', function(input, format) {
-  // デフォルトの filter の override するにはどうしたらいいんだろうかね
-  var swigFilters     = require('swig/lib/filters')
-  return swigFilters.date(input, format, tzoffset);
-});
-swig.setFilter('presentation', function(string) {
-  // 手抜き
-  return string.replace(/[\n]+#/g, "\n\n\n#");
-});
-swig.setFilter('picture', function(user) {
-  if (!user) {
-    return '';
-  }
-
-  user.fbId = user.userId; // migration
-  if (user.image && user.image != '/images/userpicture.png') {
-    return user.image;
-  } else if (user.fbId) {
-    return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
-  } else {
-    return '/images/userpicture.png';
-  }
-});
-
-app.configure(function(){
-  var models;
-
-  app.set('port', config.server.port || 3000);
-  app.engine('html', cons.swig);
-  app.set('view cache', false);
-  app.set('view engine', 'html');
-  app.set('views', __dirname + '/views');
-  app.use(express.methodOverride());
-  app.use(express.bodyParser());
-  app.use(express.cookieParser());
-  app.use(express.session({
-    rolling: true,
-    secret: config.session.secret,
-  }));
-  app.use(flash());
-  app.use(facebook.middleware({appId: config.facebook.appId, secret: config.facebook.secret}));
-  models = require('./models')(app);
-  app.use(function(req, res, next) {
-    var days = (1000*3600*24*30);
-    req.session.cookie.expires = new Date(Date.now() + days);
-    req.session.cookie.maxAge = days;
-
-    var now = new Date();
-
-    req.config = config;
-    req.baseUrl = (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + "://" + req.get('host');
-    res.locals({
-      req: req,
-      baseUrl: req.baseUrl,
-      config: config,
-      env: app.get('env'),
-      now: now,
-      tzoffset: tzoffset,
-      facebook: {appId: config.facebook.appId},
-      consts: {
-        pageGrants: models.Page.getGrantLabels(),
-        userStatus: models.User.getUserStatusLabels(),
-      },
+      if (config.crowi['facebook:appId']) {
+        app.use(facebook.middleware({
+          appId: config.crowi['facebook:appId'],
+          secret: config.crowi['facebook:secret']
+        }));
+      }
+      next();
     });
     });
-    next();
-  });
 
 
-  // register swig function
-  app.use(function(req, res, next) {
-    res.locals(require('./lib/swig_functions')(app));
+    // register swig function
+    app.use(middleware.swigFilters(app, swig));
+    app.use(middleware.swigFunctions(app));
+
+    app.use(middleware.loginChecker(app, models));
+
+    app.use(app.router);
+
     next();
     next();
-  });
-
-  app.use(function(req, res, next) {
-    // session に user object が入ってる
-    if (req.session.user && '_id' in req.session.user) {
-      models.User.findById(req.session.user._id, function(err, userData) {
-        if (err) {
-          next()
-        } else {
-          req.user = req.session.user = userData;
-          res.locals({user: req.user});
-          next();
-        }
+  }, function(next) {
+
+    if (env == 'development') {
+      swig.setDefaults({ cache: false });
+      app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
+
+      server = http.createServer(app).listen(app.get('port'), function(){
+        console.log("[" + app.get('env') + "] Express server listening on port " + app.get('port'));
       });
       });
-    } else {
-      req.user = req.session.user = false;
-      res.locals({user: req.user});
-      next();
     }
     }
-  });
-
-  app.use(express.static(__dirname + '/public'));
-  app.use(app.router);
-});
-
-app.configure('development', function(){
-  swig.setDefaults({ cache: false });
-  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
-
-});
-
-app.configure('production', function(){
-  var oneYear = 31557600000;
-
-  // Error Handler
-  app.use(function (err, req, res, next) {
-    res.status(500);
-    res.render('500', { error: err });
-  });
-});
-
-var server = 1;
-if (app.get('env') == 'development') {
-  server = http.createServer(app).listen(app.get('port'), function(){
-    console.log("[" + app.get('env') + "] Express server listening on port " + app.get('port'));
-  });
-} else {
-  server = http.createServer(app).listen(app.get('port'), '127.0.0.1', function(){
-    console.log("[" + app.get('env') + "] Express server listening on port " + app.get('port'));
-  });
-}
-
-var io = socketio.listen(server);
-io.sockets.on('connection', function (socket) {
-});
-
-app.set('io', io);
-require('./routes')(app);
+
+    if (env == 'production') {
+      var oneYear = 31557600000;
+      app.use(function (err, req, res, next) {
+        res.status(500);
+        res.render('500', { error: err });
+      });
+
+      server = http.createServer(app).listen(app.get('port'), '127.0.0.1', function(){
+        console.log("[" + app.get('env') + "] Express server listening on port " + app.get('port'));
+      });
+    }
+
+    var io = socketio.listen(server);
+    io.sockets.on('connection', function (socket) {
+    });
+
+    app.set('io', io);
+    require('./routes')(app);
+
+    next();
+  }
+]);
+

+ 29 - 0
app.json

@@ -0,0 +1,29 @@
+{
+  "name": "Crowi",
+  "description": "The simple & powerful Wiki.",
+  "keywords": [
+    "wiki",
+    "communication"
+  ],
+  "website": "https://crowi.wiki/",
+  "repository": "https://github.com/crocos/crowi",
+  "logo": "http://crowi.wiki/images/logo/128x128.png",
+  "success_url": "/",
+  "scripts": {
+    "postdeploy": "grunt"
+  },
+  "env": {
+    "NODE_ENV": "production",
+    "SECRET_TOKEN": {
+      "description": "A secret key for verifying the integrity of signed cookies.",
+      "generator": "secret"
+    },
+    "WEB_CONCURRENCY": {
+      "description": "The number of processes to run.",
+      "value": "5"
+    }
+  },
+  "addons": [
+    "mongohq:free"
+  ]
+}

+ 0 - 9
config/default.js.dist

@@ -3,9 +3,6 @@
  */
  */
 
 
 module.exports = {
 module.exports = {
-  server: {
-    port: 3000
-  },
   app: {
   app: {
     title: 'Crocos Wiki'
     title: 'Crocos Wiki'
   },
   },
@@ -21,12 +18,6 @@ module.exports = {
     accessKeyId: '',
     accessKeyId: '',
     secretAccessKey: ''
     secretAccessKey: ''
   },
   },
-  mongodb: {
-    host: 'localhost',
-    dbname: 'crowi',
-    user: '',
-    password: ''
-  },
   searcher: {
   searcher: {
     url: 'https:// ...' // crocos-wikis
     url: 'https:// ...' // crocos-wikis
   },
   },

+ 10 - 0
form/admin/app.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[app:title]').required(),
+  field('settingForm[app:confidential]')
+);
+

+ 12 - 0
form/admin/aws.js

@@ -0,0 +1,12 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[aws:region]', 'リージョン').trim().is(/^[a-z]+-[a-z]+-\d+$/, 'リージョンには、AWSリージョン名を入力してください。 例: ap-northeast-1'),
+  field('settingForm[aws:bucket]', 'バケット名').trim(),
+  field('settingForm[aws:accessKeyId]', 'Access Key Id').trim().is(/^[\da-zA-Z]$/),
+  field('settingForm[aws:secretAccessKey]', 'Secret Access Key').trim()
+);
+

+ 10 - 0
form/admin/fb.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[fb:appId]').trim().is(/^\d+$/),
+  field('settingForm[fb:seret]').trim().is(/^[\da-z]+$/)
+);
+

+ 10 - 0
form/admin/google.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[google:clientId]').trim().is(/^[\d\.a-z]+$/),
+  field('settingForm[google:clientSecret]').trim().is(/^[\da-zA-Z\-]+$/)
+);
+

+ 10 - 0
form/admin/sec.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[sec:registrationMode]').required(),
+  field('settingForm[sec:registrationWhiteList]')
+);
+

+ 7 - 0
form/index.js

@@ -5,3 +5,10 @@ exports.me = {
   user: require('./me/user'),
   user: require('./me/user'),
   password: require('./me/password')
   password: require('./me/password')
 };
 };
+exports.admin = {
+  app: require('./admin/app'),
+  sec: require('./admin/sec'),
+  aws: require('./admin/aws'),
+  google: require('./admin/google'),
+  fb: require('./admin/fb'),
+};

+ 1 - 1
lib/googleAuth.js

@@ -5,7 +5,7 @@
 'use strict';
 'use strict';
 
 
 var googleapis = require('googleapis');
 var googleapis = require('googleapis');
-var config   = require('config');
+var config   = app.use('config');
 
 
 function createOauth2Client(url) {
 function createOauth2Client(url) {
   return new googleapis.auth.OAuth2Client(
   return new googleapis.auth.OAuth2Client(

+ 94 - 0
lib/middlewares.js

@@ -1,3 +1,71 @@
+var debug = require('debug')('crowi:lib:middlewares')
+  ;
+
+exports.loginChecker = function(app, models) {
+  return function(req, res, next) {
+    // session に user object が入ってる
+    if (req.session.user && '_id' in req.session.user) {
+      models.User.findById(req.session.user._id, function(err, userData) {
+        if (err) {
+          next()
+        } else {
+          req.user = req.session.user = userData;
+          res.locals({user: req.user});
+          next();
+        }
+      });
+    } else {
+      req.user = req.session.user = false;
+      res.locals({user: req.user});
+      next();
+    }
+  };
+};
+
+exports.swigFunctions = function(app) {
+  return function(req, res, next) {
+    res.locals(require('../lib/swig_functions')(app));
+    next();
+  };
+};
+
+exports.swigFilters = function(app, swig) {
+  return function(req, res, next) {
+
+    swig.setFilter('path2name', function(string) {
+      return string.replace(/.+\/(.+)?$/, "$1");
+    });
+
+    swig.setFilter('datetz', function(input, format) {
+      // timezone
+      var swigFilters = require('swig/lib/filters')
+      return swigFilters.date(input, format, app.get('tzoffset'));
+    });
+
+    swig.setFilter('presentation', function(string) {
+      // 手抜き
+      return string.replace(/[\n]+#/g, "\n\n\n#");
+    });
+
+    swig.setFilter('picture', function(user) {
+      if (!user) {
+        return '';
+      }
+
+      user.fbId = user.userId; // migration
+      if (user.image && user.image != '/images/userpicture.png') {
+        return user.image;
+      } else if (user.fbId) {
+        return '//graph.facebook.com/' + user.fbId + '/picture?size=square';
+      } else {
+        return '/images/userpicture.png';
+      }
+    });
+
+    next();
+  };
+};
+
 exports.adminRequired = function() {
 exports.adminRequired = function() {
   return function(req, res, next) {
   return function(req, res, next) {
     if (req.user && '_id' in req.user) {
     if (req.user && '_id' in req.user) {
@@ -25,3 +93,29 @@ exports.loginRequired = function() {
     return res.redirect('/login');
     return res.redirect('/login');
   };
   };
 };
 };
+
+// this is for Installer
+exports.applicationNotInstalled = function() {
+  return function(req, res, next) {
+    var config = req.config;
+
+    debug("config.crowi", Object.keys(config.crowi).length);
+    if (Object.keys(config.crowi).length != 0) {
+      return res.render('500', { error: 'Application already installed.' });
+    }
+
+    return next();
+  };
+};
+
+exports.applicationInstalled = function() {
+  return function(req, res, next) {
+    var config = req.config;
+
+    if (Object.keys(config.crowi).length == 0) {
+      return res.redirect('/installer');
+    }
+
+    return next();
+  };
+};

+ 1 - 2
models/bookmark.js

@@ -1,4 +1,4 @@
-module.exports = function(models) {
+module.exports = function(app, models) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , debug = require('debug')('crowi:models:bookmark')
     , debug = require('debug')('crowi:models:bookmark')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
@@ -10,7 +10,6 @@ module.exports = function(models) {
     createdAt: { type: Date, default: Date.now() }
     createdAt: { type: Date, default: Date.now() }
   });
   });
 
 
-
   // bookmark チェック用
   // bookmark チェック用
   bookmarkSchema.statics.findByPageIdAndUser = function(pageId, user, callback) {
   bookmarkSchema.statics.findByPageIdAndUser = function(pageId, user, callback) {
     var Bookmark = this;
     var Bookmark = this;

+ 147 - 0
models/config.js

@@ -0,0 +1,147 @@
+module.exports = function(app) {
+  var mongoose = require('mongoose')
+    , debug = require('debug')('crowi:models:config')
+    , ObjectId = mongoose.Schema.Types.ObjectId
+    , configSchema
+    , Config
+  ;
+
+  configSchema = new mongoose.Schema({
+    ns: { type: String, required: true, index: true },
+    key: { type: String, required: true, index: true },
+    value: { type: String, required: true }
+  });
+
+  function getArrayForInstalling()
+  {
+    return {
+      'app:title'         : 'Crowi',
+      'app:confidential'  : '',
+
+      'security:registrationMode'      : 'Open',
+      'security:registrationWhiteList' : [],
+
+      'aws:bucket'          : 'crowi',
+      'aws:region'          : 'ap-northeast-1',
+      'aws:accessKeyId'     : '',
+      'aws:secretAccessKey' : '',
+
+      'searcher:url': '',
+
+      'google:clientId'     : '',
+      'google:clientSecret' : '',
+
+      'facebook:appId'  : '',
+      'facebook:secret' : '',
+    };
+  }
+
+  configSchema.statics.updateConfigCache = function(ns, config)
+  {
+    var originalConfig = app.set('config');
+    var newNSConfig = originalConfig[ns] || {};
+    Object.keys(config).forEach(function (key) {
+      if (config[key] || config[key] == '') {
+        newNSConfig[key] = config[key];
+      }
+    });
+
+    originalConfig[ns] = newNSConfig;
+    app.set('config', originalConfig);
+  };
+
+  // Execute only once for installing application
+  configSchema.statics.applicationInstall = function(callback)
+  {
+    var Config = this;
+    Config.count({ ns: 'crowi' }, function (err, count) {
+      if (count > 0) {
+        return callback(new Error('Application already installed'), null);
+      }
+      Config.updateNamespaceByArray('crowi', getArrayForInstalling(), function(err, configs) {
+        return callback(err, configs);
+      });
+    });
+  };
+
+  configSchema.statics.setupCofigFormData = function(ns, config)
+  {
+    var defaultConfig;
+    if (ns == 'crowi') {
+      defaultConfig  = getArrayForInstalling();
+    }
+    Object.keys(config[ns]).forEach(function (key) {
+      if (config[ns][key]) {
+        defaultConfig[key] = config[ns][key];
+      }
+    });
+    return defaultConfig;
+  };
+
+
+  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback)
+  {
+    var Config = this;
+    if (configs.length < 0) {
+      return callback(new Error('Argument #1 is not array.'), null);
+    }
+
+    Object.keys(configs).forEach(function (key) {
+      var value = configs[key];
+
+      Config.findOneAndUpdate(
+        { ns: ns, key: key },
+        { ns: ns, key: key, value: JSON.stringify(value) },
+        { upsert: true, },
+        function (err, config) {
+          debug('Config.findAndUpdate', err, config);
+      });
+    });
+
+    return callback(null, configs);
+  };
+
+  configSchema.statics.findAndUpdate = function(ns, key, value, callback)
+  {
+    var Config = this;
+    Config.findOneAndUpdate(
+      { ns: ns, key: key },
+      { ns: ns, key: key, value: JSON.stringify(value) },
+      { upsert: true, },
+      function (err, config) {
+        debug('Config.findAndUpdate', err, config);
+        callback(err, config);
+    });
+  };
+
+  configSchema.statics.getConfig = function(callback)
+  {
+  };
+
+  configSchema.statics.getConfigArray = function(callback)
+  {
+    var Config = this
+      , config = {};
+    config.crowi = {}; // crowi namespace
+
+    Config.find()
+      .sort({ns: 1, key: 1})
+      .exec(function(err, doc) {
+
+        doc.forEach(function(el) {
+          if (!config[el.ns]) {
+            config[el.ns] = {};
+          }
+          config[el.ns][el.key] = JSON.parse(el.value);
+        });
+
+        debug("config data", config);
+        return callback(null, config);
+      });
+  };
+
+
+  Config = mongoose.model('Config', configSchema);
+
+  return Config;
+};

+ 4 - 4
models/index.js

@@ -1,10 +1,10 @@
 module.exports = function(app) {
 module.exports = function(app) {
   var models = {};
   var models = {};
 
 
-  require('./page')(models);
-  require('./user')(models);
-  require('./revision')(models);
-  require('./bookmark')(models);
+  require('./page')(app, models);
+  require('./user')(app, models);
+  require('./revision')(app, models);
+  require('./bookmark')(app, models);
 
 
   app.set('models', models);
   app.set('models', models);
 
 

+ 14 - 12
models/page.js

@@ -1,4 +1,4 @@
-module.exports = function(models) {
+module.exports = function(app, models) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , debug = require('debug')('crowi:models:page')
     , debug = require('debug')('crowi:models:page')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
@@ -28,13 +28,13 @@ module.exports = function(models) {
   }
   }
 
 
   pageSchema = new mongoose.Schema({
   pageSchema = new mongoose.Schema({
-    path: { type: String, required: true },
+    path: { type: String, required: true, index: true },
     revision: { type: ObjectId, ref: 'Revision' },
     revision: { type: ObjectId, ref: 'Revision' },
-    redirectTo: String,
-    grant: { type: Number, default: GRANT_PUBLIC },
+    redirectTo: { type: String, index: true },
+    grant: { type: Number, default: GRANT_PUBLIC, index: true },
     grantedUsers: [{ type: ObjectId, ref: 'User' }],
     grantedUsers: [{ type: ObjectId, ref: 'User' }],
-    liker: [{ type: ObjectId, ref: 'User' }],
-    seenUsers: [{ type: ObjectId, ref: 'User' }],
+    liker: [{ type: ObjectId, ref: 'User', index: true }],
+    seenUsers: [{ type: ObjectId, ref: 'User', index: true }],
     createdAt: { type: Date, default: Date.now },
     createdAt: { type: Date, default: Date.now },
     updatedAt: Date
     updatedAt: Date
   });
   });
@@ -189,17 +189,19 @@ module.exports = function(models) {
       /^\/\-\/.*/,
       /^\/\-\/.*/,
       /^\/_r\/.*/,
       /^\/_r\/.*/,
       /.+\/edit$/,
       /.+\/edit$/,
-      /^\/(register|login|logout|admin|me|files|trash|paste|comments)/,
+      /^\/(register|login|logout|admin|me|files|trash|paste|comments).+/,
     ];
     ];
 
 
-    forbiddenPages.forEach(function(pi) {
-      var page = forbiddenPages[pi];
-      if (name.match(page)) {
-        return false;
+    var isCreatable = true;
+    forbiddenPages.forEach(function(page) {
+      var pageNameReg = new RegExp(page);
+      if (name.match(pageNameReg)) {
+        isCreatable = false;
+        return ;
       }
       }
     });
     });
 
 
-    return true;
+    return isCreatable;
   };
   };
 
 
   pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
   pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {

+ 1 - 1
models/revision.js

@@ -1,4 +1,4 @@
-module.exports = function(models) {
+module.exports = function(app, models) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
     , revisionSchema;
     , revisionSchema;

+ 6 - 6
models/user.js

@@ -1,9 +1,9 @@
-module.exports = function(models) {
+module.exports = function(app, models) {
   var mongoose = require('mongoose')
   var mongoose = require('mongoose')
     , mongoosePaginate = require('mongoose-paginate')
     , mongoosePaginate = require('mongoose-paginate')
     , debug = require('debug')('crowi:models:user')
     , debug = require('debug')('crowi:models:user')
     , crypto = require('crypto')
     , crypto = require('crypto')
-    , config = require('config')
+    , config = app.set('config')
     , ObjectId = mongoose.Schema.Types.ObjectId
     , ObjectId = mongoose.Schema.Types.ObjectId
 
 
     , STATUS_REGISTERED = 1
     , STATUS_REGISTERED = 1
@@ -32,19 +32,19 @@ module.exports = function(models) {
 
 
   function decideUserStatusOnRegistration () {
   function decideUserStatusOnRegistration () {
     // status decided depends on registrationMode
     // status decided depends on registrationMode
-    switch (config.security.registrationMode) {
+    switch (config.crowi['security.registrationMode']) {
       case 'Open':
       case 'Open':
         return STATUS_ACTIVE;
         return STATUS_ACTIVE;
       case 'Restricted':
       case 'Restricted':
         return STATUS_REGISTERED;
         return STATUS_REGISTERED;
       default:
       default:
-        return STATUS_REGISTERED; // どっちにすんのがいいんだろうな
+        return STATUS_ACTIVE; // どっちにすんのがいいんだろうな
     }
     }
   }
   }
 
 
   function generatePassword (password) {
   function generatePassword (password) {
     var hasher = crypto.createHash('sha256');
     var hasher = crypto.createHash('sha256');
-    hasher.update(config.security.passwordSeed + password);
+    hasher.update(process.env.PASSWORD_SEED + password);
 
 
     return hasher.digest('hex');
     return hasher.digest('hex');
   }
   }
@@ -198,7 +198,7 @@ module.exports = function(models) {
   };
   };
 
 
   userSchema.statics.isEmailValid = function(email, callback) {
   userSchema.statics.isEmailValid = function(email, callback) {
-    return config.security.registrationWhiteList.some(function(allowedEmail) {
+    return config.crowi['security.registrationWhiteList'].some(function(allowedEmail) {
       var re = new RegExp(allowedEmail + '$');
       var re = new RegExp(allowedEmail + '$');
 
 
       return re.test(email);
       return re.test(email);

+ 11 - 8
package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "crowi",
   "name": "crowi",
   "version": "1.0.3",
   "version": "1.0.3",
-  "description": "Crowi - The Simple & Powerful Communication Tool Based on Wiki",
+  "description": "Crocos' Wiki implementation in node.js",
   "tags": [
   "tags": [
     "wiki"
     "wiki"
   ],
   ],
@@ -11,7 +11,7 @@
   ],
   ],
   "repository": {
   "repository": {
     "type": "git",
     "type": "git",
-    "url": "https://github.com/crowi/crowi.git"
+    "url": "https://github.com/crocos/crocos-wiki.git"
   },
   },
   "engines": {
   "engines": {
     "node": "0.10.x",
     "node": "0.10.x",
@@ -20,7 +20,6 @@
   "dependencies": {
   "dependencies": {
     "async": "=0.1.18",
     "async": "=0.1.18",
     "aws-sdk": "~2.0.0-rc.19",
     "aws-sdk": "~2.0.0-rc.19",
-    "config": "=0.4.9",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "consolidate": "=0.10.0",
     "consolidate": "=0.10.0",
     "debug": "^1.0.3",
     "debug": "^1.0.3",
@@ -35,9 +34,7 @@
     "socket.io-client": "~0.9.16",
     "socket.io-client": "~0.9.16",
     "swig": "=1.3.2",
     "swig": "=1.3.2",
     "time": "=0.10.0",
     "time": "=0.10.0",
-    "mongoose-paginate": "~3.1.0"
-  },
-  "devDependencies": {
+    "mongoose-paginate": "~3.1.0",
     "grunt": "~0.4.1",
     "grunt": "~0.4.1",
     "grunt-contrib-concat": "~0.3.0",
     "grunt-contrib-concat": "~0.3.0",
     "grunt-contrib-jshint": "^0.10.0",
     "grunt-contrib-jshint": "^0.10.0",
@@ -46,6 +43,8 @@
     "grunt-sass": "~0.13.1",
     "grunt-sass": "~0.13.1",
     "reveal.js": "~2.6.2"
     "reveal.js": "~2.6.2"
   },
   },
+  "devDependencies": {
+  },
   "license": [
   "license": [
     {
     {
       "type": "MIT",
       "type": "MIT",
@@ -53,9 +52,13 @@
     }
     }
   ],
   ],
   "scripts": {
   "scripts": {
-    "test": "//"
+    "start": "node app.js",
+    "postinstall": "bower install && grunt build"
+  },
+  "env": {
+    "NODE_ENV": "production"
   },
   },
   "bugs": {
   "bugs": {
-    "url": "https://github.com/crowi/crowi/issues"
+    "url": "https://github.com/crocos/crocos-wiki/issues"
   }
   }
 }
 }

BIN
public/favicon.ico


BIN
public/logo/100x11_g.png


BIN
public/logo/100x11_w.png


BIN
public/logo/128x128.png


BIN
public/logo/135x32.png


BIN
public/logo/138x32.png


BIN
public/logo/16x16.ico


BIN
public/logo/16x16.png


BIN
public/logo/200x200.png


BIN
public/logo/32x32.png


BIN
public/logo/471x110.png


BIN
public/logo/800x200.png


+ 13 - 1
resource/css/_layout.scss

@@ -374,7 +374,19 @@
 } // }}}
 } // }}}
 
 
 .crowi.single.nologin { // {{{
 .crowi.single.nologin { // {{{
-  background: lighten($crowiHeaderBackground, 15%);
+  //background: lighten($crowiHeaderBackground, 15%);
+  background: $crowiHeaderBackground;
+
+  .installer-header {
+    margin-top: 100px;
+    text-align: center;
+
+    h1 {
+      margin: 40px 0 40px;
+      color: #fff;
+      font-size: 1.6em;
+    }
+  }
 
 
   .main {
   .main {
     margin: 0;
     margin: 0;

+ 32 - 0
routes/admin.js

@@ -5,6 +5,7 @@ module.exports = function(app) {
     , models = app.set('models')
     , models = app.set('models')
     , Page = models.Page
     , Page = models.Page
     , User = models.User
     , User = models.User
+    , Config = models.Config
 
 
     , MAX_PAGE_LIST = 5
     , MAX_PAGE_LIST = 5
     , actions = {};
     , actions = {};
@@ -66,6 +67,20 @@ module.exports = function(app) {
     return res.render('admin/index');
     return res.render('admin/index');
   };
   };
 
 
+  actions.app = {};
+  actions.app.index = function(req, res) {
+    var settingForm;
+    settingForm = Config.setupCofigFormData('crowi', req.config);
+
+    debug('settingForm', settingForm);
+    return res.render('admin/app', {
+      settingForm: settingForm,
+    });
+  };
+
+  actions.app.settingUpdate = function(req, res) {
+  };
+
   actions.user = {};
   actions.user = {};
   actions.user.index = function(req, res) {
   actions.user.index = function(req, res) {
     var page = parseInt(req.query.page) || 0;
     var page = parseInt(req.query.page) || 0;
@@ -140,6 +155,23 @@ module.exports = function(app) {
     });
     });
   };
   };
 
 
+  actions.api = {};
+  actions.api.appSetting = function(req, res) {
+    var form = req.body.settingForm;
+
+    debug("posted form", req.form);
+    debug("posted form", form);
+    if (req.form.isValid) {
+      Config.updateNamespaceByArray('crowi', form, function(err, config) {
+        Config.updateConfigCache('crowi', config)
+        return res.json({status: true});
+      });
+    } else {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+  };
+
+
   return actions;
   return actions;
 };
 };
 
 

+ 25 - 11
routes/index.js

@@ -1,18 +1,24 @@
 module.exports = function(app) {
 module.exports = function(app) {
   var middleware = require('../lib/middlewares')
   var middleware = require('../lib/middlewares')
-    , form = require('../form')
-    , page = require('./page')(app)
-    , login = require('./login')(app)
-    , logout = require('./logout')(app)
-    , me = require('./me')(app)
-    , admin = require('./admin')(app)
-    , user = require('./user')(app);
+    , 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);
 
 
   app.get('/'                        , middleware.loginRequired() , page.pageListShow);
   app.get('/'                        , middleware.loginRequired() , page.pageListShow);
-  app.get('/login'                   , login.login);
-  app.post('/login'                  , form.login                 , login.login);
-  app.post('/register'               , form.register              , login.register);
-  app.get('/register'                , login.register);
+
+  app.get('/installer'               , middleware.applicationNotInstalled() , installer.index);
+  app.post('/installer/createAdmin'  , middleware.applicationNotInstalled() , form.register , installer.createAdmin);
+  //app.post('/installer/user'         , middleware.applicationNotInstalled() , installer.createFirstUser);
+
+  app.get('/login'                   , middleware.applicationInstalled()    , login.login);
+  app.post('/login'                  , form.login                           , login.login);
+  app.post('/register'               , form.register                        , login.register);
+  app.get('/register'                , middleware.applicationInstalled()    , login.register);
   app.post('/register/google'        , login.registerGoogle);
   app.post('/register/google'        , login.registerGoogle);
   app.get('/google/callback'         , login.googleCallback);
   app.get('/google/callback'         , login.googleCallback);
   app.get('/login/google'            , login.loginGoogle);
   app.get('/login/google'            , login.loginGoogle);
@@ -20,6 +26,14 @@ module.exports = function(app) {
   app.get('/logout'                  , logout.logout);
   app.get('/logout'                  , logout.logout);
 
 
   app.get('/admin'                      , middleware.loginRequired() , middleware.adminRequired() , admin.index);
   app.get('/admin'                      , middleware.loginRequired() , middleware.adminRequired() , admin.index);
+  app.get('/admin/app'                  , middleware.loginRequired() , middleware.adminRequired() , admin.app.index);
+  app.post('/_api/admin/settings/app'   , middleware.loginRequired() , middleware.adminRequired() , form.admin.app, admin.api.appSetting);
+  app.post('/_api/admin/settings/sec'   , middleware.loginRequired() , middleware.adminRequired() , form.admin.sec, admin.api.appSetting);
+  app.post('/_api/admin/settings/aws'   , middleware.loginRequired() , middleware.adminRequired() , form.admin.aws, admin.api.appSetting);
+  app.post('/_api/admin/settings/google', middleware.loginRequired() , middleware.adminRequired() , form.admin.google, admin.api.appSetting);
+  app.post('/_api/admin/settings/fb'    , middleware.loginRequired() , middleware.adminRequired() , form.admin.fb
+  , admin.api.appSetting);
+
   app.get('/admin/users'                , middleware.loginRequired() , middleware.adminRequired() , admin.user.index);
   app.get('/admin/users'                , middleware.loginRequired() , middleware.adminRequired() , admin.user.index);
   app.post('/admin/user/:id/makeAdmin'  , middleware.loginRequired() , middleware.adminRequired() , admin.user.makeAdmin);
   app.post('/admin/user/:id/makeAdmin'  , middleware.loginRequired() , middleware.adminRequired() , admin.user.makeAdmin);
   app.post('/admin/user/:id/removeFromAdmin', middleware.loginRequired() , middleware.adminRequired() , admin.user.removeFromAdmin);
   app.post('/admin/user/:id/removeFromAdmin', middleware.loginRequired() , middleware.adminRequired() , admin.user.removeFromAdmin);

+ 49 - 0
routes/installer.js

@@ -0,0 +1,49 @@
+module.exports = function(app) {
+  'use strict';
+
+  var debug = require('debug')('crowi:routes:installer')
+    , models = app.set('models')
+    , Config = models.Config
+    , User = models.User
+
+    , actions = {};
+
+  actions.index = function(req, res) {
+    return res.render('installer');
+  };
+
+  actions.createAdmin = function(req, res) {
+    var registerForm = req.body.registerForm || {};
+
+    if (req.form.isValid) {
+      var name = registerForm.name;
+      var username = registerForm.username;
+      var email = registerForm.email;
+      var password = registerForm.password;
+
+      User.createUserByEmailAndPassword(name, username, email, password, function(err, userData) {
+        if (err) {
+          // TODO
+          return ;
+        }
+        userData.makeAdmin(function(err, userData) {
+          Config.applicationInstall(function(err, configs) {
+            if (err) {
+              // TODO
+              return ;
+            }
+
+            // login処理
+            req.user = req.session.user = userData;
+            req.flash('successMessage', 'Crowi のインストールが完了しました!はじめに、このページでこの Wiki の各種設定を確認してください。');
+            return res.redirect('admin/app');
+          });
+        });
+      });
+    } else {
+      return res.render('installer');
+    }
+  };
+
+  return actions;
+};

+ 2 - 2
routes/login.js

@@ -4,7 +4,7 @@ module.exports = function(app) {
   var googleapis = require('googleapis')
   var googleapis = require('googleapis')
     , debug = require('debug')('crowi:routes:login')
     , debug = require('debug')('crowi:routes:login')
     , models = app.set('models')
     , models = app.set('models')
-    , config = require('config')
+    , config = app.set('config')
     , Page = models.Page
     , Page = models.Page
     , User = models.User
     , User = models.User
     , Revision = models.Revision
     , Revision = models.Revision
@@ -119,7 +119,7 @@ module.exports = function(app) {
     }
     }
 
 
     // config で closed ならさよなら
     // config で closed ならさよなら
-    if (config.security.registrationMode == 'Closed') {
+    if (config.crowi['security:registrationMode'] == 'Closed') {
       return res.redirect('/');
       return res.redirect('/');
     }
     }
 
 

+ 4 - 2
routes/logout.js

@@ -2,9 +2,11 @@ module.exports = function(app) {
   return {
   return {
     logout: function(req, res) {
     logout: function(req, res) {
 
 
-      req.facebook.destroySession();
-      req.session.destroy();
+      if (req.facebook) {
+        req.facebook.destroySession();
+      }
 
 
+      req.session.destroy();
       return res.redirect('/');
       return res.redirect('/');
     }
     }
   };
   };

+ 7 - 3
routes/page.js

@@ -114,9 +114,13 @@ module.exports = function(app) {
         return res.redirect('/');
         return res.redirect('/');
       }
       }
 
 
-      pageData.seen(req.user, function(err, data) {
-        return renderPage(data, req, res);
-      });
+      if (pageData) {
+        pageData.seen(req.user, function(err, data) {
+          return renderPage(data, req, res);
+        });
+      } else {
+          return renderPage(null, req, res);
+      }
     });
     });
   };
   };
 
 

+ 0 - 12
views/500.html

@@ -1,13 +1 @@
-{% extends 'layout.html' %}
-
-{% block content_head %}
-<header>
-  <h2>Error</h2>
-</header>
-{% endblock %}
-
-{% block content_main %}
-
 Error: {{ error }}
 Error: {{ error }}
-
-{% endblock %}

+ 269 - 0
views/admin/app.html

@@ -0,0 +1,269 @@
+{% extends '../layout/2column.html' %}
+
+{% block html_title %}アプリ設定 · {% endblock %}
+
+{% block content_head %}
+<header id="page-header">
+  <h1 class="title" id="">アプリ設定</h1>
+</header>
+{% endblock %}
+
+{% block content_main %}
+<div class="content-main">
+  {% set smessage = req.flash('successMessage') %}
+  {% if smessage.length %}
+  <div class="alert alert-success">
+    {{ smessage }}
+  </div>
+  {% endif %}
+
+  {% set emessage = req.flash('errorMessage') %}
+  {% if emessage.length %}
+  <div class="alert alert-danger">
+    {{ emessage }}
+  </div>
+  {% endif %}
+
+  <div class="row">
+    <div class="col-md-3">
+      <ul class="nav nav-pills nav-stacked">
+        <li><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
+        <li class="active"><a href="/admin/app"><i class="fa fa-gears"></i> アプリ設定</a></li>
+        <li><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
+      </ul>
+    </div>
+    <div class="col-md-9">
+
+      <form action="/_api/admin/settings/app" method="post" class="form-horizontal" id="appSettingForm" role="form">
+      <fieldset>
+        <legend>アプリ設定</legend>
+        <div class="form-group">
+          <label for="settingForm[app:title]" class="col-xs-3 control-label">Wikiの名前</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[app:title]" value="{{ settingForm['app:title'] }}">
+
+            <p class="help-block">ヘッダーやHTMLタイトルに使用されるWikiの名前を変更できます。</p>
+            </p>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[app:confidential]" class="col-xs-3 control-label">コンフィデンシャル表示</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[app:confidential]" value="{{ settingForm['app:confidential'] }}" placeholder="例: 社外秘">
+            <p class="help-block">ここに入力した内容は、ヘッダー等に表示されます。</p>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-3 col-xs-6">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+      </fieldset>
+      </form>
+
+      <form action="/_api/admin/settings/sec" method="post" class="form-horizontal" id="secSettingForm" role="form">
+      <fieldset>
+      <legend>セキュリティ設定</legend>
+
+        <div class="form-group">
+          <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">公開設定</label>
+          <div class="col-xs-6">
+            <select class="form-control" name="settingForm[security:registrationMode]" value="{{ settingForm['security:registrationMode'] }}">
+              <option value="Open">公開 (だれでも登録可能)</option>
+              <option value="Resricted">制限 (登録完了には管理者の承認が必要)</option>
+              {# <option value="Closed">非公開 (登録には管理者による招待が必要)</option> #}
+            </select>
+            <p class="help-block">ここに入力した内容は、ヘッダー等に表示されます。</p>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[security:registrationWhiteList]" class="col-xs-3 control-label">登録許可メールアドレスの<br>ホワイトリスト</label>
+          <div class="col-xs-6">
+            <textarea class="form-control" type="textarea" name="settingForm[security:registrationWhiteList]" placeholder="例: @crowi.wiki">{{ settingForm['security:registrationWhiteList']|join('\n') }}</textarea>
+            <p class="help-block">登録可能なメールアドレスを制限することができます。例えば、会社で使う場合、<code>@crowi.wiki</code> などと記載すると、その会社のメールアドレスを持っている人のみ登録可能になります。<br>
+            1行に1メールアドレス入力してください。</p>
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-3 col-xs-6">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
+
+      <form action="/_api/admin/settings/aws" method="post" class="form-horizontal" id="awsSettingForm" role="form">
+      <fieldset>
+      <legend>AWS設定</legend>
+        <p class="well">S3 にアクセスするための設定を行います。AWS の設定を完了させると、ファイルアップロード機能、プロフィール写真機能などが有効になります。<br>
+          <br>
+
+          <span class="text-danger"><i class="fa fa-warning"></i> この設定を途中で変更すると、これまでにアップロードしたファイル等へのアクセスができなくなりますのでご注意下さい。</span>
+        </p>
+
+        <div class="form-group">
+          <label for="settingForm[app.region]" class="col-xs-3 control-label">リージョン</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[aws:region]" placeholder="例: ap-northeast-1" value="{{ settingForm['aws:region'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[aws:bucket]" class="col-xs-3 control-label">バケット名</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[aws:bucket]" placeholder="例: crowi"  value="{{ settingForm['aws:bucket'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[aws:accessKeyId]" class="col-xs-3 control-label">Access Key ID</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[aws:accessKeyId]" value="{{ settingForm['aws:accessKeyId'] }}">
+          </div>
+
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[aws:secretAccessKey]" class="col-xs-3 control-label">Secret Access Key</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[aws:secretAccessKey]" value="{{ settingForm['aws:secretAccessKey'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-3 col-xs-6">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
+
+      <form action="/_api/admin/settings/google" method="post" class="form-horizontal" id="googleSettingForm" role="form">
+      <fieldset>
+      <legend>Google 設定</legend>
+        <p class="well">Google プロジェクトの設定をすると、Google アカウントにコネクトして登録やログインが可能になります。</p>
+
+        <div class="form-group">
+          <label for="settingForm[google:clientId]" class="col-xs-3 control-label">Client ID</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[google:clientId]" value="{{ settingForm['google:clientId'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[google:clientSecret]" class="col-xs-3 control-label">Client Secret</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[google:clientSecret]" value="{{ settingForm['google:clientSecret'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-3 col-xs-6">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
+
+      <form action="/_api/admin/settings/fb" method="post" class="form-horizontal" id="fbSettingForm" role="form">
+      <fieldset>
+      <legend>Facebook 設定</legend>
+        <p class="well">Facebook アプリケーションの設定をすると、Facebook にコネクトして登録やログインが可能になります。</p>
+
+        <div class="form-group">
+          <label for="settingForm[facebook:appId]" class="col-xs-3 control-label">facebook ID</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[facebook:appId]" value="{{ settingForm['facebook:appId'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <label for="settingForm[facebook:secret]" class="col-xs-3 control-label">Secret</label>
+          <div class="col-xs-6">
+            <input class="form-control" type="text" name="settingForm[facebook:secret]" value="{{ settingForm['facebook:secret'] }}">
+          </div>
+        </div>
+
+        <div class="form-group">
+          <div class="col-xs-offset-3 col-xs-6">
+            <button type="submit" class="btn btn-primary">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
+    </div>
+  </div>
+
+  <script>
+    $(function()
+    {
+      $('#appSettingForm, #secSettingForm, #awsSettingForm, #googleSettingForm, #fbSettingForm').each(function() {
+        $(this).submit(function()
+        {
+          function showMessage(formId, msg, status) {
+            $('#' + formId + ' .alert').remove();
+
+            if (!status) {
+              status = 'success';
+            }
+            var $message = $('<p class="alert"></p>');
+            $message.addClass('alert-' + status);
+            $message.html(msg.replace('\n', '<br>'));
+            $message.insertAfter('#' + formId + ' legend');
+
+            if (status == 'success') {
+              setTimeout(function()
+              {
+                $message.fadeOut({
+                  complete: function() {
+                    $message.remove();
+                  }
+                });
+              }, 5000);
+            }
+          }
+
+          var $form = $(this);
+          var $id = $form.attr('id');
+          var $button = $('button', this);
+          $button.attr('disabled', 'disabled');
+          var jqxhr = $.post($form.attr('action'), $form.serialize(), function(data)
+            {
+              console.log(data);
+              if (data.status) {
+                showMessage($id, '更新しました');
+              } else {
+                showMessage($id, data.message, 'danger');
+              }
+            })
+            .fail(function() {
+              showMessage($id, 'エラーが発生しました', 'danger');
+            })
+            .always(function() {
+              $button.removeAttr('disabled');
+          });
+          return false;
+        });
+      });
+
+    });
+  </script>
+
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}
+
+{% block footer %}
+{% endblock footer %}
+
+

+ 1 - 0
views/admin/index.html

@@ -14,6 +14,7 @@
     <div class="col-md-3">
     <div class="col-md-3">
       <ul class="nav nav-pills nav-stacked">
       <ul class="nav nav-pills nav-stacked">
         <li class="active"><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
         <li class="active"><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
+        <li><a href="/admin/app"><i class="fa fa-gears"></i> アプリ設定</a></li>
         <li><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
         <li><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
       </ul>
       </ul>
     </div>
     </div>

+ 1 - 0
views/admin/users.html

@@ -28,6 +28,7 @@
     <div class="col-md-3">
     <div class="col-md-3">
       <ul class="nav nav-pills nav-stacked">
       <ul class="nav nav-pills nav-stacked">
         <li><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
         <li><a href="/admin"><i class="fa fa-cube"></i> Wiki管理トップ</a></li>
+        <li><a href="/admin/app"><i class="fa fa-gears"></i> アプリ設定</a></li>
         <li class="active"><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
         <li class="active"><a href="/admin/users"><i class="fa fa-users"></i> ユーザー管理</a></li>
       </ul>
       </ul>
     </div>
     </div>

+ 94 - 0
views/installer.html

@@ -0,0 +1,94 @@
+{% extends 'layout/single-nologin.html' %}
+
+{% block html_title %}セットアップ {% endblock %}
+
+{% block content_main %}
+
+
+<div class="login-dialog-container col-md-5">
+
+<div class="installer-header">
+  <img src="/logo/135x32.png" alt="Crowi">
+  <h1>
+    Crowi のセットアップへようこそ!
+  </h1>
+</div>
+
+
+<div class="login-dialog"  id="login-dialog">
+  <div class="login-dialog-inner">
+    <h2>管理者の作成</h2>
+
+    <p class="text-info">
+    はじめに、管理者アカウントを作成してください。
+    </p>
+
+    {% if req.form.errors.length > 0 %}
+    <div class="alert alert-danger">
+      <ul>
+      {% for error in req.form.errors %}
+        <li>{{ error }}</li>
+      {% endfor %}
+      </ul>
+    </div>
+    {% endif %}
+
+    <form role="form" action="/installer/createAdmin" method="post">
+      <label>ユーザーID</label>
+      <div class="input-group" id="input-group-username">
+        <span class="input-group-addon"><strong>@</strong></span>
+        <input type="text" class="form-control" placeholder="記入例: taroyama" name="registerForm[username]" value="{{ req.body.registerForm.username }}" required>
+      </div>
+      <p class="help-block">
+      <span id="help-block-username" class="text-danger"></span>
+      ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。
+      </p>
+
+      <label>名前</label>
+      <div class="input-group">
+        <span class="input-group-addon"><i class="fa fa-user"></i></span>
+        <input type="text" class="form-control" placeholder="記入例: 山田 太郎" name="registerForm[name]" value="{{ req.body.registerForm.name }}" required>
+      </div>
+
+      <label >メールアドレス</label>
+      <div class="input-group">
+        <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
+        <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
+      </div>
+
+      <label>パスワード</label>
+      <div class="input-group">
+        <span class="input-group-addon"><i class="fa fa-key"></i></span>
+        <input type="password" class="form-control" placeholder="Password" name="registerForm[password]" required>
+      </div>
+      <p class="help-block">
+      パスワードは6文字以上の半角英数字または記号
+      </p>
+
+      <input type="submit" class="btn btn-primary btn-lg btn-block" value="作成">
+    </form>
+
+  </div>
+</div>
+
+</div>
+
+<script>
+$(function() {
+  $('#register-form input[name="registerForm[username]"]').change(function(e) {
+    var username = $(this).val();
+    $('#input-group-username').removeClass('has-error');
+    $('#help-block-username').html("");
+
+    $.getJSON('/_api/check_username', {username: username}, function(json) {
+      if (!json.valid) {
+        $('#help-block-username').html('<i class="fa fa-warning"></i>このユーザーIDは利用できません。<br>');
+        $('#input-group-username').addClass('has-error');
+      }
+    });
+  });
+});
+</script>
+
+{% endblock %}
+

+ 7 - 99
views/layout/2column.html

@@ -4,7 +4,7 @@
 <nav class="crowi-header navbar navbar-default" role="navigation">
 <nav class="crowi-header navbar navbar-default" role="navigation">
   <!-- Brand and toggle get grouped for better mobile display -->
   <!-- Brand and toggle get grouped for better mobile display -->
   <div class="navbar-header">
   <div class="navbar-header">
-    <a class="navbar-brand" href="/">{% block title %}{{ config.app.title }}{% endblock %}</a>
+    <a class="navbar-brand" href="/">{% block title %}{{ config.crowi['app:title'] }}{% endblock %}</a>
   </div>
   </div>
 
 
   <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
   <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
@@ -18,102 +18,8 @@
     <ul class="nav navbar-nav">
     <ul class="nav navbar-nav">
       <li class=""><a href="/INDEX">INDEX</a></li>
       <li class=""><a href="/INDEX">INDEX</a></li>
     </ul>
     </ul>
-    <form id="headerSearch" class="navbar-form navbar-left form-inline" role="search">
-      <div class="form-group">
-        <input id="searchQuery" name="q" type="text" class="form-control" placeholder="検索文字...">
-        <button type="submit" class="btn btn-default">検索</button>
-        <script>
-          function Searcher () {
-          };
-          Searcher.prototype = {
-            baseUrl: "{{ config.searcher.url }}",
-            currentQuery: "",
-            searchData: [],
-            setData: function (data) {
-              this.searchData = data;
-            },
-            search: function (query) {
-              var self = this;
-              self.currentQuery = query;
-              if (query == "") {
-                return false;
-              }
-              $("#searchQuery").addClass('searching');
-              $.ajax({
-                url: self.baseUrl + '/search?type=json&callback=?',
-                data: {q: query},
-                cache: false,
-                dataType: 'jsonp'
-              }).done(function (data) {
-                self.setData(data);
-                self.showSearchWidget();
-                $("#searchQuery").removeClass('searching');
-              });
-            },
-            showSearchWidget: function () {
-              if (this.searchData.length > 0) {
-                $('#headerSearch').popover('show');
-              }
-              $("#searchQuery").removeClass('searching');
-            },
-            createSearchContent: function () {
-              var self = this;
-              var contentHtml = $('<ul class=\"search-list\"></ul>');
-              $.each(self.searchData.slice(0, 6), function (i, d) {
-                var $li = $("<li class=\"list-link\"></li>");
-                var $a = $("<a></a>");
-                $a.attr('href', d.path).html(d.path + "<br>");
-                var $span = $("<span class=\"search-description\"></span>");
-                $span.text(d.body.substr(0, 50) + "...");
-                $a.append($span);
-                $li.append($a);
-                contentHtml.append($li);
-              });
-
-              var $li = $("<li class=\"divider\"></li>");
-              contentHtml.append($li);
-              $li = $("<li class=\"next-link\"></li>");
-              $li.html("<a href=\"" + self.baseUrl + "/search?q=" + encodeURIComponent(self.currentQuery) + "\">もっと見る</a>");
-              contentHtml.append($li);
-              return contentHtml;
-            },
-            jump: function (query) {
-              self = this;
-              top.location.href = self.baseUrl + '/search?q=' + query;
-            }
-          };
-          var SearcherObject = new Searcher();
-
-          $('#headerSearch').popover({
-            placement: 'bottom',
-            trigger: 'manual',
-            html: 'true',
-            content: function () {
-              return SearcherObject.createSearchContent();
-            }
-          });
-          $('#searchQuery').on('focus', function(e) {
-            SearcherObject.showSearchWidget();
-          });
-          $('#searchQuery').on('blur', function(e) {
-            $('#headerSearch').popover('hide');
-          });
-          $('#headerSearch').on('submit', function(e) {
-            SearcherObject.jump($("#searchQuery").val());
-            return false;
-          });
+    {% include '../widget/searcher.html' %}
 
 
-          var previousText = "";
-          setInterval(function (e) {
-            var text = $("#searchQuery").val();
-            if (text != previousText) {
-              SearcherObject.search(text);
-            }
-            previousText = text;
-          }, 1000);
-        </script>
-      </div>
-    </form>
     <ul class="nav navbar-nav navbar-right">
     <ul class="nav navbar-nav navbar-right">
       <li class="aside-shown">
       <li class="aside-shown">
         <a href="" class="layout-control to-show" id="toggle-sidebar-to-show"><i class="fa fa-caret-left"></i> サイドバー表示</a>
         <a href="" class="layout-control to-show" id="toggle-sidebar-to-show"><i class="fa fa-caret-left"></i> サイドバー表示</a>
@@ -179,8 +85,8 @@
       {% else %}
       {% else %}
       <li id="login-user"><a href="/login" id="login"><i class="fa fa-user"></i> Login</a></li>
       <li id="login-user"><a href="/login" id="login"><i class="fa fa-user"></i> Login</a></li>
       {% endif %}
       {% endif %}
-      {% if config.security.confidential != '' %}
-      <li class="confidential"><a href="#">{{ config.security.confidential }}</a></li>
+      {% if config.crowi['app:confidential'] && config.crowi['app:confidential'] != '' %}
+      <li class="confidential"><a href="#">{{ config.crowi['app:confidential'] }}</a></li>
       {% endif %}
       {% endif %}
     </ul>
     </ul>
   </div><!-- /.navbar-collapse -->
   </div><!-- /.navbar-collapse -->
@@ -213,7 +119,9 @@
 
 
   <div id="footer-container" class="footer">
   <div id="footer-container" class="footer">
     <footer class="">
     <footer class="">
-      <p>&copy; 2012 {{ config.app.title }}. <a href="" data-target="#helpModal" data-toggle="modal"><i class="fa fa-question-circle"></i> ヘルプ</a></p>
+    <p>
+    <a href="" data-target="#helpModal" data-toggle="modal"><i class="fa fa-question-circle"> ヘルプ</i></a>
+    &copy; {{ now|date('Y') }} {{ config.crowi['app:title'] }} <img src="/logo/100x11_g.png" alt="powered by Crowi"> </p>
     </footer>
     </footer>
   </div>
   </div>
 </aside>
 </aside>

+ 2 - 2
views/layout/layout.html

@@ -5,7 +5,7 @@
   <meta charset="utf-8">
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
   <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 
 
-  <title>{% block html_title %}{% endblock %} {{ config.app.title }}</title>
+  <title>{% block html_title %}{% endblock %} {{ config.crowi['app.title']|default('Crowi') }}</title>
   <meta name="description" content="">
   <meta name="description" content="">
   <meta name="author" content="">
   <meta name="author" content="">
 
 
@@ -52,7 +52,7 @@
 {% block layout_head_nav %}
 {% block layout_head_nav %}
 <nav class="crowi-header navbar navbar-default" role="navigation">
 <nav class="crowi-header navbar navbar-default" role="navigation">
   <div class="navbar-header">
   <div class="navbar-header">
-    <a class="navbar-brand" href="/">{% block title %}{{ config.app.title }}{% endblock %}</a>
+    <a class="navbar-brand" href="/">{% block title %}{{ config.crowi['app.title']|default('Crowi') }}{% endblock %}</a>
   </div>
   </div>
 
 
   <div class="collapse navbar-collapse">
   <div class="collapse navbar-collapse">

+ 11 - 4
views/login.html

@@ -4,7 +4,14 @@
 
 
 {% block content_main %}
 {% block content_main %}
 
 
-<h1 class="login-page" href="/">{% block title %}{{ config.app.title }}{% endblock %}</h1>
+<h1 class="login-page">
+  {% if config.crowi['app:title'] == 'Crowi' %}
+    <img src="/logo/135x32.png" alt="Crowi">
+  {% else %}
+    {{ config.crowi['app:title'] }}<br>
+    <img src="/logo/100x11_w.png" alt="powered by Crowi">
+  {% endif %}
+</h1>
 
 
 <div class="login-dialog-container flip-container col-md-5">
 <div class="login-dialog-container flip-container col-md-5">
 
 
@@ -72,7 +79,7 @@
 
 
     <h2>新規登録</h2>
     <h2>新規登録</h2>
 
 
-    {% if config.security.registrationMode != 'Closed' %}
+    {% if config.security.registrationMode == 'Restricted' %}
     <p class="alert alert-warning">
     <p class="alert alert-warning">
     この Wiki への新規登録は制限されています。<br>
     この Wiki への新規登録は制限されています。<br>
     利用を開始するには、新規登録後、管理者による承認が必要です。
     利用を開始するには、新規登録後、管理者による承認が必要です。
@@ -132,12 +139,12 @@
         <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
         <span class="input-group-addon"><i class="fa fa-envelope"></i></span>
         <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
         <input type="email" class="form-control" placeholder="E-mail" name="registerForm[email]" value="{{ googleEmail|default(req.body.registerForm.email) }}" required>
       </div>
       </div>
-      {% if config.security && config.security.registrationWhiteList.length %}
+      {% if config.crowi['security:registrationWhiteList'] && config.crowi['security:registrationWhiteList'].length %}
       <p class="help-block">
       <p class="help-block">
       この Wiki では以下のメールアドレスのみ登録可能です。
       この Wiki では以下のメールアドレスのみ登録可能です。
       </p>
       </p>
       <ul>
       <ul>
-        {% for em in config.security.registrationWhiteList %}
+        {% for em in config.crowi['security:registrationWhiteList'] %}
         <li><code>{{ em }}</code></li>
         <li><code>{{ em }}</code></li>
         {% endfor %}
         {% endfor %}
       </ul>
       </ul>

+ 4 - 34
views/me/index.html

@@ -68,11 +68,11 @@
           </p>
           </p>
           {% endif %}
           {% endif %}
 
 
-          {% if config.security && config.security.registrationWhiteList.length %}
+          {% if config.crowi['security.registrationWhiteList'] && config.crowi['security.registrationWhiteList'].length %}
           <p class="help-block">
           <p class="help-block">
           この Wiki では以下のメールアドレスのみ登録可能です。
           この Wiki では以下のメールアドレスのみ登録可能です。
           <ul>
           <ul>
-            {% for em in config.security.registrationWhiteList %}
+            {% for em in config.crowi['security.registrationWhiteList'] %}
             <li><code>{{ em }}</code></li>
             <li><code>{{ em }}</code></li>
             {% endfor %}
             {% endfor %}
           </ul>
           </ul>
@@ -271,12 +271,12 @@
               <p class="help-block">
               <p class="help-block">
               Googleコネクトをすると、Googleアカウントでログイン可能になります。<br>
               Googleコネクトをすると、Googleアカウントでログイン可能になります。<br>
               </p>
               </p>
-              {% if config.security && config.security.registrationWhiteList.length %}
+              {% if config.crowi['security.registrationWhiteList'] && config.crowi['security.registrationWhiteList'].length %}
               <p class="help-block">
               <p class="help-block">
               この Wiki では、登録可能なメールアドレスが限定されているため、コネクト可能なGoogleアカウントは、以下のメールアドレスの発行できるGoogle Appsアカウントに限られます。
               この Wiki では、登録可能なメールアドレスが限定されているため、コネクト可能なGoogleアカウントは、以下のメールアドレスの発行できるGoogle Appsアカウントに限られます。
               </p>
               </p>
               <ul>
               <ul>
-                {% for em in config.security.registrationWhiteList %}
+                {% for em in config.crowi['security.registrationWhiteList'] %}
                 <li><code>{{ em }}</code></li>
                 <li><code>{{ em }}</code></li>
                 {% endfor %}
                 {% endfor %}
               </ul>
               </ul>
@@ -290,36 +290,6 @@
     </div> {# /Google Connect #}
     </div> {# /Google Connect #}
   </div>
   </div>
 
 
-  {#
-  <div class="form-box">
-    <form action="/me/username" method="post" class="form-horizontal" role="form">
-      <fieldset>
-        <legend>プロフィール画像の設定</legend>
-      <div class="form-group">
-        <label for="" class="col-sm-2 control-label">
-          画像の設定
-        </label>
-        <div class="col-sm-6">
-          <p>
-            <img src="//graph.facebook.com/{{ user.userId }}/picture?size=square" width="32"><br>
-          </p>
-          <p>
-            <button class="btn btn-danger">画像を削除</button>
-          </p>
-        </div>
-      </div>
-      <div class="form-group">
-        <div class="col-sm-offset-2 col-sm-10">
-          <input name="" type="file">
-          <button type="submit" class="btn btn-primary">新しい画像をアップロード</button>
-        </div>
-      </div>
-      </div>
-    </fieldset>
-    </form>
-  </div>
-  #}
-
   </div> {# end of .tab-contents #}
   </div> {# end of .tab-contents #}
 
 
   {#
   {#

+ 4 - 1
views/page.html

@@ -149,6 +149,8 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block side_header %}
 {% block side_header %}
+
+{% if page %} {# {{{ if page #}
 <div class="page-meta">
 <div class="page-meta">
   <div class="row">
   <div class="row">
     <div class="col-md-3 creator-picture">
     <div class="col-md-3 creator-picture">
@@ -240,6 +242,7 @@ $(function() {
 });
 });
 </script>
 </script>
 </div>
 </div>
+{% endif %} {# if page }}} #}
 {% endblock %} {# side_header #}
 {% endblock %} {# side_header #}
 
 
 {% block side_content %}
 {% block side_content %}
@@ -248,7 +251,7 @@ $(function() {
   <ul class="fitted-list">
   <ul class="fitted-list">
     <li data-toggle="tooltip" data-placement="bottom" title="共有用リンク" class="input-group">
     <li data-toggle="tooltip" data-placement="bottom" title="共有用リンク" class="input-group">
       <span class="input-group-addon">共有用</span>
       <span class="input-group-addon">共有用</span>
-      <input class="copy-link form-control" type="text" value="{{ config.app.title }} {{ path }}  {{ baseUrl }}/_r/{{ page._id.toString() }}">
+      <input class="copy-link form-control" type="text" value="{{ config.crowi['app:title'] }} {{ path }}  {{ baseUrl }}/_r/{{ page._id.toString() }}">
     </li>
     </li>
     <li data-toggle="tooltip" data-placement="bottom" title="Wiki記法" class="input-group">
     <li data-toggle="tooltip" data-placement="bottom" title="Wiki記法" class="input-group">
       <span class="input-group-addon">Wikiタグ</span>
       <span class="input-group-addon">Wikiタグ</span>

+ 102 - 0
views/widget/searcher.html

@@ -0,0 +1,102 @@
+{% if config.crowi['searcher.url'] %}
+
+<form id="headerSearch" class="navbar-form navbar-left form-inline" role="search">
+  <div class="form-group">
+    <input id="searchQuery" name="q" type="text" class="form-control" placeholder="検索文字...">
+    <button type="submit" class="btn btn-default">検索</button>
+    <p>
+    </p>
+    <script>
+      function Searcher () {
+      };
+      Searcher.prototype = {
+        baseUrl: "{{ config.crowi['searcher.url'] }}",
+        currentQuery: "",
+        searchData: [],
+        setData: function (data) {
+          this.searchData = data;
+        },
+        search: function (query) {
+          var self = this;
+          self.currentQuery = query;
+          if (query == "") {
+            return false;
+          }
+          $("#searchQuery").addClass('searching');
+          $.ajax({
+            url: self.baseUrl + '/search?type=json&callback=?',
+            data: {q: query},
+            cache: false,
+            dataType: 'jsonp'
+          }).done(function (data) {
+            self.setData(data);
+            self.showSearchWidget();
+            $("#searchQuery").removeClass('searching');
+          });
+        },
+        showSearchWidget: function () {
+          if (this.searchData.length > 0) {
+            $('#headerSearch').popover('show');
+          }
+          $("#searchQuery").removeClass('searching');
+        },
+        createSearchContent: function () {
+          var self = this;
+          var contentHtml = $('<ul class=\"search-list\"></ul>');
+          $.each(self.searchData.slice(0, 6), function (i, d) {
+            var $li = $("<li class=\"list-link\"></li>");
+            var $a = $("<a></a>");
+            $a.attr('href', d.path).html(d.path + "<br>");
+            var $span = $("<span class=\"search-description\"></span>");
+            $span.text(d.body.substr(0, 50) + "...");
+            $a.append($span);
+            $li.append($a);
+            contentHtml.append($li);
+          });
+
+          var $li = $("<li class=\"divider\"></li>");
+          contentHtml.append($li);
+          $li = $("<li class=\"next-link\"></li>");
+          $li.html("<a href=\"" + self.baseUrl + "/search?q=" + encodeURIComponent(self.currentQuery) + "\">もっと見る</a>");
+          contentHtml.append($li);
+          return contentHtml;
+        },
+        jump: function (query) {
+          self = this;
+          top.location.href = self.baseUrl + '/search?q=' + query;
+        }
+      };
+      var SearcherObject = new Searcher();
+
+      $('#headerSearch').popover({
+        placement: 'bottom',
+        trigger: 'manual',
+        html: 'true',
+        content: function () {
+          return SearcherObject.createSearchContent();
+        }
+      });
+      $('#searchQuery').on('focus', function(e) {
+        SearcherObject.showSearchWidget();
+      });
+      $('#searchQuery').on('blur', function(e) {
+        $('#headerSearch').popover('hide');
+      });
+      $('#headerSearch').on('submit', function(e) {
+        SearcherObject.jump($("#searchQuery").val());
+        return false;
+      });
+
+      var previousText = "";
+      setInterval(function (e) {
+        var text = $("#searchQuery").val();
+        if (text != previousText) {
+          SearcherObject.search(text);
+        }
+        previousText = text;
+      }, 1000);
+    </script>
+  </div>
+</form>
+
+{% endif %}