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

Merge branch 'master' into imprv/saml-uses-only-env-vars-option

utsushiiro 7 лет назад
Родитель
Сommit
bc072e5a1d

+ 2 - 0
CHANGES.md

@@ -5,10 +5,12 @@ CHANGES
 
 * Feature: NO_CDN Mode
 * Feature: Add option to show/hide restricted pages in list
+* Feature: MongoDB GridFS quota
 * Improvement: Refactor Access Control
 * Improvement: Checkbox behavior of task list
 * Improvement: Fixed search input on search result page
 * Improvement: Add 'christmas' theme
+* Improvement: Select default language of new users
 * Fix: Hide restricted pages contents in timeline
 * Support: Upgrade libs
     * googleapis

+ 1 - 1
README.md

@@ -169,7 +169,7 @@ Environment Variables
       * `mongodb` : MongoDB GridFS (Setting-less)
       * `local` : Server's Local file system (Setting-less)
       * `none` : Disable file uploading
-    * MONGODB_GRIDFS_LIMIT: Limit amount of uploaded file with GridFS: `Infinity`
+    * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
 * **Option to integrate with external systems**
     * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
         * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/management-cookbook/integrate-with-hackmd).**

+ 6 - 4
bin/download-cdn-resources.js

@@ -5,7 +5,7 @@
  */
 require('module-alias/register');
 
-const logger = require('@alias/logger')('growi:bin:download-resources');
+const logger = require('@alias/logger')('growi:bin:download-cdn-resources');
 
 // check env var
 const noCdn = !!process.env.NO_CDN;
@@ -15,13 +15,15 @@ if (!noCdn) {
   process.exit(0);
 }
 
+logger.info('This is NO_CDN mode. Start to download resources.');
+
+const CdnResourcesDownloader = require('@commons/service/cdn-resources-downloader');
 const CdnResourcesService = require('@commons/service/cdn-resources-service');
 
+const downloader = new CdnResourcesDownloader();
 const service = new CdnResourcesService();
 
-logger.info('This is NO_CDN mode. Start to download resources.');
-
-service.downloadAndWriteAll()
+service.downloadAndWriteAll(downloader)
   .then(() => {
     logger.info('Download is terminated successfully');
   })

+ 0 - 22
bin/download-resources.js

