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

Merge commit 'c0d7307db64507c30ca43bb3e1909b8f796591ec' into feat/limit-amount-of-gridfs-use

yusueketk 7 лет назад
Родитель
Сommit
cfbac01176

+ 7 - 1
CHANGES.md

@@ -1,7 +1,13 @@
 CHANGES
 ========
 
-## 3.2.9-RC
+## 3.2.10-RC
+
+* Fix: Pages in trash are available to create
+* Fix: Couldn't create portal page under Crowi Classic Behavior
+
+
+## 3.2.9
 
 * Feature: Attachment Storing to MongoDB GridFS
 * Fix: row/col moving of Spreadsheet like GUI (Handsontable) doesn't work

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.2.9-RC",
+  "version": "3.2.10-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 6 - 6
src/client/js/app.js

@@ -68,7 +68,7 @@ let pageContent = '';
 let markdown = '';
 let slackChannels;
 if (mainContent !== null) {
-  pageId = mainContent.getAttribute('data-page-id');
+  pageId = mainContent.getAttribute('data-page-id') || null;
   pageRevisionId = mainContent.getAttribute('data-page-revision-id');
   pageRevisionCreatedAt = +mainContent.getAttribute('data-page-revision-created');
   pageRevisionIdHackmdSynced = mainContent.getAttribute('data-page-revision-id-hackmd-synced') || null;
@@ -228,11 +228,7 @@ const saveWithSubmitButton = function() {
   options.socketClientId = socketClientId;
 
   let promise = undefined;
-  if (editorMode === 'builtin') {
-    // get markdown
-    promise = Promise.resolve(componentInstances.pageEditor.getMarkdown());
-  }
-  else {
+  if (editorMode === 'hackmd') {
     // get markdown
     promise = componentInstances.pageEditorByHackmd.getMarkdown();
     // use revisionId of PageEditorByHackmd
@@ -240,6 +236,10 @@ const saveWithSubmitButton = function() {
     // set option to sync
     options.isSyncRevisionToHackmd = true;
   }
+  else {
+    // get markdown
+    promise = Promise.resolve(componentInstances.pageEditor.getMarkdown());
+  }
   // create or update
   if (pageId == null) {
     promise = promise.then(markdown => {

+ 4 - 1
src/client/js/legacy/crowi.js

@@ -481,6 +481,7 @@ $(function() {
 
   $('#create-portal-button').on('click', function(e) {
     $('body').addClass('on-edit');
+    $('body').addClass('builtin-editor');
 
     const path = $('.content-main').data('path');
     if (path != '/' && $('.content-main').data('page-id') == '') {
@@ -493,8 +494,10 @@ $(function() {
     }
   });
   $('#portal-form-close').on('click', function(e) {
+    $('#edit').removeClass('active');
     $('body').removeClass('on-edit');
-    return false;
+    $('body').removeClass('builtin-editor');
+    location.hash = '#';
   });
 
   /*

+ 9 - 0
src/server/crowi/index.js

@@ -33,6 +33,7 @@ function Crowi(rootdir) {
   this.cacheDir    = path.join(this.tmpDir, 'cache');
 
   this.config = {};
+  this.configManager = null;
   this.searcher = null;
   this.mailer = {};
   this.passportService = null;
@@ -78,6 +79,8 @@ Crowi.prototype.init = function() {
       return self.setupSessionConfig();
     }).then(function() {
       return self.setupAppConfig();
+    }).then(function() {
+      return self.setupConfigManager();
     }).then(function() {
       return self.scanRuntimeVersions();
     }).then(function() {
@@ -205,6 +208,12 @@ Crowi.prototype.setupAppConfig = function() {
   });
 };
 
+Crowi.prototype.setupConfigManager = async function() {
+  const ConfigManager = require('../service/config-manager');
+  this.configManager = new ConfigManager(this.model('Config'));
+  return await this.configManager.loadConfigs();
+};
+
 Crowi.prototype.setupModels = function() {
   var self = this
     ;

+ 14 - 0
src/server/models/config.js

@@ -149,6 +149,20 @@ module.exports = function(crowi) {
     return config.markdown[key];
   }
 
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultCrowiConfigsObject = function() {
+    return getDefaultCrowiConfigs();
+  };
+
+  /**
+   * It is deprecated to use this for anything other than ConfigLoader#load.
+   */
+  configSchema.statics.getDefaultMarkdownConfigsObject = function() {
+    return getDefaultMarkdownConfigs();
+  };
+
   configSchema.statics.getRestrictGuestModeLabels = function() {
     var labels = {};
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'security_setting.guest_mode.deny';

+ 200 - 0
src/server/service/config-loader.js

@@ -0,0 +1,200 @@
+const debug = require('debug')('growi:service:ConfigLoader');
+
+const TYPES = {
+  NUMBER:  { parse: (v) => parseInt(v) },
+  STRING:  { parse: (v) => v },
+  BOOLEAN: { parse: (v) => /^(true|1)$/i.test(v) }
+};
+
+/**
+ * The following env vars are excluded because these are currently used before the configuration setup.
+ * - MONGO_URI
+ * - NODE_ENV
+ * - PORT
+ * - REDIS_URI
+ * - SESSION_NAME
+ * - PASSWORD_SEED
+ * - SECRET_TOKEN
+ *
+ *  The commented out item has not yet entered the migration work.
+ *  So, parameters of these are under consideration.
+ */
+const ENV_VAR_NAME_TO_CONFIG_INFO = {
+  // ELASTICSEARCH_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // FILE_UPLOAD: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // HACKMD_URI_FOR_SERVER: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // PLANTUML_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // BLOCKDIAG_URI: {
+  //   ns:      ,
+  //   key:     ,
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GOOGLE_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-google:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_ID: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientId',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CLIENT_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:clientSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_GITHUB_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-github:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_KEY: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerKey',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CONSUMER_SECRET: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:consumerSecret',
+  //   type:    ,
+  //   default:
+  // },
+  // OAUTH_TWITTER_CALLBACK_URI: {
+  //   ns:      'crowi',
+  //   key:     'security:passport-twitter:callbackUrl',
+  //   type:    ,
+  //   default:
+  // },
+  SAML_ENTRY_POINT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:entryPoint',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CALLBACK_URI: {
+    ns:      'crowi',
+    key:     'security:passport-saml:callbackUrl',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_ISSUER: {
+    ns:      'crowi',
+    key:     'security:passport-saml:issuer',
+    type:    TYPES.STRING,
+    default: null
+  },
+  SAML_CERT: {
+    ns:      'crowi',
+    key:     'security:passport-saml:cert',
+    type:    TYPES.STRING,
+    default: null
+  }
+};
+
+class ConfigLoader {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+  }
+
+  /**
+   * return a config object
+   */
+  async load() {
+    const configFromDB = await this.loadFromDB();
+    const configFromEnvVars = this.loadFromEnvVars();
+
+    // merge defaults
+    let mergedConfigFromDB = Object.assign({'crowi': this.configModel.getDefaultCrowiConfigsObject()}, configFromDB);
+    mergedConfigFromDB = Object.assign({'markdown': this.configModel.getDefaultMarkdownConfigsObject()}, mergedConfigFromDB);
+
+    return {
+      fromDB: mergedConfigFromDB,
+      fromEnvVars: configFromEnvVars
+    };
+  }
+
+  async loadFromDB() {
+    const config = {};
+    const docs = await this.configModel.find().exec();
+
+    for (const doc of docs) {
+      if (!config[doc.ns]) {
+        config[doc.ns] = {};
+      }
+      config[doc.ns][doc.key] = JSON.parse(doc.value);
+    }
+
+    debug('ConfigLoader#loadFromDB', config);
+
+    return config;
+  }
+
+  loadFromEnvVars() {
+    const config = {};
+    for (const ENV_VAR_NAME of Object.keys(ENV_VAR_NAME_TO_CONFIG_INFO)) {
+      const configInfo = ENV_VAR_NAME_TO_CONFIG_INFO[ENV_VAR_NAME];
+      if (config[configInfo.ns] === undefined) {
+        config[configInfo.ns] = {};
+      }
+
+      if (process.env[ENV_VAR_NAME] === undefined) {
+        config[configInfo.ns][configInfo.key] = configInfo.default;
+      }
+      else {
+        config[configInfo.ns][configInfo.key] = configInfo.type.parse(process.env[ENV_VAR_NAME]);
+      }
+    }
+
+    debug('ConfigLoader#loadFromEnvVars', config);
+
+    return config;
+  }
+}
+
+module.exports = ConfigLoader;

+ 125 - 0
src/server/service/config-manager.js

@@ -0,0 +1,125 @@
+const ConfigLoader = require('../service/config-loader')
+  , debug = require('debug')('growi:service:ConfigManager');
+
+class ConfigManager {
+
+  constructor(configModel) {
+    this.configModel = configModel;
+    this.configLoader = new ConfigLoader(this.configModel);
+    this.configObject = null;
+  }
+
+  /**
+   * load configs from the database and the environment variables
+   */
+  async loadConfigs() {
+    this.configObject = await this.configLoader.load();
+
+    debug('ConfigManager#loadConfigs', this.configObject);
+  }
+
+  /**
+   * get a config specified by namespace & key
+   *
+   * Basically, search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * In some case, this search method changes.(not yet implemented)
+   */
+  getConfig(namespace, key) {
+    return this.defaultSearch(namespace, key);
+  }
+
+  /**
+   * private api
+   *
+   * Search a specified config from configs loaded from database at first
+   * and then from configs loaded from env vars.
+   *
+   * the followings are the meanings of each special return value.
+   * - null:      a specified config is not set.
+   * - undefined: a specified config does not exist.
+   */
+  defaultSearch(namespace, key) {
+    if (!this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key)) {
+      return undefined;
+    }
+
+    if (this.configExistsInDB(namespace, key) && !this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromDB[namespace][key];
+    }
+
+    if (!this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      return this.configObject.fromEnvVars[namespace][key];
+    }
+
+    if (this.configExistsInDB(namespace, key) && this.configExistsInEnvVars(namespace, key) ) {
+      if (this.configObject.fromDB[namespace][key] !== null) {
+        return this.configObject.fromDB[namespace][key];
+      }
+      else {
+        return this.configObject.fromEnvVars[namespace][key];
+      }
+    }
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the database
+   * @returns {boolean}
+   */
+  configExistsInDB(namespace, key) {
+    if (this.configObject.fromDB[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromDB[namespace][key] !== undefined;
+  }
+
+  /**
+   * check whether a specified config exists in configs loaded from the environment variables
+   * @returns {boolean}
+   */
+  configExistsInEnvVars(namespace, key) {
+    if (this.configObject.fromEnvVars[namespace] === undefined) {
+      return false;
+    }
+
+    return this.configObject.fromEnvVars[namespace][key] !== undefined;
+  }
+
+  /**
+   * update configs by a iterable object consisting of several objects with ns, key, value fields
+   *
+   * For example:
+   * ```
+   *  updateConfigs(
+   *   [{
+   *     ns:    'some namespace 1',
+   *     key:   'some key 1',
+   *     value: 'some value 1'
+   *   }, {
+   *     ns:    'some namespace 2',
+   *     key:   'some key 2',
+   *     value: 'some value 2'
+   *   }]
+   *  );
+   * ```
+   */
+  async updateConfigs(configs) {
+    const results = [];
+    for (const config of configs) {
+      results.push(
+        this.configModel.findOneAndUpdate(
+          { ns: config.ns, key: config.key },
+          { ns: config.ns, key: config.key, value: JSON.stringify(config.value) },
+          { upsert: true, }
+        ).exec()
+      );
+    }
+    await Promise.all(results);
+
+    await this.loadConfigs();
+  }
+}
+
+module.exports = ConfigManager;

+ 1 - 1
src/server/views/widget/create_portal.html

@@ -1,4 +1,4 @@
 <div class="portal-form-button">
-  <button class="btn btn-primary" id="create-portal-button" {% if not user %}disabled{% endif %}>Create Portal</button>
+  <a class="btn btn-primary" id="create-portal-button" href="#edit" data-toggle="tab" {% if not user %}disabled{% endif %}>Create Portal</a>
   <p class="help-block"><a href="#" data-target="#help-portal" data-toggle="modal"><i class="icon-question"></i> What is Portal?</a></p>
 </div>

+ 2 - 1
src/server/views/widget/not_found_tabs.html

@@ -5,10 +5,11 @@
     </a>
   </li>
 
+  {% if !isTrashPage() and !page.isDeleted() %}
   <li class="nav-main-left-tab">
     <a {% if user %}href="#edit" data-toggle="tab"{% endif %} class="edit-button {% if not user %}edit-button-disabled{% endif %}">
       <i class="icon-note"></i> {{ t('Create') }}
     </a>
   </li>
-
+  {% endif %}
 </ul>

+ 6 - 4
src/server/views/widget/page_alerts.html

@@ -12,14 +12,16 @@
       </p>
     {% endif %}
 
-    {% if page.isDeleted() %}
+    {% if isTrashPage() %}
     <div class="alert alert-warning alert-trash d-flex align-items-center justify-content-between">
       <div>
         <i class="icon-trash" aria-hidden="true"></i>
-        This page is in the trash.<br>
-        Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm img-circle"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
+        This page is in the trash.
+        {% if page.isDeleted() %}
+        <br>Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm img-circle"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
+        {% endif %}
       </div>
-      {% if user %}
+      {% if page.isDeleted() and user %}
       <ul class="list-inline">
         <li>
           <a href="#" class="btn btn-default btn-rounded btn-sm" data-target="#putBackPage" data-toggle="modal"><i class="icon-action-undo" aria-hidden="true"></i> {{ t('Put Back') }}</a>

+ 1 - 1
src/server/views/widget/page_tabs.html

@@ -81,7 +81,7 @@
 <ul class="nav nav-tabs nav-tabs-create-portal hidden-print">
 
   <li class="nav-main-left-tab">
-    <a id="portal-form-close" href="#">
+    <a id="portal-form-close" href="#" data-toggle="tab">
       <i class="icon-action-undo"></i> {{ t('Cancel') }}
     </a>
   </li>

+ 1 - 1
src/server/views/widget/page_tabs_kibela.html

@@ -81,7 +81,7 @@
 <ul class="nav nav-tabs customtab nav-tabs-create-portal hidden-print">
 
   <li class="nav-main-left-tab">
-    <a id="portal-form-close" href="#">
+    <a id="portal-form-close" href="#" data-toggle="tab">
       <i class="icon-action-undo"></i> {{ t('Cancel') }}
     </a>
   </li>