فهرست منبع

Merge branch 'master' into rc/1.0.x

Yuki Takei 9 سال پیش
والد
کامیت
badb173c62

+ 12 - 0
CHANGES.md

@@ -1,8 +1,20 @@
 CHANGES
 ========
 
+## 1.0.8
+
+* Feature: Ensure to delete page completely
+* Feature: Ensure to delete redirect page
+* Fix: https access to Gravatar (this time for sure)
+
+## 1.0.7
+
+* Feature: Keyboard navigation for search box
+* Improvement: Intelligent Search
+
 ## 1.0.6
 
+* Feature: Copy button that copies page path to clipboard
 * Fix: https access to Gravatar
 * Fix: server watching crash with `Error: read ECONNRESET` on Google Chrome
 

+ 54 - 1
lib/models/page.js

@@ -536,6 +536,20 @@ module.exports = function(crowi) {
     });
   };
 
+  pageSchema.statics.findPageByRedirectTo = function(path) {
+    var Page = this;
+
+    return new Promise(function(resolve, reject) {
+      Page.findOne({redirectTo: path}, function(err, pageData) {
+        if (err || pageData === null) {
+          return reject(err);
+        }
+
+        return resolve(pageData);
+      });
+    });
+  };
+
   pageSchema.statics.findListByCreator = function(user, option, currentUser) {
     var Page = this;
     var User = crowi.model('User');
@@ -903,9 +917,11 @@ module.exports = function(crowi) {
         return Revision.removeRevisionsByPath(pageData.path);
       }).then(function(done) {
         return Page.removePageById(pageId);
+      }).then(function(done) {
+        return Page.removeRedirectOriginPageByPath(pageData.path);
       }).then(function(done) {
         pageEvent.emit('delete', pageData, user); // update as renamed page
-        resolve();
+        resolve(pageData);
       }).catch(reject);
     });
   };
@@ -925,6 +941,43 @@ module.exports = function(crowi) {
     });
   };
 
+  pageSchema.statics.removePageByPath = function(pagePath) {
+    var Page = this;
+
+    return Page.findPageByPath(redirectPath)
+      .then(function(pageData) {
+        return Page.removePageById(pageData.id);
+      });
+  };
+
+  /**
+   * remove the page that is redirecting to specified `pagePath` recursively
+   *  ex: when
+   *    '/page1' redirects to '/page2' and
+   *    '/page2' redirects to '/page3'
+   *    and given '/page3',
+   *    '/page1' and '/page2' will be removed
+   *
+   * @param {string} pagePath
+   */
+  pageSchema.statics.removeRedirectOriginPageByPath = function(pagePath) {
+    var Page = this;
+
+    return Page.findPageByRedirectTo(pagePath)
+      .then((redirectOriginPageData) => {
+        // remove
+        return Page.removePageById(redirectOriginPageData.id)
+          // remove recursive
+          .then(() => {
+            return Page.removeRedirectOriginPageByPath(redirectOriginPageData.path)
+          });
+      })
+      .catch((err) => {
+        // do nothing if origin page doesn't exist
+        return Promise.resolve();
+      })
+  };
+
   pageSchema.statics.rename = function(pageData, newPagePath, user, options) {
     var Page = this
       , Revision = crowi.model('Revision')

+ 1 - 0
lib/routes/index.js

@@ -113,6 +113,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.rename'       , accessTokenParser , loginRequired(crowi, app) , csrf, page.api.rename);
   app.post('/_api/pages.remove'       , loginRequired(crowi, app) , csrf, page.api.remove); // (Avoid from API Token)
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
+  app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);
   app.get( '/_api/bookmarks.get'      , accessTokenParser , loginRequired(crowi, app) , bookmark.api.get);

+ 39 - 1
lib/routes/page.js