@@ -1,22 +0,0 @@
-/**
- * the tool for download CDN resources and save as file
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-
-require('module-alias/register');
-
-const logger = require('@alias/logger')('growi:bin:download-resources');
-const CdnResourcesService = require('@commons/service/cdn-resources-service');
-
-const service = new CdnResourcesService();
-
-logger.info('Start to download.');
-
-service.downloadAndWriteAll()
-  .then(() => {
-    logger.info('Download is terminated successfully');
-  })
-  .catch(err => {
-    logger.error(err);
-  });

+ 1 - 1
config/env.dev.js

@@ -1,7 +1,7 @@
 module.exports = {
   NODE_ENV: 'development',
   FILE_UPLOAD: 'mongodb',
-  // MONGODB_GRIDFS_LIMIT: 10485760,   // 10MB
+  // MONGO_GRIDFS_TOTAL_LIMIT: 10485760,   // 10MB
   // MATHJAX: 1,
   // NO_CDN: true,
   ELASTICSEARCH_URI: 'http://localhost:9200/growi',

+ 2 - 4
package.json

@@ -39,11 +39,10 @@
     "migrate:status": "migrate-mongo status -f config/migrate.js",
     "migrate:up": "migrate-mongo up -f config/migrate.js",
     "migrate:down": "migrate-mongo down -f config/migrate.js",
-    "mkdirp": "mkdirp",
     "plugin:def": "node bin/generate-plugin-definitions-source.js",
     "prebuild:dev:watch": "npm run prebuild:dev",
     "prebuild:dev": "npm run clean:app && env-cmd config/env.dev.js npm run plugin:def && env-cmd config/env.dev.js npm run resource",
-    "prebuild:prod": "npm run clean && env-cmd config/env.prod.js npm run plugin:def && env-cmd config/env.dev.js npm run resource",
+    "prebuild:prod": "npm run clean && env-cmd config/env.prod.js npm run plugin:def && env-cmd config/env.prod.js npm run resource",
     "preserver:prod": "npm run migrate",
     "prestart": "npm run build:prod",
     "resource": "node bin/download-cdn-resources.js",
@@ -92,7 +91,6 @@
     "i18next-express-middleware": "^1.4.1",
     "i18next-node-fs-backend": "^2.1.0",
     "i18next-sprintf-postprocessor": "^0.2.2",
-    "markdown-it-blockdiag": "^1.0.2",
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^4.0.0",
@@ -161,7 +159,7 @@
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^8.4.0",
-    "markdown-it-blockdiag": "^1.0.0",
+    "markdown-it-blockdiag": "^1.0.2",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",

+ 17 - 4
resource/locales/en-US/translation.json

@@ -174,6 +174,12 @@
       }
   },
 
+  "page_api_error": {
+    "notfound_or_forbidden": "Original page is not found or forbidden.",
+    "already_exists": "New page is already exists.",
+    "outdated": "Page is updated someone and now outdated. "
+  },
+
   "modal_rename": {
     "label": {
       "Rename page": "Rename page",
@@ -194,8 +200,12 @@
   "modal_delete": {
     "label": {
       "Delete Page": "Delete Page",
-      "recursively": "Process recursively",
-      "completely": "Delete completely"
+      "Delete recursively": "Delete recursively",
+      "Delete completely": "Delete completely"
+    },
+    "help": {
+      "recursively": "Delete children of under <code>%s</code> recursively",
+      "completely": "Delete completely instead of putting it into trash"
     }
   },
 
@@ -207,10 +217,13 @@
     }
   },
 
-  "modal_putBack": {
+  "modal_putback": {
     "label": {
       "Put Back Page": "Put Back Page",
-      "recursively": "Process recursively"
+      "recursively": "Put Back recursively"
+    },
+    "help": {
+      "recursively": "Put Back children of under <code>%s</code> recursively"
     }
   },
 

+ 16 - 6
resource/locales/ja/translation.json

@@ -190,8 +190,11 @@
       }
   },
 
-
-
+  "page_api_error": {
+    "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
+    "already_exists": "新しいページが既に存在しています。",
+    "outdated": "ページが他のユーザーによって更新されました。"
+  },
 
   "modal_rename": {
     "label": {
@@ -213,8 +216,12 @@
   "modal_delete": {
     "label": {
       "Delete Page": "ページを削除する",
-      "recursively": "全ての子ページも処理",
-      "completely": "完全削除"
+      "Delete recursively": "全ての子ページも削除",
+      "Delete completely": "完全削除"
+    },
+    "help": {
+      "recursively": "<code>%s</code> 配下のページも削除します",
+      "completely": "ゴミ箱を経由せず、完全に削除します"
     }
   },
 
@@ -226,10 +233,13 @@
     }
   },
 
-  "modal_putBack": {
+  "modal_putback": {
     "label": {
       "Put Back Page": "ページを元に戻す",
-      "recursively": "全ての子ページも処理"
+      "recursively": "全ての子ページも元に戻す"
+    },
+    "help": {
+      "recursively": "<code>%s</code> 配下のページも元に戻します"
     }
   },
 

+ 17 - 13
src/client/js/components/InstallerForm.js → src/client/js/components/InstallerForm.jsx

@@ -13,6 +13,10 @@ class InstallerForm extends React.Component {
     this.checkUserName = this.checkUserName.bind(this);
   }
 
+  componentWillMount() {
+    this.changeLanguage('en-US');
+  }
+
   checkUserName(event) {
     const axios = require('axios').create({
       headers: {
@@ -40,6 +44,19 @@ class InstallerForm extends React.Component {
         </p>
 
         <form role="form" action="/installer/createAdmin" method="post" id="register-form">
+          <div className="input-group m-t-20 m-b-20 mx-auto">
+            <div className="radio radio-primary radio-inline">
+              <input type="radio" id="radioLangEn" name="registerForm[app:globalLang]" value="en-US"
+                     defaultChecked={ true } onClick={() => this.changeLanguage('en-US')} />
+              <label htmlFor="radioLangEn">English</label>
+            </div>
+            <div className="radio radio-primary radio-inline">
+              <input type="radio" id="radioLangJa" name="registerForm[app:globalLang]" value="ja"
+                     defaultChecked={ false } onClick={() => this.changeLanguage('ja')} />
+              <label htmlFor="radioLangJa">日本語</label>
+            </div>
+          </div>
+
           <div className={'input-group' + hasErrorClass}>
             <span className="input-group-addon"><i className="icon-user" /></span>
             <input type="text" className="form-control" placeholder={ this.props.t('User ID') }
@@ -67,19 +84,6 @@ class InstallerForm extends React.Component {
 
           <input type="hidden" name="_csrf" value={ this.props.csrf } />
 
-          <div className="input-group m-t-20 m-b-20 mx-auto">
-            <div className="radio radio-primary radio-inline">
-              <input type="radio" id="radioLangEn" name="registerForm[app:globalLang]" value="en-US"
-                     defaultChecked={ true } onClick={() => this.changeLanguage('en-US')} />
-              <label htmlFor="radioLangEn">{ this.props.t('English') }</label>
-            </div>
-            <div className="radio radio-primary radio-inline">
-              <input type="radio" id="radioLangJa" name="registerForm[app:globalLang]" value="ja"
-                     defaultChecked={ false } onClick={() => this.changeLanguage('ja')} />
-              <label htmlFor="radioLangJa">{ this.props.t('Japanese') }</label>
-            </div>
-          </div>
-
           <div className="input-group m-t-30 m-b-20 d-flex justify-content-center">
             <button type="submit" className="fcbtn btn btn-success btn-1b btn-register">
               <span className="btn-label"><i className="icon-user-follow" /></span>

+ 5 - 3
src/client/js/components/PageHistory.js

@@ -97,7 +97,9 @@ class PageHistory extends React.Component {
       return ;
     }
 
-    this.props.crowi.apiGet('/revisions.get', {revision_id: revision._id})
+    this.props.crowi.apiGet('/revisions.get',
+      { page_id: this.props.pageId, revision_id: revision._id}
+    )
     .then(res => {
       if (res.ok) {
         this.setState({
@@ -110,10 +112,10 @@ class PageHistory extends React.Component {
           })
         });
       }
-    }).catch(err => {
+    })
+    .catch(err => {
 
     });
-
   }
 
   render() {

+ 2 - 3
src/client/js/installer.js

@@ -4,10 +4,9 @@ import { I18nextProvider } from 'react-i18next';
 
 import i18nFactory from './i18n';
 
-import InstallerForm    from './components/InstallerForm';
+import InstallerForm from './components/InstallerForm';
 
-const userlang = $('body').data('userlang');
-const i18n = i18nFactory(userlang);
+const i18n = i18nFactory();
 
 // render InstallerForm
 const installerFormElem = document.getElementById('installer-form');

+ 22 - 10
src/client/js/legacy/crowi.js

@@ -351,7 +351,7 @@ $(function() {
   // rename/unportalize
   $('#renamePage, #unportalize').on('shown.bs.modal', function(e) {
     $('#renamePage #newPageName').focus();
-    $('#renamePage .msg-already-exists, #unportalize .msg-already-exists').hide();
+    $('#renamePage .msg, #unportalize .msg').hide();
   });
   $('#renamePageForm, #unportalize-form').submit(function(e) {
     // create name-value map
@@ -369,9 +369,10 @@ $(function() {
       dataType: 'json'
     })
     .done(function(res) {
+      // error
       if (!res.ok) {
-        // if already exists
-        $('#renamePage .msg-already-exists, #unportalize .msg-already-exists').show();
+        $('#renamePage .msg, #unportalize .msg').hide();
+        $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
         $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
           <a href="${nameValueMap.new_path}">${nameValueMap.new_path} <i class="icon-login"></i></a>
         `);
@@ -388,7 +389,7 @@ $(function() {
   // duplicate
   $('#duplicatePage').on('shown.bs.modal', function(e) {
     $('#duplicatePage #duplicatePageName').focus();
-    $('#duplicatePage .msg-already-exists').hide();
+    $('#duplicatePage .msg').hide();
   });
   $('#duplicatePageForm, #unportalize-form').submit(function(e) {
     // create name-value map
@@ -403,9 +404,10 @@ $(function() {
       data: $(this).serialize(),
       dataType: 'json'
     }).done(function(res) {
+      // error
       if (!res.ok) {
-        // if already exists
-        $('#duplicatePage .msg-already-exists').show();
+        $('#duplicatePage .msg').hide();
+        $(`#duplicatePage .msg-${res.code}`).show();
         $('#duplicatePage #linkToNewPage').html(`
           <a href="${nameValueMap.new_path}">${nameValueMap.new_path} <i class="icon-login"></i></a>
         `);
@@ -420,6 +422,9 @@ $(function() {
   });
 
   // delete
+  $('#deletePage').on('shown.bs.modal', function(e) {
+    $('#deletePage .msg').hide();
+  });
   $('#delete-page-form').submit(function(e) {
     $.ajax({
       type: 'POST',
@@ -427,9 +432,10 @@ $(function() {
       data: $('#delete-page-form').serialize(),
       dataType: 'json'
     }).done(function(res) {
+      // error
       if (!res.ok) {
-        $('#delete-errors').html('<i class="fa fa-times-circle"></i> ' + res.error);
-        $('#delete-errors').addClass('alert-danger');
+        $('#deletePage .msg').hide();
+        $(`#deletePage .msg-${res.code}`).show();
       }
       else {
         const page = res.page;
@@ -439,6 +445,11 @@ $(function() {
 
     return false;
   });
+
+  // Put Back
+  $('#putBackPage').on('shown.bs.modal', function(e) {
+    $('#putBackPage .msg').hide();
+  });
   $('#revert-delete-page-form').submit(function(e) {
     $.ajax({
       type: 'POST',
@@ -446,9 +457,10 @@ $(function() {
       data: $('#revert-delete-page-form').serialize(),
       dataType: 'json'
     }).done(function(res) {
+      // error
       if (!res.ok) {
-        $('#delete-errors').html('<i class="fa fa-times-circle"></i> ' + res.error);
-        $('#delete-errors').addClass('alert-danger');
+        $('#putBackPage .msg').hide();
+        $(`#putBackPage .msg-${res.code}`).show();
       }
       else {
         const page = res.page;

+ 12 - 0
src/lib/models/cdn-resource.js

@@ -0,0 +1,12 @@
+/**
+ * Value Object
+ */
+class CdnResource {
+  constructor(name, url, outDir) {
+    this.name = name;
+    this.url = url;
+    this.outDir = outDir;
+  }
+}
+
+module.exports = CdnResource;

