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

Merge branch 'master' into auth

Yuki Takei 6 лет назад
Родитель
Сommit
4fed73c0ea

+ 15 - 11
resource/locales/en-US/translation.json

@@ -195,7 +195,7 @@
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
 
-  "Security Settings": "Security Settings",
+  "security_settings": "Security Settings",
 
   "API Settings": "API Settings",
   "API Token Settings": "API Token Settings",
@@ -270,7 +270,8 @@
   "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. "
+    "outdated": "Page is updated someone and now outdated.",
+    "user_not_admin": "Only admin user can delete completely"
   },
 
   "modal_rename": {
@@ -291,15 +292,13 @@
   "Delete Completely": "Delete Completely",
 
   "modal_delete": {
-    "label": {
-      "Delete Page": "Delete Page",
-      "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"
-    }
+    "delete_page": "Delete Page",
+    "deleting_page": "Deleting Page",
+    "delete_recursively": "Delete child pages under %s recursively.",
+    "delete_completely": "Delete Completely",
+    "delete_completely_restriction": "You have no admin to delete completely.",
+    "recursively": "Delete children of under <code>%s</code> recursively.",
+    "completely": "Delete completely instead of putting it into trash."
   },
 
   "modal_duplicate": {
@@ -459,6 +458,11 @@
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
     "page_listing_2": "Page listing/searching<br>restricted by User Group",
     "page_listing_2_desc": "Show pages that are restricted by User Group when listing/searching",
+    "complete_deletion": "Restrict Complete Deletion of Pages",
+    "complete_deletion_explain": "Restricts users who can completely delete pages.",
+    "admin_only": "Admin Only",
+    "admin_and_author": "Admin and Author",
+    "anyone": "Anyone",
 
 		"Authentication mechanism settings": "Authentication mechanism settings",
     "note": "Note",

+ 15 - 12
resource/locales/ja/translation.json

@@ -195,7 +195,7 @@
   "Re-enter new password": "(確認用)",
   "Password is not set": "パスワードが設定されていません",
 
-  "Security Settings": "セキュリティ設定",
+  "security_settings": "セキュリティ設定",
 
   "API Settings": "API設定",
   "API Token Settings": "API Token設定",
@@ -270,7 +270,8 @@
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "already_exists": "新しいページが既に存在しています。",
-    "outdated": "ページが他のユーザーによって更新されました。"
+    "outdated": "ページが他のユーザーによって更新されました。",
+    "user_not_admin": "権限のあるユーザーのみが完全削除できます"
   },
 
   "modal_rename": {
@@ -291,15 +292,13 @@
   "Delete Completely": "完全削除",
 
   "modal_delete": {
-    "label": {
-      "Delete Page": "ページを削除する",
-      "Delete recursively": "全ての子ページも削除",
-      "Delete completely": "完全削除"
-    },
-    "help": {
-      "recursively": "<code>%s</code> 配下のページも削除します",
-      "completely": "ゴミ箱を経由せず、完全に削除します"
-    }
+    "delete_page": "ページを削除する",
+    "deleting_page": "ページパス",
+    "delete_recursively": "全ての子ページも削除",
+    "delete_completely": "完全削除",
+    "delete_completely_restriction": "完全削除をするための権限がありません。",
+    "recursively": "<code>%s</code> 配下のページも削除します",
+    "completely": "ゴミ箱を経由せず、完全に削除します"
   },
 
   "modal_duplicate": {
@@ -441,7 +440,6 @@
 
   "security_setting": {
     "Basic authentication": "Basic認証",
-    "Security settings": "セキュリティ設定",
     "Guest users access": "ゲストユーザーのアクセス",
     "Register limitation": "登録の制限",
     "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -459,6 +457,11 @@
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2_desc": "ページのリスト表示や検索結果において、特定グループにのみ閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
+    "complete_deletion": "完全削除制限機能",
+    "complete_deletion_explain": "完全削除できるユーザーを制限します。",
+    "admin_only": "管理者のみ可能",
+    "admin_and_author": "管理者とページ作者が可能",
+    "anyone": "誰でも可能",
 
     "Authentication mechanism settings":"認証機構設定",
     "note": "メモ",

+ 28 - 16
src/client/styles/agile-admin/inverse/colors/antarctic.scss

@@ -16,20 +16,23 @@ $active-nav-tabs-bgcolor: $themecolor;
 $logo-mark-fill: $themelight;
 $wikilinktext: lighten($themecolor, 5%);
 $wikilinktext-hover: lighten($wikilinktext, 15%);
-$inline-code-color: darken($subthemecolor, 5%);
-$inline-code-bg: lighten($subthemecolor, 70%);
+$inline-code-color: #c7254e;
+$inline-code-bg: #f9f2f4;
 $border: $subthemecolor;
 $border-original: $subthemecolor;
 $navbar-border: $themecolor;
-$background-color: rgba($color: $themelight, $alpha: 0.8);
-$info:$subthemecolor;
+$background-color: rgba(
+  $color: $themelight,
+  $alpha: 0.8,
+);
+$info: $subthemecolor;
 
 @import 'apply-colors';
 @import 'apply-colors-light';
 
 // change color of highlighted header in wiki (default: orange)
 .code-line,
-ul>.text-muted {
+ul > .text-muted {
   color: $subthemecolor;
 }
 
@@ -50,7 +53,7 @@ ul>.text-muted {
 }
 
 // add background-image
-.main-container>#wrapper>#page-wrapper,
+.main-container > #wrapper > #page-wrapper,
 .page-editor-preview-container {
   background-image: url('/images/themes/antarctic/bg.svg');
   background-attachment: fixed;
@@ -99,7 +102,7 @@ header.affix {
   }
 }
 
-#wrapper>.navbar>.navbar-header {
+#wrapper > .navbar > .navbar-header {
   border-bottom: 4px solid $accentcolor;
 }
 
@@ -111,7 +114,7 @@ header.affix {
   .page-comment-main {
     box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
 
-    .page-comment-creator>a {
+    .page-comment-creator > a {
       border-bottom: 1px double $subthemecolor;
     }
   }
@@ -131,7 +134,7 @@ header.affix {
     }
 
     .nav.nav-tabs {
-      >li.active>a {
+      > li.active > a {
         background: $themecolor;
         border-bottom: solid 1px $themecolor;
         border-bottom-color: $themecolor;
@@ -144,21 +147,23 @@ header.affix {
  * Tabs
  */
 
-.nav.nav-tabs>li.active>a {
+.nav.nav-tabs > li.active > a {
   color: $themelight;
 }
 
 .text-info,
 body:not(.on-edit) .nav.nav-tabs {
-  >li>a {
+  > li > a {
     color: $subthemecolor;
   }
 
-  >li.active>a {
+  > li.active > a {
     color: $themelight;
-    background: linear-gradient(rgba($active-nav-tabs-bgcolor, 0) 50%,
+    background: linear-gradient(
+      rgba($active-nav-tabs-bgcolor, 0) 50%,
       rgba($active-nav-tabs-bgcolor, 0) 90%,
-      $active-nav-tabs-bgcolor 100%); // overwrite only the bottom pixel
+      $active-nav-tabs-bgcolor 100%
+    ); // overwrite only the bottom pixel
     background-color: $themecolor;
   }
 }
@@ -199,7 +204,6 @@ body:not(.on-edit) .nav.nav-tabs {
       ul {
         padding-left: 5px;
       }
-
     }
   }
 }
@@ -208,7 +212,7 @@ body:not(.on-edit) .nav.nav-tabs {
  *  Login page
  */
 
-.login-page>#wrapper>#page-wrapper {
+.login-page > #wrapper > #page-wrapper {
   background-image: url('/images/themes/antarctic/topimage.svg');
   background-attachment: fixed;
   background-position: center center;
@@ -222,3 +226,11 @@ body:not(.on-edit) .nav.nav-tabs {
     }
   }
 }
+
+/*
+ *  for Hightlight-js
+ */
+
+.hljs-ln {
+  background-color: transparent;
+}

+ 49 - 0
src/migrations/20190624110950-fill-last-update-user.js

@@ -0,0 +1,49 @@
+require('module-alias/register');
+const logger = require('@alias/logger')('growi:migrate:abolish-page-group-relation');
+
+const mongoose = require('mongoose');
+const config = require('@root/config/migrate');
+
+/**
+ * FIX https://github.com/weseek/growi/issues/1067
+ */
+module.exports = {
+
+  async up(db) {
+    logger.info('Apply migration');
+    mongoose.connect(config.mongoUri, config.mongodb.options);
+
+    const Page = require('@server/models/page')();
+
+    // see https://stackoverflow.com/questions/3974985/update-mongodb-field-using-value-of-another-field/37280419#37280419
+
+    // retrieve target data
+    const pages = await Page.find({
+      $or: [
+        { lastUpdateUser: { $exists: false } },
+        { lastUpdateUser: { $eq: null } },
+      ],
+    }).select('_id creator');
+
+    // create requests for bulkWrite
+    const requests = pages.map((page) => {
+      return {
+        updateOne: {
+          filter: { _id: page._id },
+          update: { $set: { lastUpdateUser: page.creator } },
+        },
+      };
+    });
+
+    if (requests.length > 0) {
+      await db.collection('pages').bulkWrite(requests);
+    }
+
+    logger.info('Migration has successfully applied');
+  },
+
+  down(db) {
+    // do not rollback
+  },
+
+};

+ 1 - 0
src/server/form/admin/customfeatures.js

@@ -4,6 +4,7 @@ const field = form.field;
 
 module.exports = form(
   field('settingForm[customize:isEnabledTimeline]').trim().toBooleanStrict(),
+  field('settingForm[customize:isEnabledDeleteCompletely]').trim().toBooleanStrict(),
   field('settingForm[customize:isSavedStatesOfTabChanges]').trim().toBooleanStrict(),
   field('settingForm[customize:isEnabledAttachTitleHeader]').trim().toBooleanStrict(),
   field('settingForm[customize:showRecentCreatedNumber]').trim().toInt(),

+ 1 - 0
src/server/form/admin/securityGeneral.js

@@ -12,4 +12,5 @@ module.exports = form(
   field('settingForm[security:registrationWhiteList]').custom(normalizeCRLF).custom(stringToArray),
   field('settingForm[security:list-policy:hideRestrictedByOwner]').trim().toBooleanStrict(),
   field('settingForm[security:list-policy:hideRestrictedByGroup]').trim().toBooleanStrict(),
+  field('settingForm[security:pageCompleteDeletionAuthority]'),
 );

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

@@ -64,6 +64,7 @@ module.exports = function(crowi) {
 
       'security:list-policy:hideRestrictedByOwner' : false,
       'security:list-policy:hideRestrictedByGroup' : false,
+      'security:pageCompleteDeletionAuthority' : null,
 
       'security:isEnabledPassport' : false,
       'security:passport-ldap:isEnabled' : false,

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

@@ -468,12 +468,12 @@ module.exports = function(crowi) {
   };
 
   pageSchema.methods.applyScope = function(user, grant, grantUserGroupId) {
-    this.grant = grant;
-
     // reset
     this.grantedUsers = [];
     this.grantedGroup = null;
 
+    this.grant = grant || GRANT_PUBLIC;
+
     if (grant !== GRANT_PUBLIC && grant !== GRANT_USER_GROUP) {
       this.grantedUsers.push(user._id);
     }
@@ -934,7 +934,7 @@ module.exports = function(crowi) {
       .cursor();
   };
 
-  async function pushRevision(pageData, newRevision, user, grant, grantUserGroupId) {
+  async function pushRevision(pageData, newRevision, user) {
     await newRevision.save();
     debug('Successfully saved new revision', newRevision);
 
@@ -973,7 +973,7 @@ module.exports = function(crowi) {
     // sanitize path
     path = crowi.xss.process(path); // eslint-disable-line no-param-reassign
 
-    let grant = options.grant || GRANT_PUBLIC;
+    let grant = options.grant;
     // force public
     if (isPortalPath(path)) {
       grant = GRANT_PUBLIC;
@@ -997,7 +997,7 @@ module.exports = function(crowi) {
 
     let savedPage = await page.save();
     const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
-    const revision = await pushRevision(savedPage, newRevision, user, grant, grantUserGroupId);
+    const revision = await pushRevision(savedPage, newRevision, user);
     savedPage = await this.findByPath(revision.path)
       .populate('revision')
       .populate('creator');
@@ -1012,8 +1012,8 @@ module.exports = function(crowi) {
     validateCrowi();
 
     const Revision = crowi.model('Revision');
-    const grant = options.grant || null;
-    const grantUserGroupId = options.grantUserGroupId || null;
+    const grant = options.grant || pageData.grant; //                                  use the previous data if absence
+    const grantUserGroupId = options.grantUserGroupId || pageData.grantUserGroupId; // use the previous data if absence
     const isSyncRevisionToHackmd = options.isSyncRevisionToHackmd;
     const socketClientId = options.socketClientId || null;
 
@@ -1023,7 +1023,7 @@ module.exports = function(crowi) {
     // update existing page
     let savedPage = await pageData.save();
     const newRevision = await Revision.prepareRevision(pageData, body, previousBody, user);
-    const revision = await pushRevision(savedPage, newRevision, user, grant, grantUserGroupId);
+    const revision = await pushRevision(savedPage, newRevision, user);
     savedPage = await this.findByPath(revision.path)
       .populate('revision')
       .populate('creator');

+ 12 - 0
src/server/models/user.js

@@ -199,6 +199,18 @@ module.exports = function(crowi) {
     });
   };
 
+  userSchema.methods.canDeleteCompletely = function(creatorId) {
+    const pageCompleteDeletionAuthority = crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority');
+    if (pageCompleteDeletionAuthority == null || this.admin) {
+      return true;
+    }
+    if (pageCompleteDeletionAuthority === 'adminAndAuthor') {
+      return (this._id.equals(creatorId));
+    }
+
+    return false;
+  };
+
   userSchema.methods.updateApiToken = function(callback) {
     const self = this;
 

+ 11 - 5
src/server/routes/page.js

@@ -567,9 +567,12 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error('Page exists', 'already_exists'));
     }
 
-    const options = {
-      grant, grantUserGroupId, overwriteScopesOfDescendants, socketClientId, pageTags,
-    };
+    const options = { socketClientId };
+    if (grant != null) {
+      options.grant = grant;
+      options.grantUserGroupId = grantUserGroupId;
+    }
+
     const createdPage = await Page.create(pagePath, body, req.user, options);
 
     let savedTags;
@@ -648,8 +651,6 @@ module.exports = function(crowi, app) {
     const options = { isSyncRevisionToHackmd, socketClientId };
     if (grant != null) {
       options.grant = grant;
-    }
-    if (grantUserGroupId != null) {
       options.grantUserGroupId = grantUserGroupId;
     }
 
@@ -950,6 +951,9 @@ module.exports = function(crowi, app) {
 
     try {
       if (isCompletely) {
+        if (!req.user.canDeleteCompletely(page.creator)) {
+          return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
+        }
         if (isRecursively) {
           page = await Page.completelyDeletePageRecursively(page, req.user, options);
         }
@@ -1116,6 +1120,8 @@ module.exports = function(crowi, app) {
     req.body.path = newPagePath;
     req.body.body = page.revision.body;
     req.body.grant = page.grant;
+    req.body.grantedUsers = page.grantedUsers;
+    req.body.grantedGroup = page.grantedGroup;
     req.body.pageTags = originTags;
 
     return api.create(req, res);

+ 20 - 3
src/server/views/admin/security.html

@@ -1,11 +1,11 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customTitle(t('Security settings')) }} · {% endblock %}
+{% block html_title %}{{ customTitle(t('security_settings')) }} · {% endblock %}
 
 {% block content_header %}
 <div class="header-wrap">
   <header id="page-header">
-    <h1 id="admin-title" class="title">{{ t('Security settings') }}</h1>
+    <h1 id="admin-title" class="title">{{ t('security_settings') }}</h1>
   </header>
 </div>
 {% endblock %}
@@ -38,7 +38,7 @@
 
       <form action="/_api/admin/security/general" method="post" class="form-horizontal" id="generalSetting" role="form">
         <fieldset>
-        <legend class="alert-anchor">{{ t('security_setting.Security settings') }}</legend>
+        <legend class="alert-anchor">{{ t('security_settings') }}</legend>
 
           <div class="form-group">
             <label for="settingForm[security:registrationMode]" class="col-xs-3 control-label">{{ t('Basic authentication') }}</label>
@@ -136,6 +136,23 @@
             </div>
           </div>
 
+          <div class="form-group">
+            {% set configName = 'settingForm[security:pageCompleteDeletionAuthority]' %}
+            {% set configValue = getConfig('crowi','security:pageCompleteDeletionAuthority') %}
+            <label for="{{configName}}" class="col-xs-3 control-label">{{ t('security_setting.complete_deletion') }}</label>
+            <div class="col-xs-6">
+              <select class="form-control selectpicker" name="settingForm[security:pageCompleteDeletionAuthority]" value="{{ configValue }}">
+                <option value="adminOnly" {% if configValue =="adiminOnly" %}selected{% endif %}>{{ t('security_setting.admin_only') }}</option>
+                <option value="adminAndAuthor" {% if configValue == "adminAndAuthor" %}selected{% endif %}>{{ t('security_setting.admin_and_author') }}</option>
+                <option value=null {% if configValue == null  %}selected{% endif %}>{{ t('security_setting.anyone') }}</option>
+              </select>
+
+              <p class="help-block small">
+                {{ t('security_setting.complete_deletion_explain') }}
+              </p>
+            </div>
+          </div>
+
           <div class="form-group">
             <div class="col-xs-offset-3 col-xs-6">
               <input type="hidden" name="_csrf" value="{{ csrf() }}">

+ 1 - 1
src/server/views/admin/widget/menu.html

@@ -4,7 +4,7 @@
 <ul class="nav nav-pills nav-stacked">
   <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="icon-fw icon-home"></i> {{ t('Management Wiki Home') }}</a></li>
   <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="icon-fw icon-settings"></i> {{ t('App Settings') }}</a></li>
-  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('Security Settings') }}</a></li>
+  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('security_settings') }}</a></li>
   <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown Settings') }}</a></li>
   <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
   <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-download"></i> {{ t('Import Data') }}</a></li>

+ 13 - 10
src/server/views/modal/delete.html

@@ -8,15 +8,15 @@
           <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.Delete completely') }}
+            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.delete_completely') }}
             {% else %}
-            <i class="icon-fw icon-trash"></i> {{ t('modal_delete.label.Delete Page') }}
+            <i class="icon-fw icon-trash"></i> {{ t('modal_delete.delete_page') }}
             {% endif %}
           </div>
         </div>
         <div class="modal-body">
           <div class="form-group">
-            <label for="">Deleting page:</label><br>
+            <label for="">{{ t('modal_delete.deleting_page') }}:</label><br>
             <code>{{ page.path }}</code>
           </div>
 
@@ -25,17 +25,20 @@
           {% if page.grant != 2 %}
           <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) }}
+            <label for="cbDeleteRecursively">{{ t('modal_delete.delete_recursively') }}</label>
+            <p class="help-block"> {{ t('modal_delete.recursively', page.path) }}
             </p>
           </div>
           {% endif %}
           {% 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>
+          <input name="completely" id="cbDeleteCompletely" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} value="1"  type="checkbox">
+            <label for="cbDeleteCompletely" class="text-danger">{{ t('modal_delete.delete_completely') }}</label>
+            {% if !user.canDeleteCompletely(page.creator._id) %}
+              <p class="bg-danger text-white p-2 mt-2"> <i class="icon-ban" ></i>{{ t('modal_delete.delete_completely_restriction') }}</p>
+            {% else %}
+            <p class="help-block"> {{ t('modal_delete.completely') }}</p>
+            {% endif %}
           </div>
           {% endif %}
         </div>
@@ -51,7 +54,7 @@
                 <input type="hidden" name="completely" value="true">
                 <button type="submit" class="m-l-10 btn btn-sm btn-danger delete-button">
                   <i class="icon-fire" aria-hidden="true"></i>
-                  {{ t('Delete Completely') }}
+                  {{ t('delete_completely') }}
                 </button>
               {% else %}
                 <button type="submit" class="m-l-10 btn btn-sm btn-default delete-button">

+ 2 - 2
src/server/views/widget/alert_breaking_changes.html

@@ -3,13 +3,13 @@
 {% if getConfig('crowi', 'security:isEnabledPassport') !== true %}
 <div class="myadmin-alert alert alert-warning mb-0">
   <i class="icon-exclamation"></i>
-  {{ t("breaking_changes.v346_passport_is_not_enabled", '<a href="/admin/security">' + t('Security settings') + '<i class="icon-login"></i></a>') }}
+  {{ t("breaking_changes.v346_passport_is_not_enabled", '<a href="/admin/security">' + t('security_settings') + '<i class="icon-login"></i></a>') }}
 </div>
 {% endif %}
 
 {% if getConfig('crowi', 'security:basicName') || getConfig('crowi', 'security:basicSecret') %}
 <div class="myadmin-alert alert alert-warning mb-0">
   <i class="icon-exclamation"></i>
-  {{ t("breaking_changes.v346_using_basic_auth", '<a href="/admin/security">' + t('Security settings') + '<i class="icon-login"></i></a>') }}
+  {{ t("breaking_changes.v346_using_basic_auth", '<a href="/admin/security">' + t('security_settings') + '<i class="icon-login"></i></a>') }}
 </div>
 {% endif %}

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

@@ -2,6 +2,9 @@
   <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-user_not_admin">
+    <strong><i class="icon-fw icon-ban"></i>{{ t('page_api_error.user_not_admin') }}</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>

+ 2 - 2
src/server/views/widget/page_alerts.html

@@ -25,10 +25,10 @@
       {% 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>
+          <button 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') }}</button>
         </li>
         <li>
-          <a href="#" class="btn btn-danger btn-rounded btn-sm" data-target="#deletePage" data-toggle="modal"><i class="icon-fire" aria-hidden="true"></i> {{ t('Delete Completely') }}</a>
+            <button href="#" class="btn btn-danger btn-rounded btn-sm" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} data-target="#deletePage" data-toggle="modal"><i class="icon-fire" aria-hidden="true"></i> {{ t('Delete Completely') }}</button>
         </li>
       </ul>{# /.pull-right #}
       {% endif %}