@@ -228,7 +228,7 @@ module.exports = function(crowi, app) {
     }
 
     if (pageData.redirectTo) {
-      return res.redirect(encodeURI(pageData.redirectTo + '?renamed=' + pageData.path));
+      return res.redirect(encodeURI(pageData.redirectTo + '?redirectFrom=' + pageData.path));
     }
 
     var renderVars = {
@@ -752,10 +752,19 @@ module.exports = function(crowi, app) {
     var pageId = req.body.page_id;
     var previousRevision = req.body.revision_id || null;
 
+    // get completely flag
+    const isCompletely = (req.body.completely !== undefined);
+
     Page.findPageByIdAndGrantedUser(pageId, req.user)
     .then(function(pageData) {
       debug('Delete page', pageData._id, pageData.path);
 
+      if (isCompletely) {
+        return Page.completelyDeletePage(pageData, req.user);
+      }
+
+      // else
+
       if (!pageData.isUpdatable(previousRevision)) {
         throw new Error('Someone could update this page, so couldn\'t delete.');
       }
@@ -849,5 +858,34 @@ module.exports = function(crowi, app) {
     });
   };
 
+  /**
+   * @api {post} /pages.unlink Remove the redirecting page
+   * @apiName UnlinkPage
+   * @apiGroup Page
+   *
+   * @apiParam {String} page_id Page Id.
+   * @apiParam {String} revision_id
+   */
+  api.unlink = function(req, res){
+    var pageId = req.body.page_id;
+
+    Page.findPageByIdAndGrantedUser(pageId, req.user)
+    .then(function(pageData) {
+      debug('Unlink page', pageData._id, pageData.path);
+
+      return Page.removeRedirectOriginPageByPath(pageData.path)
+        .then(() => pageData);
+    }).then(function(data) {
+      debug('Redirect Page deleted', data.path);
+      var result = {};
+      result.page = data;
+
+      return res.json(ApiResponse.success(result));
+    }).catch(function(err) {
+      debug('Error occured while get setting', err, err.stack);
+      return res.json(ApiResponse.error('Failed to delete redirect page.'));
+    });
+  };
+
   return actions;
 };

+ 1 - 1
lib/util/middlewares.js

@@ -62,7 +62,7 @@ exports.swigFilters = function(app, swig) {
   // define a function for Gravatar
   const generateGravatarSrc = function(user) {
     const hash = md5(user.email.trim().toLowerCase());
-    return `http://www.gravatar.com/avatar/${hash}`;
+    return `https://gravatar.com/avatar/${hash}`;
   };
 
   // define a function for uploaded picture

+ 1 - 1
lib/views/me/index.html

@@ -103,7 +103,7 @@
           <div class="radio">
             <h4>
               <input type="radio" name="imagetypeForm[isGravatarEnabled]" value="true" {% if user.isGravatarEnabled %}checked="checked"{% endif %}>
-              <img src="https://www.gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
+              <img src="https://gravatar.com/avatar/00000000000000000000000000000000?s=24" /> Gravatar
               <a href="https://gravatar.com/">
                 <small><i class="fa fa-external-link" aria-hidden="true"></i></small>
               </a>

+ 8 - 8
lib/views/modal/delete.html

@@ -9,13 +9,10 @@
           <h4 class="modal-title"><i class="fa fa-trash-o"></i> Delete Page</h4>
         </div>
         <div class="modal-body">
-          <ul>
-            <li>This page will be moved to the <a href="/trash/">trash</a>.</li>
-          </ul>
-            <div class="form-group">
-              <label for="">This page:</label><br>
-              <code>{{ page.path }}</code>
-            </div>
+          <div class="form-group">
+            <label for="">Deleting page:</label><br>
+            <code>{{ page.path }}</code>
+          </div>
         </div>
         <div class="modal-footer">
           <p><small class="pull-left" id="delete-errors"></small></p>
@@ -23,7 +20,10 @@
           <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() }}">
-          <input type="submit" class="btn btn-danger" value="Delete!">
+          <label class="checkbox-inline text-danger">
+            <input type="checkbox" name="completely">completely
+          </label>
+          <button type="submit" class="btn btn-danger delete-button">Delete</button>
         </div>
 
       </form>

+ 5 - 1
lib/views/modal/unportalize.html

@@ -32,7 +32,11 @@
             </div>
         </div>
         <div class="modal-footer">
-          <p><small class="pull-left" id="newPageNameCheck"></small></p>
+          <p class="pull-left text-left">
+            <small id="newPageNameCheck"></small>
+            <br>
+            <span id="linkToNewPage"></span>
+          </p>
           <input type="hidden" name="_csrf" value="{{ csrf() }}">
           <input type="hidden" name="path" value="{{ page.path }}">
           <input type="hidden" class="form-control" name="new_path" id="newPageName" value="{{ unportalizedPath }}">