+ 2 - 12
src/lib/service/cdn-resources-downloader.js

@@ -7,17 +7,8 @@ const mkdirp = require('mkdirp');
 const replaceStream = require('replacestream');
 const streamToPromise = require('stream-to-promise');
 
+const CdnResource = require('../models/cdn-resource');
 
-/**
- * Value Object
- */
-class CdnResource {
-  constructor(name, url, outDir) {
-    this.name = name;
-    this.url = url;
-    this.outDir = outDir;
-  }
-}
 
 class CdnResourcesDownloader {
   constructor() {
@@ -109,7 +100,7 @@ class CdnResourcesDownloader {
    */
   generateReplaceUrlInCssStream(cdnResource, assetsResourcesStore, webroot) {
     return replaceStream(
-      /url\((?!"data:)["']?(.+?)["']?\)/g,    // https://regex101.com/r/Sds38A/2
+      /url\((?!['"]?data:)["']?(.+?)["']?\)/g,    // https://regex101.com/r/Sds38A/3
       (match, url) => {
         // generate URL Object
         const parsedUrl = url.startsWith('http')
@@ -152,5 +143,4 @@ class CdnResourcesDownloader {
 
 }
 
-CdnResourcesDownloader.CdnResource = CdnResource;
 module.exports = CdnResourcesDownloader;

+ 11 - 7
src/lib/service/cdn-resources-service.js

@@ -3,9 +3,6 @@ const urljoin = require('url-join');
 
 const helpers = require('@commons/util/helpers');
 
-const CdnResourcesDownloader = require('./cdn-resources-downloader');
-const CdnResource = CdnResourcesDownloader.CdnResource;
-
 const cdnLocalScriptRoot = 'public/js/cdn';
 const cdnLocalScriptWebRoot = '/js/cdn';
 const cdnLocalStyleRoot = 'public/styles/cdn';
@@ -39,8 +36,15 @@ class CdnResourcesService {
     return (manifests.length > 0) ? manifests[0] : null;
   }
 
-  async downloadAndWriteAll() {
-    const downloader = new CdnResourcesDownloader();
+  /**
+   * download all resources from CDN and write to FS
+   *
+   * !! This method should be invoked only by /bin/download-cdn-resources when build client !!
+   *
+   * @param {CdnResourceDownloader} cdnResourceDownloader
+   */
+  async downloadAndWriteAll(cdnResourceDownloader) {
+    const CdnResource = require('@commons/models/cdn-resource');
 
     const cdnScriptResources = this.cdnManifests.js.map(manifest => {
       const outDir = helpers.root(cdnLocalScriptRoot);
@@ -58,8 +62,8 @@ class CdnResourcesService {
     };
 
     return Promise.all([
-      downloader.downloadScripts(cdnScriptResources),
-      downloader.downloadStyles(cdnStyleResources, dlStylesOptions),
+      cdnResourceDownloader.downloadScripts(cdnScriptResources),
+      cdnResourceDownloader.downloadStyles(cdnStyleResources, dlStylesOptions),
     ]);
   }
 

+ 3 - 3
src/server/models/page.js

@@ -897,7 +897,7 @@ module.exports = function(crowi) {
         savedPage = newPage;
       })
       .then(() => {
-        const newRevision = Revision.prepareRevision(savedPage, body, user, {format: format});
+        const newRevision = Revision.prepareRevision(savedPage, body, null, user, {format: format});
         return pushRevision(savedPage, newRevision, user);
       })
       .then(() => {
@@ -908,7 +908,7 @@ module.exports = function(crowi) {
       });
   };
 
-  pageSchema.statics.updatePage = async function(pageData, body, user, options = {}) {
+  pageSchema.statics.updatePage = async function(pageData, body, previousBody, user, options = {}) {
     validateCrowi();
 
     const Page = this
@@ -922,7 +922,7 @@ module.exports = function(crowi) {
     // update existing page
     applyGrant(pageData, user, grant, grantUserGroupId);
     let savedPage = await pageData.save();
-    const newRevision = await Revision.prepareRevision(pageData, body, user);
+    const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
     const revision = await pushRevision(savedPage, newRevision, user, grant, grantUserGroupId);
     savedPage = await Page.findByPath(revision.path).populate('revision').populate('creator');
 

+ 2 - 2
src/server/models/revision.js

@@ -93,7 +93,7 @@ module.exports = function(crowi) {
     });
   };
 
-  revisionSchema.statics.prepareRevision = function(pageData, body, user, options) {
+  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, options) {
     const Revision = this;
 
     if (!options) {
@@ -112,7 +112,7 @@ module.exports = function(crowi) {
     newRevision.author = user._id;
     newRevision.createdAt = Date.now();
     if (pageData.revision != null) {
-      newRevision.hasDiffToPrev = body !== pageData.revision.body;
+      newRevision.hasDiffToPrev = body !== previousBody;
     }
 
     return newRevision;

+ 19 - 17
src/server/routes/page.js

@@ -527,7 +527,7 @@ module.exports = function(crowi, app) {
     // check page existence
     const isExist = await Page.count({path: pagePath}) > 0;
     if (isExist) {
-      return res.json(ApiResponse.error('Page exists'));
+      return res.json(ApiResponse.error('Page exists', 'already_exists'));
     }
 
     const options = {grant, grantUserGroupId, socketClientId};
@@ -584,13 +584,13 @@ module.exports = function(crowi, app) {
     // check page existence
     const isExist = await Page.count({_id: pageId}) > 0;
     if (!isExist) {
-      return res.json(ApiResponse.error(`Page('${pageId}' is not found or forbidden`));
+      return res.json(ApiResponse.error(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
     }
 
     // check revision
     let page = await Page.findByIdAndViewer(pageId, req.user);
     if (page != null && revisionId != null && !page.isUpdatable(revisionId)) {
-      return res.json(ApiResponse.error('Posted param "revisionId" is outdated.'));
+      return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'outdated'));
     }
 
     const options = {isSyncRevisionToHackmd, socketClientId};
@@ -602,7 +602,9 @@ module.exports = function(crowi, app) {
     }
 
     try {
-      page = await Page.updatePage(page, pageBody, req.user, options);
+      const Revision = crowi.model('Revision');
+      const previousRevision = await Revision.findById(revisionId);
+      page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
     }
     catch (err) {
       logger.error('error on _api/pages.update', err);
@@ -656,7 +658,7 @@ module.exports = function(crowi, app) {
       }
 
       if (page == null) {
-        throw new Error(`Page '${pageId || pagePath}' is not found or forbidden`);
+        throw new Error(`Page '${pageId || pagePath}' is not found or forbidden`, 'notfound_or_forbidden');
       }
 
       page.initLatestRevisionField();
@@ -836,7 +838,7 @@ module.exports = function(crowi, app) {
     let page = await Page.findByIdAndViewer(pageId, req.user);
 
     if (page == null) {
-      return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`));
+      return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
     }
 
     debug('Delete page', page._id, page.path);
@@ -852,7 +854,7 @@ module.exports = function(crowi, app) {
       }
       else {
         if (!page.isUpdatable(previousRevision)) {
-          throw new Error('Someone could update this page, so couldn\'t delete.');
+          return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
         }
 
         if (isRecursively) {
@@ -865,7 +867,7 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       logger.error('Error occured while get setting', err);
-      return res.json(ApiResponse.error('Failed to delete page.'));
+      return res.json(ApiResponse.error('Failed to delete page.', 'unknown'));
     }
 
     debug('Page deleted', page.path);
@@ -896,7 +898,7 @@ module.exports = function(crowi, app) {
     try {
       page = await Page.findByIdAndViewer(pageId, req.user);
       if (page == null) {
-        throw new Error(`Page '${pageId}' is not found or forbidden`);
+        throw new Error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden');
       }
 
       if (isRecursively) {
@@ -937,16 +939,16 @@ module.exports = function(crowi, app) {
       moveUnderTrees: req.body.move_trees || 0,
       socketClientId: +req.body.socketClientId || undefined,
     };
-    const isRecursiveMove = req.body.move_recursively || 0;
+    const isRecursively = req.body.recursively || 0;
 
     if (!Page.isCreatableName(newPagePath)) {
-      return res.json(ApiResponse.error(`このページ名は作成できません (${newPagePath})`));
+      return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
     }
 
     const isExist = await Page.count({ path: newPagePath }) > 0;
     if (isExist) {
       // if page found, cannot cannot rename to that path
-      return res.json(ApiResponse.error(`'new_path=${newPagePath}' already exists`));
+      return res.json(ApiResponse.error(`'new_path=${newPagePath}' already exists`, 'already_exists'));
     }
 
     let page;
@@ -955,14 +957,14 @@ module.exports = function(crowi, app) {
       page = await Page.findByIdAndViewer(pageId, req.user);
 
       if (page == null) {
-        throw new Error(`Page '${pageId}' is not found or forbidden`);
+        return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
       }
 
       if (!page.isUpdatable(previousRevision)) {
-        throw new Error('Someone could update this page, so couldn\'t delete.');
+        return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
       }
 
-      if (isRecursiveMove) {
+      if (isRecursively) {
         page = await Page.renameRecursively(page, newPagePath, req.user, options);
       }
       else {
@@ -971,7 +973,7 @@ module.exports = function(crowi, app) {
     }
     catch (err) {
       logger.error(err);
-      return res.json(ApiResponse.error('Failed to update page.'));
+      return res.json(ApiResponse.error('Failed to update page.', 'unknown'));
     }
 
     const result = {};
@@ -1000,7 +1002,7 @@ module.exports = function(crowi, app) {
     const page = await Page.findByIdAndViewer(pageId, req.user);
 
     if (page == null) {
-      throw new Error(`Page '${pageId}' is not found or forbidden`);
+      return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
     }
 
     await page.populateDataToShowRevision();

+ 1 - 1
src/server/routes/revision.js

@@ -33,7 +33,7 @@ module.exports = function(crowi, app) {
     }
 
     try {
-      const revision = await Revision.findById(revisionId);
+      const revision = await Revision.findById(revisionId).populate('author', 'User');
       return res.json(ApiResponse.success({ revision }));
     }
     catch (err) {

+ 1 - 1
src/server/service/file-uploader/aws.js

@@ -162,7 +162,7 @@ module.exports = function(crowi) {
   };
 
   /**
-   * chech storage for fileUpload reaches MONGODB_GRIDFS_LIMIT (for gridfs)
+   * chech storage for fileUpload reaches MONGO_GRIDFS_TOTAL_LIMIT (for gridfs)
    */
   lib.checkCapacity = async(uploadFileSize) => {
     return true;

+ 3 - 3
src/server/service/file-uploader/gridfs.js

@@ -60,16 +60,16 @@ module.exports = function(crowi) {
   };
 
   /**
-   * chech storage for fileUpload reaches MONGODB_GRIDFS_LIMIT (for gridfs)
+   * chech storage for fileUpload reaches MONGO_GRIDFS_TOTAL_LIMIT (for gridfs)
    */
   lib.checkCapacity = async(uploadFileSize) => {
     // skip checking if env var is undefined
-    if (process.env.MONGODB_GRIDFS_LIMIT == null) {
+    if (process.env.MONGO_GRIDFS_TOTAL_LIMIT == null) {
       return true;
     }
 
     const usingFilesSize = await getCollectionSize();
-    return (+process.env.MONGODB_GRIDFS_LIMIT > usingFilesSize + +uploadFileSize);
+    return (+process.env.MONGO_GRIDFS_TOTAL_LIMIT > usingFilesSize + +uploadFileSize);
   };
 
   lib.uploadFile = async function(filePath, contentType, fileStream, options) {

+ 1 - 1
src/server/service/file-uploader/local.js

@@ -56,7 +56,7 @@ module.exports = function(crowi) {
   };
 
   /**
-   * chech storage for fileUpload reaches MONGODB_GRIDFS_LIMIT (for gridfs)
+   * chech storage for fileUpload reaches MONGO_GRIDFS_TOTAL_LIMIT (for gridfs)
    */
   lib.checkCapacity = async(uploadFileSize) => {
     return true;

+ 5 - 6
src/server/util/apiResponse.js

@@ -3,12 +3,11 @@
 function ApiResponse() {
 }
 
-ApiResponse.error = function(err) {
-  var result = {};
+ApiResponse.error = function(err, code) {
+  const result = {};
 
-  result = {
-    ok: false
-  };
+  result.ok = false;
+  result.code = code;
 
   if (err instanceof Error) {
     result.error = err.toString();
@@ -21,7 +20,7 @@ ApiResponse.error = function(err) {
 };
 
 ApiResponse.success = function(data) {
-  var result = data || {};
+  const result = data || {};
 
   result.ok = true;
   return result;

+ 1 - 1
src/server/views/admin/app.html

@@ -60,7 +60,7 @@
         </div>
 
         <div class="form-group">
-          <label class="col-xs-3 control-label tbd">(TBD) {{ t('app_setting.Default Language for new users') }}</label>
+          <label class="col-xs-3 control-label">{{ t('app_setting.Default Language for new users') }}</label>
           <div class="col-xs-6">
             <div class="radio radio-primary radio-inline">
                 <input type="radio" id="radioLangEn" name="settingForm[app:globalLang]" value="{{ consts.language.LANG_EN }}" {% if appGlobalLang() == consts.language.LANG_EN %}checked="checked"{% endif %}>

+ 19 - 8
src/server/views/modal/delete.html

@@ -8,7 +8,7 @@
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
           <div class="modal-title">
             {% if page.isDeleted() %}
-            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.label.completely') }}
+            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.label.Delete completely') }}
             {% else %}
             <i class="icon-fw icon-trash"></i> {{ t('modal_delete.label.Delete Page') }}
             {% endif %}
@@ -19,18 +19,32 @@
             <label for="">Deleting page:</label><br>
             <code>{{ page.path }}</code>
           </div>
+
+          <hr>
+
+          <div class="checkbox checkbox-warning">
+            <input name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
+            <label for="cbDeleteRecursively">{{ t('modal_delete.label.Delete recursively') }}</label>
+            <p class="help-block"> {{ t('modal_delete.help.recursively', page.path) }}
+            </p>
+          </div>
+          {% if not page.isDeleted() %}
+          <div class="checkbox checkbox-danger">
+            <input name="completely" id="cbDeleteCompletely" value="1"  type="checkbox">
+              <label for="cbDeleteCompletely" class="text-danger">{{ t('modal_delete.label.Delete completely') }}</label>
+              <p class="help-block"> {{ t('modal_delete.help.completely') }}
+              </p>
+          </div>
+          {% endif %}
         </div>
         <div class="modal-footer">
           <div class="d-flex justify-content-between">
-            <p><small id="delete-errors"></small></p>
+            {% include '../widget/modal/page-api-error-messages.html' %}
             <div>
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="hidden" name="path" value="{{ page.path }}">
               <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
               <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-              <label class="checkbox-inline">
-                <input type="checkbox" name="recursively">{{ t('modal_delete.label.recursively') }}
-              </label>
               {% if page.isDeleted() %}
                 <input type="hidden" name="completely" value="true">
                 <button type="submit" class="m-l-10 btn btn-sm btn-danger delete-button">
@@ -38,9 +52,6 @@
                   {{ t('Delete Completely') }}
                 </button>
               {% else %}
-                <label class="checkbox-inline text-danger">
-                  <input type="checkbox" name="completely">{{ t('modal_delete.label.completely') }}
-                </label>
                 <button type="submit" class="m-l-10 btn btn-sm btn-default delete-button">
                   <i class="icon-trash" aria-hidden="true"></i>
                   {{ t('Delete') }}

+ 1 - 6
src/server/views/modal/duplicate.html

@@ -23,12 +23,7 @@
         </div>
         <div class="modal-footer">
           <div class="d-flex justify-content-between">
-            <p>
-              <span class="text-danger msg-already-exists">
-                <strong><i class="icon-fw icon-ban"></i>{{ t('Page is already exists.') }}</strong>
-              </span>
-              <small id="linkToNewPage" class="msg-already-exists"></small>
-            </p>
+            {% include '../widget/modal/page-api-error-messages.html' %}
             <div>
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="hidden" name="path" value="{{ page.path }}">

+ 19 - 12
src/server/views/modal/put_back.html

@@ -6,26 +6,33 @@
 
         <div class="modal-header bg-info">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title"><i class="icon-action-undo"></i> {{ t('modal_putBack.label.Put Back Page') }}</div>
+          <div class="modal-title"><i class="icon-action-undo"></i> {{ t('modal_putback.label.Put Back Page') }}</div>
         </div>
         <div class="modal-body">
           <div class="form-group">
             <label for="">Put back page:</label><br>
             <code>{{ page.path }}</code>
           </div>
+          <div class="checkbox checkbox-warning">
+            <input name="recursively" id="cbPutbackRecursively" value="1" type="checkbox" checked>
+            <label for="cbPutbackRecursively">{{ t('modal_putback.label.recursively') }}</label>
+            <p class="help-block"> {{ t('modal_putback.help.recursively', page.path) }}
+            </p>
+          </div>
         </div>
         <div class="modal-footer">
-          <p><small class="pull-left" id="put_back-errors"></small></p>
-          <input type="hidden" name="_csrf" value="{{ csrf() }}">
-          <input type="hidden" name="path" value="{{ page.path }}">
-          <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-          <label class="checkbox-inline">
-            <input type="checkbox" name="recursively" checked> {{ t('modal_putBack.label.recursively') }}
-          </label>
-          <button type="submit" class="btn btn-sm btn-info putBack-button">
-            <i class="icon-action-undo" aria-hidden="true"></i>
-            {{ t('Put Back') }}
-          </button>
+          <div class="d-flex justify-content-between">
+            {% include '../widget/modal/page-api-error-messages.html' %}
+            <div>
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <input type="hidden" name="path" value="{{ page.path }}">
+              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
+              <button type="submit" class="btn btn-sm btn-info putBack-button">
+                <i class="icon-action-undo" aria-hidden="true"></i>
+                {{ t('Put Back') }}
+              </button>
+            </div>
+          </div>
         </div>
 
       </form>

+ 26 - 35
src/server/views/modal/rename.html

@@ -6,48 +6,39 @@
 
         <div class="modal-header bg-primary">
           <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-          <div class="modal-title">{{ t('Rename page') }}</div>
+          <div class="modal-title">{{ t('modal_rename.label.Rename page') }}</div>
         </div>
         <div class="modal-body">
-            <div class="form-group">
-              <label for="">{{ t('Current page name') }}</label><br>
-              <code>{{ page.path }}</code>
-            </div>
-            <div class="form-group">
-              <label for="newPageName">{{ t('New page name') }}</label><br>
-              <div class="input-group">
-                <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
-                <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
-              </div>
+          <div class="form-group">
+            <label for="">{{ t('modal_rename.label.Current page name') }}</label><br>
+            <code>{{ page.path }}</code>
+          </div>
+          <div class="form-group">
+            <label for="newPageName">{{ t('modal_rename.label.New page name') }}</label><br>
+            <div class="input-group">
+              <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
+              <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
             </div>
-            <div class="checkbox checkbox-info">
-              <input name="move_recursively" id="cbRecursively" value="1" type="checkbox" checked>
-              <label for="cbRecursively">{{ t('modal_rename.label.Move recursively') }}</label>
-              <p class="help-block"> {{ t('modal_rename.help.recursive', page.path) }}
+          </div>
+
+          <hr>
+
+          <div class="checkbox checkbox-warning">
+            <input name="recursively" id="cbRenameRecursively" value="1" type="checkbox" checked>
+            <label for="cbRenameRecursively">{{ t('modal_rename.label.Move recursively') }}</label>
+            <p class="help-block"> {{ t('modal_rename.help.recursive', page.path) }}
+            </p>
+          </div>
+          <div class="checkbox checkbox-info">
+            <input name="create_redirect" id="cbRenameRedirect" value="1"  type="checkbox">
+              <label for="cbRenameRedirect">{{ t('modal_rename.label.Redirect') }}</label>
+              <p class="help-block"> {{ t('modal_rename.help.redirect', page.path) }}
               </p>
-            </div>
-            <div class="checkbox checkbox-info">
-              <input name="create_redirect" id="cbRedirect" value="1"  type="checkbox">
-               <label for="cbRedirect">{{ t('modal_rename.label.Redirect') }}</label>
-               <p class="help-block"> {{ t('modal_rename.help.redirect', page.path) }}
-               </p>
-            </div>
-            {# <div class="checkbox"> #}
-            {#    <label> #}
-            {#      <input name="moveUnderTrees" value="1" type="checkbox"> 下層ページも全部移動する #}
-            {#    </label> #}
-            {#    <p class="help-block">チェックを入れると、<code>{{ page.path }}</code>以下の階層以下もすべて移動します。</p> #}
-            {#    <p class="help-block">例: <code>/hoge/fuga/move</code> を <code>/foo/bar/move</code> に移動すると、<code>/hoge/fuga/move/page1</code> も <code>/foo/bar/move/page1</code> に。</p> #}
-            {# </div> #}
+          </div>
         </div>
         <div class="modal-footer">
           <div class="d-flex justify-content-between">
-            <p>
-              <span class="text-danger msg-already-exists">
-                <strong><i class="icon-fw icon-ban"></i>{{ t('Page is already exists.') }}</strong>
-              </span>
-              <small id="linkToNewPage" class="msg-already-exists"></small>
-            </p>
+            {% include '../widget/modal/page-api-error-messages.html' %}
             <div>
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="hidden" name="path" value="{{ page.path }}">

+ 1 - 6
src/server/views/modal/unportalize.html

@@ -33,12 +33,7 @@
         </div>
         <div class="modal-footer">
           <div class="d-flex justify-content-between">
-            <p>
-              <span class="text-danger msg-already-exists">
-                <strong><i class="icon-fw icon-ban"></i>{{ t('Page is already exists.') }}</strong>
-              </span>
-              <small id="linkToNewPage" class="msg-already-exists"></small>
-            </p>
+            {% include '../widget/modal/page-api-error-messages.html' %}
             <div>
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="hidden" name="path" value="{{ page.path }}">

+ 21 - 0
src/server/views/widget/modal/page-api-error-messages.html

@@ -0,0 +1,21 @@
+<p>
+  <span class="text-danger msg msg-notfound_or_forbidden">
+    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.notfound_or_forbidden') }}</strong>
+  </span>
+  <span class="text-danger msg msg-already_exists">
+    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.already_exists') }}</strong>
+    <small id="linkToNewPage"></small>
+  </span>
+  <span class="text-warning msg msg-outdated">
+    <strong><i class="icon-fw icon-bulb"></i> {{ t('page_api_error.outdated') }}</strong>
+    <a href="javascript:location.reload();">
+      <i class="fa fa-angle-double-right"></i> {{ t('Load latest') }}
+    </a>
+  </span>
+  <span class="text-danger msg msg-invalid_path">
+    <strong><i class="icon-fw icon-ban"></i> Invalid path</strong>
+  </span>
+  <span class="text-danger msg msg-unknown">
+    <strong><i class="icon-fw icon-ban"></i> Unknown error occured</strong>
+  </span>
+</p>