index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. const debug = require('debug')('growi:crowi');
  2. const logger = require('@alias/logger')('growi:crowi');
  3. const pkg = require('@root/package.json');
  4. const InterceptorManager = require('@commons/service/interceptor-manager');
  5. const CdnResourcesService = require('@commons/service/cdn-resources-service');
  6. const Xss = require('@commons/service/xss');
  7. const path = require('path');
  8. const sep = path.sep;
  9. const mongoose = require('mongoose');
  10. const models = require('../models');
  11. const PluginService = require('../plugins/plugin.service');
  12. function Crowi(rootdir) {
  13. const self = this;
  14. this.version = pkg.version;
  15. this.runtimeVersions = undefined; // initialized by scanRuntimeVersions()
  16. this.rootDir = rootdir;
  17. this.pluginDir = path.join(this.rootDir, 'node_modules') + sep;
  18. this.publicDir = path.join(this.rootDir, 'public') + sep;
  19. this.libDir = path.join(this.rootDir, 'src/server') + sep;
  20. this.eventsDir = path.join(this.libDir, 'events') + sep;
  21. this.viewsDir = path.join(this.libDir, 'views') + sep;
  22. this.resourceDir = path.join(this.rootDir, 'resource') + sep;
  23. this.localeDir = path.join(this.resourceDir, 'locales') + sep;
  24. this.tmpDir = path.join(this.rootDir, 'tmp') + sep;
  25. this.cacheDir = path.join(this.tmpDir, 'cache');
  26. this.config = {};
  27. this.configManager = null;
  28. this.searcher = null;
  29. this.mailer = {};
  30. this.passportService = null;
  31. this.globalNotificationService = null;
  32. this.slackNotificationService = null;
  33. this.xssService = null;
  34. this.aclService = null;
  35. this.appService = null;
  36. this.fileUploadService = null;
  37. this.restQiitaAPIService = null;
  38. this.exportService = null;
  39. this.cdnResourcesService = new CdnResourcesService();
  40. this.interceptorManager = new InterceptorManager();
  41. this.xss = new Xss();
  42. this.tokens = null;
  43. this.models = {};
  44. this.env = process.env;
  45. this.node_env = this.env.NODE_ENV || 'development';
  46. this.port = this.env.PORT || 3000;
  47. this.events = {
  48. user: new (require(`${self.eventsDir}user`))(this),
  49. page: new (require(`${self.eventsDir}page`))(this),
  50. search: new (require(`${self.eventsDir}search`))(this),
  51. bookmark: new (require(`${self.eventsDir}bookmark`))(this),
  52. tag: new (require(`${self.eventsDir}tag`))(this),
  53. };
  54. }
  55. function getMongoUrl(env) {
  56. return env.MONGOLAB_URI // for B.C.
  57. || env.MONGODB_URI // MONGOLAB changes their env name
  58. || env.MONGOHQ_URL
  59. || env.MONGO_URI
  60. || ((process.env.NODE_ENV === 'test') ? 'mongodb://localhost/growi_test' : 'mongodb://localhost/growi');
  61. }
  62. Crowi.prototype.init = async function() {
  63. await this.setupDatabase();
  64. await this.setupModels();
  65. await this.setupSessionConfig();
  66. await this.setupConfigManager();
  67. // customizeService depends on AppService and XssService
  68. // passportService depends on appService
  69. // slack depends on setUpSlacklNotification
  70. await Promise.all([
  71. this.setUpApp(),
  72. this.setUpXss(),
  73. this.setUpSlacklNotification(),
  74. ]);
  75. await Promise.all([
  76. this.scanRuntimeVersions(),
  77. this.setupPassport(),
  78. this.setupSearcher(),
  79. this.setupMailer(),
  80. this.setupSlack(),
  81. this.setupCsrf(),
  82. this.setUpFileUpload(),
  83. this.setUpAcl(),
  84. this.setUpCustomize(),
  85. this.setUpRestQiitaAPI(),
  86. this.setupUserGroup(),
  87. this.setupExport(),
  88. ]);
  89. // globalNotification depends on slack and mailer
  90. await Promise.all([
  91. this.setUpGlobalNotification(),
  92. ]);
  93. };
  94. Crowi.prototype.initForTest = async function() {
  95. await this.setupModels();
  96. await this.setupConfigManager();
  97. // // customizeService depends on AppService and XssService
  98. // // passportService depends on appService
  99. // // slack depends on setUpSlacklNotification
  100. await Promise.all([
  101. this.setUpApp(),
  102. // this.setUpXss(),
  103. // this.setUpSlacklNotification(),
  104. ]);
  105. await Promise.all([
  106. // this.scanRuntimeVersions(),
  107. // this.setupPassport(),
  108. // this.setupSearcher(),
  109. // this.setupMailer(),
  110. // this.setupSlack(),
  111. // this.setupCsrf(),
  112. // this.setUpFileUpload(),
  113. this.setUpAcl(),
  114. // this.setUpCustomize(),
  115. // this.setUpRestQiitaAPI(),
  116. ]);
  117. // globalNotification depends on slack and mailer
  118. // await Promise.all([
  119. // this.setUpGlobalNotification(),
  120. // ]);
  121. };
  122. Crowi.prototype.isPageId = function(pageId) {
  123. if (!pageId) {
  124. return false;
  125. }
  126. if (typeof pageId === 'string' && pageId.match(/^[\da-f]{24}$/)) {
  127. return true;
  128. }
  129. return false;
  130. };
  131. Crowi.prototype.setConfig = function(config) {
  132. this.config = config;
  133. };
  134. Crowi.prototype.getConfig = function() {
  135. return this.config;
  136. };
  137. Crowi.prototype.getEnv = function() {
  138. return this.env;
  139. };
  140. // getter/setter of model instance
  141. //
  142. Crowi.prototype.model = function(name, model) {
  143. if (model != null) {
  144. this.models[name] = model;
  145. }
  146. return this.models[name];
  147. };
  148. // getter/setter of event instance
  149. Crowi.prototype.event = function(name, event) {
  150. if (event) {
  151. this.events[name] = event;
  152. }
  153. return this.events[name];
  154. };
  155. Crowi.prototype.setupDatabase = function() {
  156. // mongoUri = mongodb://user:password@host/dbname
  157. mongoose.Promise = global.Promise;
  158. const mongoUri = getMongoUrl(this.env);
  159. return mongoose.connect(mongoUri, { useNewUrlParser: true });
  160. };
  161. Crowi.prototype.setupSessionConfig = function() {
  162. const self = this;
  163. const session = require('express-session');
  164. const sessionAge = (1000 * 3600 * 24 * 30);
  165. const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
  166. const mongoUrl = getMongoUrl(this.env);
  167. let sessionConfig;
  168. return new Promise(((resolve, reject) => {
  169. sessionConfig = {
  170. rolling: true,
  171. secret: self.env.SECRET_TOKEN || 'this is default session secret',
  172. resave: false,
  173. saveUninitialized: true,
  174. cookie: {
  175. maxAge: sessionAge,
  176. },
  177. };
  178. if (self.env.SESSION_NAME) {
  179. sessionConfig.name = self.env.SESSION_NAME;
  180. }
  181. // use Redis for session store
  182. if (redisUrl) {
  183. const RedisStore = require('connect-redis')(session);
  184. sessionConfig.store = new RedisStore({ url: redisUrl });
  185. }
  186. // use MongoDB for session store
  187. else {
  188. const MongoStore = require('connect-mongo')(session);
  189. sessionConfig.store = new MongoStore({ url: mongoUrl });
  190. }
  191. self.sessionConfig = sessionConfig;
  192. resolve();
  193. }));
  194. };
  195. Crowi.prototype.setupConfigManager = async function() {
  196. const ConfigManager = require('../service/config-manager');
  197. this.configManager = new ConfigManager(this.model('Config'));
  198. return this.configManager.loadConfigs();
  199. };
  200. Crowi.prototype.setupModels = async function() {
  201. Object.keys(models).forEach((key) => {
  202. return this.model(key, models[key](this));
  203. });
  204. };
  205. Crowi.prototype.getIo = function() {
  206. return this.io;
  207. };
  208. Crowi.prototype.scanRuntimeVersions = async function() {
  209. const self = this;
  210. const check = require('check-node-version');
  211. return new Promise((resolve, reject) => {
  212. check((err, result) => {
  213. if (err) {
  214. reject(err);
  215. }
  216. self.runtimeVersions = result;
  217. resolve();
  218. });
  219. });
  220. };
  221. Crowi.prototype.getSearcher = function() {
  222. return this.searcher;
  223. };
  224. Crowi.prototype.getMailer = function() {
  225. return this.mailer;
  226. };
  227. Crowi.prototype.getSlack = function() {
  228. return this.slack;
  229. };
  230. Crowi.prototype.getInterceptorManager = function() {
  231. return this.interceptorManager;
  232. };
  233. Crowi.prototype.getGlobalNotificationService = function() {
  234. return this.globalNotificationService;
  235. };
  236. Crowi.prototype.getRestQiitaAPIService = function() {
  237. return this.restQiitaAPIService;
  238. };
  239. Crowi.prototype.setupPassport = async function() {
  240. debug('Passport is enabled');
  241. // initialize service
  242. const PassportService = require('../service/passport');
  243. if (this.passportService == null) {
  244. this.passportService = new PassportService(this);
  245. }
  246. this.passportService.setupSerializer();
  247. // setup strategies
  248. try {
  249. this.passportService.setupLocalStrategy();
  250. this.passportService.setupLdapStrategy();
  251. this.passportService.setupGoogleStrategy();
  252. this.passportService.setupGitHubStrategy();
  253. this.passportService.setupTwitterStrategy();
  254. this.passportService.setupOidcStrategy();
  255. this.passportService.setupSamlStrategy();
  256. this.passportService.setupBasicStrategy();
  257. }
  258. catch (err) {
  259. logger.error(err);
  260. }
  261. return Promise.resolve();
  262. };
  263. Crowi.prototype.setupSearcher = async function() {
  264. const self = this;
  265. const searcherUri = this.env.ELASTICSEARCH_URI
  266. || this.env.BONSAI_URL
  267. || null;
  268. if (searcherUri) {
  269. try {
  270. self.searcher = new (require(path.join(self.libDir, 'util', 'search')))(self, searcherUri);
  271. self.searcher.initIndices();
  272. }
  273. catch (e) {
  274. logger.error('Error on setup searcher', e);
  275. self.searcher = null;
  276. }
  277. }
  278. };
  279. Crowi.prototype.setupMailer = async function() {
  280. const self = this;
  281. return new Promise(((resolve, reject) => {
  282. self.mailer = require('../util/mailer')(self);
  283. resolve();
  284. }));
  285. };
  286. Crowi.prototype.setupSlack = async function() {
  287. const self = this;
  288. return new Promise(((resolve, reject) => {
  289. self.slack = require('../util/slack')(self);
  290. resolve();
  291. }));
  292. };
  293. Crowi.prototype.setupCsrf = async function() {
  294. const Tokens = require('csrf');
  295. this.tokens = new Tokens();
  296. return Promise.resolve();
  297. };
  298. Crowi.prototype.getTokens = function() {
  299. return this.tokens;
  300. };
  301. Crowi.prototype.start = async function() {
  302. // init CrowiDev
  303. if (this.node_env === 'development') {
  304. const CrowiDev = require('./dev');
  305. this.crowiDev = new CrowiDev(this);
  306. this.crowiDev.init();
  307. }
  308. await this.init();
  309. const express = await this.buildServer();
  310. // setup plugins
  311. this.pluginService = new PluginService(this, express);
  312. this.pluginService.autoDetectAndLoadPlugins();
  313. const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
  314. // listen
  315. const serverListening = server.listen(this.port, () => {
  316. logger.info(`[${this.node_env}] Express server is listening on port ${this.port}`);
  317. if (this.node_env === 'development') {
  318. this.crowiDev.setupExpressAfterListening(express);
  319. }
  320. });
  321. // setup WebSocket
  322. const io = require('socket.io')(serverListening);
  323. io.sockets.on('connection', (socket) => {
  324. });
  325. this.io = io;
  326. // setup Express Routes
  327. this.setupRoutesAtLast(express);
  328. return serverListening;
  329. };
  330. Crowi.prototype.buildServer = function() {
  331. const express = require('express')();
  332. const env = this.node_env;
  333. require('./express-init')(this, express);
  334. // use bunyan
  335. if (env === 'production') {
  336. const expressBunyanLogger = require('express-bunyan-logger');
  337. const logger = require('@alias/logger')('express');
  338. express.use(expressBunyanLogger({
  339. logger,
  340. excludes: ['*'],
  341. }));
  342. }
  343. // use morgan
  344. else {
  345. const morgan = require('morgan');
  346. express.use(morgan('dev'));
  347. }
  348. return Promise.resolve(express);
  349. };
  350. /**
  351. * setup Express Routes
  352. * !! this must be at last because it includes '/*' route !!
  353. */
  354. Crowi.prototype.setupRoutesAtLast = function(app) {
  355. require('../routes')(this, app);
  356. };
  357. /**
  358. * require API for plugins
  359. *
  360. * @param {string} modulePath relative path from /lib/crowi/index.js
  361. * @return {module}
  362. *
  363. * @memberof Crowi
  364. */
  365. Crowi.prototype.require = function(modulePath) {
  366. return require(modulePath);
  367. };
  368. /**
  369. * setup GlobalNotificationService
  370. */
  371. Crowi.prototype.setUpGlobalNotification = async function() {
  372. const GlobalNotificationService = require('../service/global-notification');
  373. if (this.globalNotificationService == null) {
  374. this.globalNotificationService = new GlobalNotificationService(this);
  375. }
  376. };
  377. /**
  378. * setup SlackNotificationService
  379. */
  380. Crowi.prototype.setUpSlacklNotification = async function() {
  381. const SlackNotificationService = require('../service/slack-notification');
  382. if (this.slackNotificationService == null) {
  383. this.slackNotificationService = new SlackNotificationService(this.configManager);
  384. }
  385. };
  386. /**
  387. * setup XssService
  388. */
  389. Crowi.prototype.setUpXss = async function() {
  390. const XssService = require('../service/xss');
  391. if (this.xssService == null) {
  392. this.xssService = new XssService(this.configManager);
  393. }
  394. };
  395. /**
  396. * setup AclService
  397. */
  398. Crowi.prototype.setUpAcl = async function() {
  399. const AclService = require('../service/acl');
  400. if (this.aclService == null) {
  401. this.aclService = new AclService(this.configManager);
  402. }
  403. };
  404. /**
  405. * setup CustomizeService
  406. */
  407. Crowi.prototype.setUpCustomize = async function() {
  408. const CustomizeService = require('../service/customize');
  409. if (this.customizeService == null) {
  410. this.customizeService = new CustomizeService(this.configManager, this.appService, this.xssService);
  411. this.customizeService.initCustomCss();
  412. this.customizeService.initCustomTitle();
  413. }
  414. };
  415. /**
  416. * setup AppService
  417. */
  418. Crowi.prototype.setUpApp = async function() {
  419. const AppService = require('../service/app');
  420. if (this.appService == null) {
  421. this.appService = new AppService(this.configManager);
  422. }
  423. };
  424. /**
  425. * setup FileUploadService
  426. */
  427. Crowi.prototype.setUpFileUpload = async function() {
  428. if (this.fileUploadService == null) {
  429. this.fileUploadService = require('../service/file-uploader')(this);
  430. }
  431. };
  432. /**
  433. * setup RestQiitaAPIService
  434. */
  435. Crowi.prototype.setUpRestQiitaAPI = async function() {
  436. const RestQiitaAPIService = require('../service/rest-qiita-API');
  437. if (this.restQiitaAPIService == null) {
  438. this.restQiitaAPIService = new RestQiitaAPIService(this);
  439. }
  440. };
  441. Crowi.prototype.setupUserGroup = async function() {
  442. const UserGroupService = require('../service/user-group');
  443. if (this.userGroupService == null) {
  444. this.userGroupService = new UserGroupService(this);
  445. return this.userGroupService.init();
  446. }
  447. };
  448. Crowi.prototype.setupExport = async function() {
  449. const ExportService = require('../service/export');
  450. if (this.exportService == null) {
  451. this.exportService = new ExportService(this);
  452. }
  453. };
  454. module.exports = Crowi;