+ 57 - 13
lib/views/page.html

@@ -82,20 +82,37 @@
   {% else %}
 
   {% if page.isDeleted() %}
-  <div class="alert alert-danger">
-    <form role="form" class="pull-right" id="revert-delete-page-form" onsubmit="return false;">
-      <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="submit" class="btn btn-danger btn-inverse btn-sm" value="Put Back!">
-    </form>
-    <p>
+  <div class="alert alert-danger alert-trash">
+    <div>
+      <ul class="list-inline pull-right">
+        <li>
+          <form role="form" id="revert-delete-page-form" onsubmit="return false;">
+            <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-success btn-sm">
+              <i class="fa fa-undo" aria-hidden="true"></i>
+              Put Back
+            </button>
+          </form>
+        </li>
+        <li>
+          <form role="form" id="delete-page-form" onsubmit="return false;">
+            <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="completely" value="true">
+            <button type="submit" class="btn btn-danger btn-sm">
+              <i class="fa fa-times-circle" aria-hidden="true"></i>
+              Delete Completely
+            </button>
+          </form>
+        </li>
+      </ul>{# /.pull-right #}
       <i class="fa fa-trash-o" aria-hidden="true"></i>
       This page is in the trash.<br>
-    </p>
-    <p>
-    Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm picture-rounded"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
-    </p>
+      Deleted by <img src="{{ page.lastUpdateUser|picture }}" class="picture picture-sm picture-rounded"> {{ page.lastUpdateUser.name }} at {{ page.updatedAt|datetz('Y-m-d H:i:s') }}
+    </div>
   </div>
   {% endif %}
 
@@ -134,10 +151,37 @@
 
   <div class="tab-content wiki-content">
   {% if req.query.renamed and not page.isDeleted() %}
+  <div class="alert alert-info alert-moved">
+    <span>
+      <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.query.renamed) }}
+    </span>
+  </div>
+  {% endif %}
+  {% if req.query.redirectFrom and not page.isDeleted() %}
+  <div class="alert alert-info alert-moved">
+    <div>
+      <form role="form" id="unlink-page-form" onsubmit="return false;">
+        <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-default btn-sm pull-right">
+          <i class="fa fa-unlink" aria-hidden="true"></i>
+          Unlink
+        </button>
+      </form>
+      <span>
+        <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.query.redirectFrom) }}
+      </span>
+    </div>
+  </div>
+  {% endif %}
+  {% if req.query.unlinked %}
   <div class="alert alert-info">
-    <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.query.renamed) }}
+    <strong>{{ t('Unlinked') }}: </strong> {{ t('page_page.notice.unlinked') }}
   </div>
   {% endif %}
+
+
   {% if not page.isLatestRevision() %}
   <div class="alert alert-warning">
     <strong>{{ t('Warning') }}: </strong> {{ t('page_page.notice.version') }} <i class="fa fa-magic"></i> <a href="{{ page.path }}">{{ t('Show latest') }}</a>

+ 2 - 0
locales/en-US/translation.json

@@ -4,6 +4,7 @@
   "Delete": "Delete",
   "Move": "Move",
   "Moved": "Moved",
+  "Unlinked": "Unlinked",
   "Like!": "Like!",
   "Seen by": "Seen by",
   "Cancel": "Cancel",
@@ -135,6 +136,7 @@
       "notice": {
           "version": "This is not the current version.",
           "moved": "This page was moved from <code>%s</code>",
+          "unlinked": "Redirect pages to this page have been deleted.",
           "restricted": "Access to this page is restricted"
       }
   },

+ 2 - 0
locales/ja/translation.json

@@ -4,6 +4,7 @@
   "Delete": "削除",
   "Move": "移動",
   "Moved": "移動しました",
+  "Unlinked": "リダイレクト削除",
   "Like!": "いいね!",
   "Seen by": "見た人",
   "Cancel": "キャンセル",
@@ -135,6 +136,7 @@
       "notice": {
           "version": "これは現在の版ではありません。",
           "moved": "このページは <code>%s</code> から移動しました。",
+          "unlinked": "このページへのリダイレクトは削除されました。",
           "restricted": "このページの閲覧は制限されています"
       }
   },

+ 5 - 0
resource/css/_delete.scss

