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

Merge pull request #771 from weseek/imprv/inherit-parent-scope

Imprv/inherit parent scope
Yuki Takei 7 лет назад
Родитель
Сommit
7e9628af9c

+ 1 - 0
resource/locales/en-US/translation.json

@@ -170,6 +170,7 @@
 
   "page_edit": {
     "Show active line": "Show active line",
+    "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
     "notice": {
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
     }

+ 1 - 0
resource/locales/ja/translation.json

@@ -186,6 +186,7 @@
 
   "page_edit": {
     "Show active line": "アクティブ行をハイライト",
+    "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
     "notice": {
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     }

+ 5 - 2
src/client/js/app.js

@@ -213,7 +213,7 @@ const saveWithSubmitButtonSuccessHandler = function() {
   location.href = pagePath;
 };
 
-const saveWithSubmitButton = function() {
+const saveWithSubmitButton = function(submitOpts) {
   const editorMode = crowi.getCrowiForJquery().getCurrentEditorMode();
   if (editorMode == null) {
     // do nothing
@@ -225,6 +225,9 @@ const saveWithSubmitButton = function() {
   const options = componentInstances.savePageControls.getCurrentOptionsToSave();
   options.socketClientId = socketClientId;
 
+  // set 'submitOpts.overwriteScopesOfDescendants' to options
+  options.overwriteScopesOfDescendants = submitOpts ? !!submitOpts.overwriteScopesOfDescendants : false;
+
   let promise = undefined;
   if (editorMode === 'hackmd') {
     // get markdown
@@ -409,7 +412,7 @@ const pageEditorOptionsSelectorElem = document.getElementById('page-editor-optio
 if (pageEditorOptionsSelectorElem) {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-    <OptionsSelector crowi={crowi}
+      <OptionsSelector crowi={crowi}
         editorOptions={editorOptions} previewOptions={previewOptions}
         onChange={(newEditorOptions, newPreviewOptions) => { // set onChange event handler
           // set options

+ 18 - 3
src/client/js/components/SavePageControls.jsx

@@ -2,6 +2,10 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { translate } from 'react-i18next';
 
+import ButtonToolbar from 'react-bootstrap/es/ButtonToolbar';
+import SplitButton  from 'react-bootstrap/es/SplitButton';
+import MenuItem from 'react-bootstrap/es/MenuItem';
+
 import SlackNotification from './SlackNotification';
 import GrantSelector from './SavePageControls/GrantSelector';
 
@@ -16,6 +20,7 @@ class SavePageControls extends React.PureComponent {
 
     this.getCurrentOptionsToSave = this.getCurrentOptionsToSave.bind(this);
     this.submit = this.submit.bind(this);
+    this.submitAndOverwriteScopesOfDescendants = this.submitAndOverwriteScopesOfDescendants.bind(this);
   }
 
   componentWillMount() {
@@ -39,12 +44,17 @@ class SavePageControls extends React.PureComponent {
     this.props.onSubmit();
   }
 
+  submitAndOverwriteScopesOfDescendants() {
+    this.props.onSubmit({ overwriteScopesOfDescendants: true });
+  }
+
   render() {
     const { t } = this.props;
 
     const config = this.props.crowi.getConfig();
     const isAclEnabled = config.isAclEnabled;
-    const label = this.state.pageId == null ? t('Create') : t('Update');
+    const labelSubmitButton = this.state.pageId == null ? t('Create') : t('Update');
+    const labelOverwriteScopes = t('page_edit.overwrite_scopes', { operation: labelSubmitButton });
 
     return (
       <div className="d-flex align-items-center form-inline">
@@ -58,7 +68,6 @@ class SavePageControls extends React.PureComponent {
               slackChannels={this.props.slackChannels} />
         </div>
 
-
         {isAclEnabled &&
           <div className="mr-2">
             <GrantSelector crowi={this.props.crowi}
@@ -73,7 +82,13 @@ class SavePageControls extends React.PureComponent {
           </div>
         }
 
-        <button className="btn btn-primary btn-submit" onClick={this.submit}>{label}</button>
+        <ButtonToolbar>
+          <SplitButton id="spl-btn-submit" bsStyle="primary" className="btn-submit" dropup pullRight onClick={this.submit}
+              title={labelSubmitButton}>
+            <MenuItem eventKey="1" onClick={this.submitAndOverwriteScopesOfDescendants}>{labelOverwriteScopes}</MenuItem>
+            {/* <MenuItem divider /> */}
+          </SplitButton>
+        </ButtonToolbar>
       </div>
     );
   }

+ 132 - 76
src/server/models/page.js

@@ -52,8 +52,8 @@ const pageSchema = new mongoose.Schema({
   pageIdOnHackmd: String,
   revisionHackmdSynced: { type: ObjectId, ref: 'Revision' },  // the revision that is synced to HackMD
   hasDraftOnHackmd: { type: Boolean },                        // set true if revision and revisionHackmdSynced are same but HackMD document has modified
-  createdAt: { type: Date, default: Date.now },
-  updatedAt: Date
+  createdAt: { type: Date, default: Date.now() },
+  updatedAt: { type: Date, default: Date.now() },
 }, {
   toJSON: {getters: true},
   toObject: {getters: true}
@@ -409,6 +409,22 @@ module.exports = function(crowi) {
     return this.populate('revision').execPopulate();
   };
 
+  pageSchema.methods.applyScope = function(user, grant, grantUserGroupId) {
+    this.grant = grant;
+
+    // reset
+    this.grantedUsers = [];
+    this.grantedGroup = null;
+
+    if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP) {
+      this.grantedUsers.push(user._id);
+    }
+
+    if (grant === GRANT_USER_GROUP) {
+      this.grantedGroup = grantUserGroupId;
+    }
+  }
+
 
   pageSchema.statics.updateCommentCount = function(pageId) {
     validateCrowi();
@@ -542,19 +558,20 @@ module.exports = function(crowi) {
   /**
    * @param {string} id ObjectId
    * @param {User} user User instance
+   * @param {UserGroup[]} userGroups List of UserGroup instances
    */
-  pageSchema.statics.findByIdAndViewer = async function(id, user) {
+  pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups) {
     const baseQuery = this.findOne({_id: id});
 
-    let userGroups = [];
-    if (user != null) {
+    let relatedUserGroups = userGroups;
+    if (user != null && relatedUserGroups == null) {
       validateCrowi();
       const UserGroupRelation = crowi.model('UserGroupRelation');
-      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
     const queryBuilder = new PageQueryBuilder(baseQuery);
-    queryBuilder.addConditionToFilteringByViewer(user, userGroups);
+    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
 
     return await queryBuilder.query.exec();
   };
@@ -567,25 +584,61 @@ module.exports = function(crowi) {
     return this.findOne({path});
   };
 
-  pageSchema.statics.findByPathAndViewer = async function(path, user) {
+  /**
+   * @param {string} path Page path
+   * @param {User} user User instance
+   * @param {UserGroup[]} userGroups List of UserGroup instances
+   */
+  pageSchema.statics.findByPathAndViewer = async function(path, user, userGroups) {
     if (path == null) {
       throw new Error('path is required.');
     }
 
-    // const Page = this;
     const baseQuery = this.findOne({path});
-    const queryBuilder = new PageQueryBuilder(baseQuery);
 
-    if (user != null) {
+    let relatedUserGroups = userGroups;
+    if (user != null && relatedUserGroups == null) {
       validateCrowi();
       const UserGroupRelation = crowi.model('UserGroupRelation');
-      const userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
-      queryBuilder.addConditionToFilteringByViewer(user, userGroups);
+      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     }
 
+    const queryBuilder = new PageQueryBuilder(baseQuery);
+    queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
+
     return await queryBuilder.query.exec();
   };
 
+  /**
+   * @param {string} path Page path
+   * @param {User} user User instance
+   * @param {UserGroup[]} userGroups List of UserGroup instances
+   */
+  pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups) {
+    if (path == null) {
+      throw new Error('path is required.');
+    }
+
+    if (path === '/') {
+      return null;
+    }
+
+    const parentPath = nodePath.dirname(path);
+
+    let relatedUserGroups = userGroups;
+    if (user != null && relatedUserGroups == null) {
+      validateCrowi();
+      const UserGroupRelation = crowi.model('UserGroupRelation');
+      relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+    }
+
+    const page = await this.findByPathAndViewer(parentPath, user, relatedUserGroups);
+
+    return (page != null)
+      ? page
+      : this.findAncestorByPathAndViewer(parentPath, user, relatedUserGroups);
+  };
+
   pageSchema.statics.findByRedirectTo = function(path) {
     return this.findOne({redirectTo: path});
   };
@@ -826,20 +879,11 @@ module.exports = function(crowi) {
     return pageData.save();
   }
 
-  async function applyGrant(page, user, grant, grantUserGroupId) {
+  async function validateAppliedScope(user, grant, grantUserGroupId) {
     if (grant == GRANT_USER_GROUP && grantUserGroupId == null) {
       throw new Error('grant userGroupId is not specified');
     }
 
-    page.grant = grant;
-    if (grant == GRANT_PUBLIC || grant == GRANT_USER_GROUP) {
-      page.grantedUsers = [];
-    }
-    else {
-      page.grantedUsers = [];
-      page.grantedUsers.push(user._id);
-    }
-
     if (grant == GRANT_USER_GROUP) {
       const UserGroupRelation = crowi.model('UserGroupRelation');
       const count = await UserGroupRelation.countByGroupIdAndUser(grantUserGroupId, user);
@@ -847,86 +891,75 @@ module.exports = function(crowi) {
       if (count === 0) {
         throw new Error('no relations were exist for group and user.');
       }
-
-      page.grantedGroup = grantUserGroupId;
     }
   }
 
-  pageSchema.statics.create = function(path, body, user, options = {}) {
+  pageSchema.statics.create = async function(path, body, user, options = {}) {
     validateCrowi();
 
-    const Page = this
-      , Revision = crowi.model('Revision')
-      , format = options.format || 'markdown'
-      , redirectTo = options.redirectTo || null
-      , grantUserGroupId = options.grantUserGroupId || null
-      , socketClientId = options.socketClientId || null
-      ;
-
-    let grant = options.grant || GRANT_PUBLIC;
+    const Page = this;
+    const Revision = crowi.model('Revision');
+    const format = options.format || 'markdown';
+    const redirectTo = options.redirectTo || null;
+    const grantUserGroupId = options.grantUserGroupId || null;
+    const socketClientId = options.socketClientId || null;
 
     // sanitize path
     path = crowi.xss.process(path);
 
+    let grant = options.grant || GRANT_PUBLIC;
     // force public
     if (isPortalPath(path)) {
       grant = GRANT_PUBLIC;
     }
 
-    let savedPage = undefined;
-    return Page.findOne({path: path})
-      .then(pageData => {
-        if (pageData) {
-          throw new Error('Cannot create new page to existed path');
-        }
+    const isExist = await this.count({path: path});
 
-        const newPage = new Page();
-        newPage.path = path;
-        newPage.creator = user;
-        newPage.lastUpdateUser = user;
-        newPage.createdAt = Date.now();
-        newPage.updatedAt = Date.now();
-        newPage.redirectTo = redirectTo;
-        newPage.status = STATUS_PUBLISHED;
-        applyGrant(newPage, user, grant, grantUserGroupId);
-
-        return newPage.save();
-      })
-      .then((newPage) => {
-        savedPage = newPage;
-      })
-      .then(() => {
-        const newRevision = Revision.prepareRevision(savedPage, body, null, user, {format: format});
-        return pushRevision(savedPage, newRevision, user);
-      })
-      .then(() => {
-        if (socketClientId != null) {
-          pageEvent.emit('create', savedPage, user, socketClientId);
-        }
-        return savedPage;
-      });
+    if (isExist) {
+      throw new Error('Cannot create new page to existed path');
+    }
+
+    const page = new Page();
+    page.path = path;
+    page.creator = user;
+    page.lastUpdateUser = user;
+    page.redirectTo = redirectTo;
+    page.status = STATUS_PUBLISHED;
+
+    await validateAppliedScope(user, grant, grantUserGroupId);
+    page.applyScope(user, grant, grantUserGroupId);
+
+    let savedPage = await page.save();
+    const newRevision = Revision.prepareRevision(savedPage, body, null, user, {format: format});
+    const revision = await pushRevision(savedPage, newRevision, user, grant, grantUserGroupId);
+    savedPage = await this.findByPath(revision.path).populate('revision').populate('creator');
+
+    if (socketClientId != null) {
+      pageEvent.emit('create', savedPage, user, socketClientId);
+    }
+    return savedPage;
   };
 
   pageSchema.statics.updatePage = async function(pageData, body, previousBody, user, options = {}) {
     validateCrowi();
 
-    const Page = this
-      , Revision = crowi.model('Revision')
-      , grant = options.grant || null
-      , grantUserGroupId = options.grantUserGroupId || null
-      , isSyncRevisionToHackmd = options.isSyncRevisionToHackmd
-      , socketClientId = options.socketClientId || null
-      ;
+    const Revision = crowi.model('Revision');
+    const grant = options.grant || null;
+    const grantUserGroupId = options.grantUserGroupId || null;
+    const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
+    const socketClientId = options.socketClientId || null;
+
+    await validateAppliedScope(user, grant, grantUserGroupId);
+    pageData.applyScope(user, grant, grantUserGroupId);
 
     // update existing page
-    applyGrant(pageData, user, grant, grantUserGroupId);
     let savedPage = await pageData.save();
     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');
+    savedPage = await this.findByPath(revision.path).populate('revision').populate('creator');
 
     if (isSyncRevisionToHackmd) {
-      savedPage = await Page.syncRevisionToHackmd(savedPage);
+      savedPage = await this.syncRevisionToHackmd(savedPage);
     }
 
     if (socketClientId != null) {
@@ -935,6 +968,29 @@ module.exports = function(crowi) {
     return savedPage;
   };
 
+  pageSchema.statics.applyScopesToDescendantsAsyncronously = async function(parentPage, user) {
+    const builder = new PageQueryBuilder(this.find());
+    builder.addConditionToListWithDescendants(parentPage.path);
+
+    builder.addConditionToExcludeRedirect();
+
+    // add grant conditions
+    await addConditionToFilteringByViewerForList(builder, user);
+
+    // get all pages that the specified user can update
+    const pages = await builder.query.exec();
+
+    for (const page of pages) {
+      // skip parentPage
+      if (page.id === parentPage.id) {
+        continue;
+      }
+
+      page.applyScope(user, parentPage.grant, parentPage.grantedGroup);
+      page.save();
+    }
+  };
+
   pageSchema.statics.deletePage = async function(pageData, user, options = {}) {
     const newPath = this.getDeletedPageName(pageData.path)
       , isTrashed = checkIfTrashed(pageData.path)

+ 27 - 1
src/server/routes/page.js

@@ -124,6 +124,12 @@ module.exports = function(crowi, app) {
     }
   }
 
+  function addRendarVarsForScope(renderVars, page) {
+    renderVars.grant = page.grant;
+    renderVars.grantedGroupId = page.grantedGroup ? page.grantedGroup.id : null;
+    renderVars.grantedGroupName = page.grantedGroup ? page.grantedGroup.name : null;
+  }
+
   async function addRenderVarsForSlack(renderVars, page) {
     renderVars.slack = await getSlackChannels(page);
   }
@@ -241,6 +247,7 @@ module.exports = function(crowi, app) {
     // populate
     page = await page.populateDataToShowRevision();
     addRendarVarsForPage(renderVars, page);
+    addRendarVarsForScope(renderVars, page);
 
     await addRenderVarsForSlack(renderVars, page);
     await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit);
@@ -397,6 +404,13 @@ module.exports = function(crowi, app) {
         template = replacePlaceholdersOfTemplate(template, req);
         renderVars.template = template;
       }
+
+      // add scope variables by ancestor page
+      const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
+      if (ancestor != null) {
+        await ancestor.populate('grantedGroup').execPopulate();
+        addRendarVarsForScope(renderVars, ancestor);
+      }
     }
 
     const limit = 50;
@@ -516,6 +530,7 @@ module.exports = function(crowi, app) {
     const pagePath = req.body.path || null;
     const grant = req.body.grant || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
+    const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const isSlackEnabled = !!req.body.isSlackEnabled;   // cast to boolean
     const slackChannels = req.body.slackChannels || null;
     const socketClientId = req.body.socketClientId || undefined;
@@ -530,7 +545,7 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Page exists', 'already_exists'));
     }
 
-    const options = {grant, grantUserGroupId, socketClientId};
+    const options = {grant, grantUserGroupId, overwriteScopesOfDescendants, socketClientId};
     const createdPage = await Page.create(pagePath, body, req.user, options);
 
     const result = { page: serializeToObj(createdPage) };
@@ -538,6 +553,11 @@ module.exports = function(crowi, app) {
     result.page.creator = User.filterToPublicFields(createdPage.creator);
     res.json(ApiResponse.success(result));
 
+    // update scopes for descendants
+    if (overwriteScopesOfDescendants) {
+      Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
+    }
+
     // global notification
     try {
       await globalNotificationService.notifyPageCreate(createdPage);
@@ -572,6 +592,7 @@ module.exports = function(crowi, app) {
     const revisionId = req.body.revision_id || null;
     const grant = req.body.grant || null;
     const grantUserGroupId = req.body.grantUserGroupId || null;
+    const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
     const isSlackEnabled = !!req.body.isSlackEnabled;                     // cast to boolean
     const slackChannels = req.body.slackChannels || null;
     const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd;     // cast to boolean
@@ -615,6 +636,11 @@ module.exports = function(crowi, app) {
     result.page.lastUpdateUser = User.filterToPublicFields(page.lastUpdateUser);
     res.json(ApiResponse.success(result));
 
+    // update scopes for descendants
+    if (overwriteScopesOfDescendants) {
+      Page.applyScopesToDescendantsAsyncronously(page, req.user);
+    }
+
     // global notification
     try {
       await globalNotificationService.notifyPageEdit(page);

+ 3 - 3
src/server/views/_form.html

@@ -16,9 +16,9 @@
   </div>
 
   <div id="save-page-controls"
-    data-grant="{{ page.grant }}"
-    data-grant-group="{{ page.grantedGroup._id.toString() }}"
-    data-grant-group-name="{{ page.grantedGroup.name }}">
+    data-grant="{{ grant }}"
+    data-grant-group="{{ grantedGroupId }}"
+    data-grant-group-name="{{ grantedGroupName }}">
   </div>
 
 </div>