@@ -0,0 +1,5 @@
+#deletePage {
+  .delete-button {
+    margin-left: .5em;
+  }
+}

+ 16 - 0
resource/css/_page.scss

@@ -114,6 +114,22 @@
       .tab-content {
         margin-top: 30px;
       }
+
+      // alert component settings for trash page and moved page
+      // see: https://jsfiddle.net/me420sky/2/
+      .alert-trash, .alert-moved {
+        padding: 10px 15px;
+
+        span {
+          line-height: 25px;
+        }
+
+        >div:after {
+          clear: both;
+          content: '';
+          display: table;
+        }
+      }
     }
   } // }}}
 

+ 2 - 2
resource/css/_search.scss

@@ -80,8 +80,8 @@
     nav {
       &.affix {
         top: 8px;
-        width: 33.33333%;
-        padding-right: 30px;
+        width: 32%;
+        padding-right: 0;
         padding-bottom: 50px;
         height: 100%;
         overflow-y: scroll;

+ 1 - 0
resource/css/crowi.scss

@@ -14,6 +14,7 @@ $bootstrap-sass-asset-helper: true;
 // crowi component
 @import 'admin';
 @import 'comment';
+@import 'delete';
 @import 'form';
 @import 'layout';
 @import 'page_list';

+ 4 - 3
resource/js/components/Page/RevisionUrl.js

@@ -15,11 +15,12 @@ export default class RevisionUrl extends React.Component {
       fontSize: "1em"
     }
 
-    const text = this.props.pagePath + '\n' + this.props.url;
+    const urlText = decodeURIComponent(this.props.url);
+    const copiedText = this.props.pagePath + '\n' + this.props.url;
     return (
       <span>
-        {this.props.url}
-        <CopyButton buttonId="btnCopyRevisionUrl" text={text}
+        {urlText}
+        <CopyButton buttonId="btnCopyRevisionUrl" text={copiedText}
             buttonClassName="btn btn-default" buttonStyle={buttonStyle} iconClassName="fa fa-link text-muted" />
       </span>
     );

+ 1 - 1
resource/js/components/User/UserPicture.js

@@ -20,7 +20,7 @@ export default class UserPicture extends React.Component {
 
   generateGravatarSrc(user) {
     const hash = md5(user.email.trim().toLowerCase());
-    return `https://www.gravatar.com/avatar/${hash}`;
+    return `https://gravatar.com/avatar/${hash}`;
   }
 
   getClassName() {

+ 28 - 0
resource/js/legacy/crowi.js

@@ -231,6 +231,12 @@ $(function() {
     $('#newPageName').focus();
   });
   $('#renamePageForm, #unportalize-form').submit(function(e) {
+    // create name-value map
+    let nameValueMap = {};
+    $(this).serializeArray().forEach((obj) => {
+      nameValueMap[obj.name] = obj.value;
+    })
+
     $.ajax({
       type: 'POST',
       url: '/_api/pages.rename',
@@ -238,8 +244,12 @@ $(function() {
       dataType: 'json'
     }).done(function(res) {
       if (!res.ok) {
+        // if already exists
         $('#newPageNameCheck').html('<i class="fa fa-times-circle"></i> ' + res.error);
         $('#newPageNameCheck').addClass('alert-danger');
+        $('#linkToNewPage').html(`
+          <i class="fa fa-fw fa-arrow-right"></i><a href="${nameValueMap.new_path}">${nameValueMap.new_path}</a>
+        `);
       } else {
         var page = res.page;
 
@@ -294,6 +304,24 @@ $(function() {
 
     return false;
   });
+  $('#unlink-page-form').submit(function(e) {
+    $.ajax({
+      type: 'POST',
+      url: '/_api/pages.unlink',
+      data: $('#unlink-page-form').serialize(),
+      dataType: 'json'
+    }).done(function(res) {
+      if (!res.ok) {
+        $('#delete-errors').html('<i class="fa fa-times-circle"></i> ' + res.error);
+        $('#delete-errors').addClass('alert-danger');
+      } else {
+        var page = res.page;
+        top.location.href = page.path + '?unlinked=true';
+      }
+    });
+
+    return false;
+  });
 
   $('#create-portal-button').on('click', function(e) {
     $('.portal').removeClass('hide');