فهرست منبع

Merge branch 'dev/4.0.x' into support/refactor-addConditionToListByStartWith

itizawa 5 سال پیش
والد
کامیت
bdd93da899
73فایلهای تغییر یافته به همراه434 افزوده شده و 718 حذف شده
  1. 7 0
      .github/workflows/build.yml
  2. 14 1
      CHANGES.md
  3. 1 1
      bin/download-cdn-resources.js
  4. 0 1
      package.json
  5. 66 64
      resource/locales/en-US/admin/admin.json
  6. 82 73
      resource/locales/en-US/translation.json
  7. 2 2
      resource/locales/en-US/welcome.md
  8. 4 2
      resource/locales/ja/admin/admin.json
  9. 14 5
      resource/locales/ja/translation.json
  10. 14 8
      src/client/js/admin.jsx
  11. 9 4
      src/client/js/app.jsx
  12. 2 2
      src/client/js/components/Admin/App/AwsSetting.jsx
  13. 2 2
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  14. 1 1
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  15. 1 1
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  16. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  17. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  18. 2 2
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  19. 3 3
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  20. 1 1
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  21. 2 2
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  22. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  23. 56 0
      src/client/js/components/ErrorBoudary.jsx
  24. 1 1
      src/client/js/components/Me/AssociateModal.jsx
  25. 2 2
      src/client/js/components/Me/ExternalAccountLinkedMe.jsx
  26. 4 1
      src/client/js/components/Me/PersonalSettings.jsx
  27. 1 1
      src/client/js/components/MyDraftList/MyDraftList.jsx
  28. 3 3
      src/client/js/components/Page/CopyDropdown.jsx
  29. 1 1
      src/client/js/components/PageEditor/Editor.jsx
  30. 2 2
      src/client/js/components/PageList/Page.jsx
  31. 3 3
      src/client/js/components/PageList/PagePathLabel.jsx
  32. 1 10
      src/client/js/components/PageTimeline.jsx
  33. 1 1
      src/client/js/components/SavePageControls/GrantSelector.jsx
  34. 12 4
      src/client/js/components/SearchPage/DeletePageListModal.jsx
  35. 11 6
      src/client/js/components/SearchPage/SearchResult.jsx
  36. 1 1
      src/client/js/components/SearchTypeahead.jsx
  37. 1 1
      src/client/js/components/StaffCredit/Contributor.js
  38. 6 6
      src/client/js/components/StaffCredit/StaffCredit.jsx
  39. 1 0
      src/client/js/legacy/crowi.js
  40. 2 1
      src/client/js/services/PageContainer.js
  41. 16 18
      src/client/styles/scss/_page_list.scss
  42. 11 11
      src/client/styles/scss/_staff_credit.scss
  43. 0 2
      src/server/crowi/express-init.js
  44. 1 1
      src/server/models/config.js
  45. 1 1
      src/server/models/page.js
  46. 1 1
      src/server/routes/attachment.js
  47. 16 15
      src/server/routes/page.js
  48. 1 1
      src/server/service/import.js
  49. 1 1
      src/server/service/search-delegator/elasticsearch.js
  50. 1 2
      src/server/util/swigFunctions.js
  51. 0 317
      src/server/views/admin/Users_reserve.html
  52. 2 2
      src/server/views/admin/index.html
  53. 1 2
      src/server/views/admin/markdown.html
  54. 0 3
      src/server/views/admin/search.html
  55. 0 7
      src/server/views/customlayout-selector/forbidden.html
  56. 0 7
      src/server/views/customlayout-selector/not_creatable.html
  57. 0 7
      src/server/views/customlayout-selector/not_found.html
  58. 0 7
      src/server/views/customlayout-selector/page.html
  59. 0 7
      src/server/views/customlayout-selector/page_list.html
  60. 0 7
      src/server/views/customlayout-selector/user_page.html
  61. 1 1
      src/server/views/modal/delete.html
  62. 1 1
      src/server/views/page_presentation.html
  63. 2 2
      src/server/views/widget/forbidden_content.html
  64. 2 2
      src/server/views/widget/not_creatable_content.html
  65. 13 4
      src/server/views/widget/not_found_content.html
  66. 6 5
      src/server/views/widget/page_alerts.html
  67. 2 2
      src/server/views/widget/page_content.html
  68. 8 5
      src/server/views/widget/page_list.html
  69. 2 2
      src/server/views/widget/page_list_and_timeline.html
  70. 2 2
      src/server/views/widget/page_list_and_timeline_kibela.html
  71. 0 32
      src/server/views/widget/pager.html
  72. 2 2
      src/test/models/page.test.js
  73. 0 15
      yarn.lock

+ 7 - 0
.github/workflows/build.yml

@@ -66,6 +66,13 @@ jobs:
         additional-tags: 'latest'
         publish: true
 
+    - name: Slack Notification
+      uses: weseek/ghaction-release-slack-notification@master
+      with:
+        channel: '#general'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+        created_tag: v${{ env.SEMVER }}
+
     - name: Check whether workspace is clean
       run: |
         STATUS=`git status --porcelain`

+ 14 - 1
CHANGES.md

@@ -2,12 +2,25 @@
 
 ## v4.0.0-RC
 
+### BREAKING CHANGES
+
+* Crowi Classic Behavior is removed
+* Crowi Classic Layout is removed
+* 'default-dark' theme is now merged as a dark mode variant of 'default' theme
+* 'blue-night' theme is now merged as a dark mode variant of 'mono-blue' theme
+
+### Updates
+
+* Feature: Switch Light/Dark Mode
+* Improvement: Migrate to Bootstrap 4
+* Improvement: Copy Page URL menu item to copy path dropdown
+* Improvement: Show contributors by Bootstrap Modal
 * Support: Upgrade libs
     * bootstrap
 
 ## v3.8.1-RC
 
-*
+* Fix: Unset overflow-y style for Edit Tags Modal
 
 ## v3.8.0
 

+ 1 - 1
bin/download-cdn-resources.js

@@ -27,7 +27,7 @@ const service = new CdnResourcesService();
 
 service.downloadAndWriteAll(downloader)
   .then(() => {
-    logger.info('Download is terminated successfully');
+    logger.info('Download is completed successfully');
   })
   .catch((err) => {
     logger.error(err);

+ 0 - 1
package.json

@@ -98,7 +98,6 @@
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
-    "express-sanitizer": "^1.0.4",
     "express-session": "^1.16.1",
     "express-validator": "^6.1.1",
     "express-webpack-assets": "^0.1.0",

+ 66 - 64
resource/locales/en-US/admin/admin.json

@@ -1,35 +1,35 @@
 {
   "admin_top": {
     "management_wiki": "Management Wiki",
-    "system_information": "System Information",
-    "wiki_administrator": "Only Wiki administrator can access this page",
-    "assign_administrator": "You can assign administrator from Assign administrator button in the User management page",
+    "system_information": "System information",
+    "wiki_administrator": "Only wiki administrator can access this page",
+    "assign_administrator": "You can assign the selected user to be a wiki administrator on the User Management page using the 'Give admin access' button",
     "list_of_installed_plugins": "List of installed plugins",
     "package_name": "Package name",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
     "list_of_env_vars":"List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
-    "about_security": "Check <a href='/admin/security'>Securtiy Management</a> for security environment variables."
+    "about_security": "Check <a href='/admin/security'>Securtiy Settings</a> for security environment variables."
   },
   "app_setting": {
     "site_name": "Site name",
-    "sitename_change": "You can change Site Name which is used for header and HTML title.",
+    "sitename_change": "You can change site name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "site_url_desc": "This is for the site URL setting.",
     "site_url_warn": "Some features don't work because the site URL is not set.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "confidential_name": "Confidential name",
     "confidential_example": "ex): internal use only",
-    "default_language": "Default Language for new users",
-    "file_uploading": "File Uploading",
-    "enable_files_except_image": "Enable file upload other than image files.",
+    "default_language": "Default language for new users",
+    "file_uploading": "File uploading",
+    "enable_files_except_image": "Enabling this option will allow upload of any file type. Without this option, only image file upload is supported.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "update": "Update",
     "mail_settings": "Mail settings",
     "smtp_used": "If you have SMTP settings, it will be used.",
     "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
-    "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
+    "neihter_of": "If neither is selected, then no email will be sent.",
     "from_e-mail_address": "From e-mail address",
     "smtp_settings": "SMTP settings",
     "host": "Host",
@@ -51,18 +51,18 @@
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used."
   },
   "markdown_setting": {
-    "lineBreak_header": "Line Break Setting",
+    "lineBreak_header": "Line break setting",
     "lineBreak_desc": "You can change line break settings.",
     "lineBreak_options": {
-      "enable_lineBreak": "Enable Line Break",
-      "enable_lineBreak_desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
-      "enable_lineBreak_for_comment": "Enable Line Break in comment",
-      "enable_lineBreak_for_comment_desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML"
+      "enable_lineBreak": "Enable line break",
+      "enable_lineBreak_desc": "Convert line break in the text page to<code>&lt;br&gt;</code>in HTML",
+      "enable_lineBreak_for_comment": "Enable line break in comment",
+      "enable_lineBreak_for_comment_desc": "Convert line break in comment to<code>&lt;br&gt;</code>in HTML"
     },
-    "presentation_header": "Presentation Setting",
+    "presentation_header": "Presentation setting",
     "presentation_desc": "You can change presentation settings.",
     "presentation_options": {
-      "page_break_setting": "Page break Setting",
+      "page_break_setting": "Page break setting",
       "preset_one_separator": "Preset 1",
       "preset_one_separator_desc": "3 Blank lines",
       "preset_one_separator_value": "\\n\\n\\n",
@@ -70,16 +70,16 @@
       "preset_two_separator_desc": "5 Hyphens",
       "preset_two_separator_value": "-----",
       "custom_separator": "Custom",
-      "custom_separator_desc": "Regular Expression"
+      "custom_separator_desc": "Regular expression"
     },
-    "xss_header": "Prevent XSS(Cross Site Scripting) Setting",
+    "xss_header": "Prevent XSS(cross site scripting) setting",
     "xss_desc": "You can change the handling of HTML tags in markdown text.",
     "xss_options": {
-      "enable_xss_prevention": "Enable XSS Prevention",
-      "ignore_all_tags": "Ignore All Tags",
-      "ignore_all_tags_desc": "Stripe all HTML tags and attributes",
-      "recommended_setting": "Recommended Setting",
-      "custom_whitelist": "Custom Whitelist",
+      "enable_xss_prevention": "Enable XSS prevention",
+      "remove_all_tags": "Remove all tags",
+      "remove_all_tags_desc": "Stripe all HTML tags and attributes",
+      "recommended_setting": "Recommended setting",
+      "custom_whitelist": "Custom whitelist",
       "tag_names": "Tag names",
       "tag_attributes": "Tag attributes",
       "import_recommended": "Import recommended {{target}}"
@@ -90,11 +90,11 @@
     "layout": "Layout",
     "theme": "Theme",
     "layout_desc": {
-      "growi_title": "Simple and Clear",
+      "growi_title": "Simple and clear",
       "growi_text1": "Full screen layout and thin margins/paddings",
       "growi_text2": "Show and post comments at the bottom of the page",
-      "growi_text3": "Affix Table-of-contents",
-      "kibela_title": "Easy Viewing Structure",
+      "growi_text3": "Affix table-of-contents",
+      "kibela_title": "Easy viewing structure",
       "kibela_text1": "Center aligned contents",
       "kibela_text2": "Show and post comments at the bottom of the page",
       "kibela_text3": "Affix Table-of-contents"
@@ -103,30 +103,30 @@
     "function_desc": "You can choose Valid/Invalid of the function",
     "function_options": {
       "timeline": "Timeline function",
-      "timeline_desc1": "You can show the timeline of the subpages.",
-      "timeline_desc2": "If there are many subpages, performance decreases while page loading.",
+      "timeline_desc1": "You can show the timeline of the child pages.",
+      "timeline_desc2": "If there are many child pages, performance decreases while page loading.",
       "timeline_desc3": "You can speed up list page display by invalidating.",
       "tab_switch": "Save tab-switching in the browser",
       "tab_switch_desc1": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
       "tab_switch_desc2": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
       "attach_title_header": "Add h1 section when create new page automatically",
       "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
-      "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
+      "recent_created__n_draft_num_desc": "Number of recently created pages & drafts displayed",
       "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
-      "stale_notification": "Display Notification on Stale Pages",
+      "stale_notification": "Display notification on stale pages",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
     },
-    "code_highlight": "Code Highlight",
+    "code_highlight": "Code highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
-    "custom_title": "Custom Title",
+    "custom_title": "Custom title",
     "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag.<br><code>&#123;&#123;sitename&#125;&#125;</code> will be automatically replaced with the app name, and <code>&#123;&#123;page&#125;&#125;</code> will be replaced with the page name/path.",
-    "custom_header": "Custom HTML Header",
+    "custom_header": "Custom HTML header",
     "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",
     "write_css": "You can write CSS that is applied to whole system.",
-    "ctrl_space": "Ctrl+Space to Autocomplete",
+    "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change."
@@ -134,7 +134,7 @@
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
-    "import_growi_archive": "Import GROWI Archive",
+    "import_growi_archive": "Import GROWI archive",
     "growi_settings": {
       "description_of_import_mode": {
         "about": "When you import data with the same name as an existing one, choose from the following three modes below.",
@@ -147,7 +147,7 @@
       "extracted_file": "Extracted File",
       "collection": "Collection",
       "upload": "Upload",
-      "discard": "Discard Uploaded Data",
+      "discard": "Discard uploaded data",
       "errors": {
         "at_least_one": "Select one or more collections.",
         "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
@@ -192,7 +192,7 @@
     },
     "import": "Import",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
-    "Directory_hierarchy_tag": "Directory Hierarchy Tag"
+    "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
   "export_management": {
     "exporting_collection_list": "Exporting Collection List",
@@ -213,11 +213,11 @@
     "delete": "Delete"
   },
   "user_management": {
-    "invite_users": "Invite New Users",
+    "invite_users": "Invite new users",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "invite_modal": {
       "emails": "Emails",
-      "invite_thru_email": "Send Invitation Email",
+      "invite_thru_email": "Send invitation email",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
@@ -226,15 +226,15 @@
     },
     "user_table": {
       "administrator": "Administrator",
-      "edit_menu": "Edit Menu",
-      "reset_password": "Reset Password",
+      "edit_menu": "Edit menu",
+      "reset_password": "Reset password",
       "administrator_menu": "Administrator Menu",
       "accept": "Accept",
-      "deactivate_account": "Deactivate Account",
+      "deactivate_account": "Deactivate account",
       "your_own": "You cannot deactivate your own account",
-      "remove_admin_access": "Remove Admin Access",
+      "remove_admin_access": "Remove admin access",
       "cannot_remove": "You cannot remove yourself from administrator",
-      "give_admin_access": "Give Admin Access"
+      "give_admin_access": "Give admin access"
     },
     "reset_password": "Reset Password",
     "reset_password_modal": {
@@ -245,13 +245,15 @@
       "new_password": "New Password"
     },
     "external_account": "External Account Management",
+    "external_accounts":"External accounts",
+    "create_external_account":"Create external account",
     "external_account_list": "External Account List",
     "invite": "Invite",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
-    "authentication_provider": "Authentication Provider",
+    "authentication_provider": "Authentication provider",
     "manage": "Manage",
-    "password_setting": "Password Setting",
+    "password_setting": "Password setting",
     "password_setting_help": "Is password set?",
     "set": "Yes",
     "unset": "No",
@@ -260,34 +262,34 @@
     "current_users": "Current users:"
   },
   "user_group_management": {
-    "create_group": "Create New Group",
-    "deny_create_group": "You can't create a new group with the current settings",
-    "group_name": "Group Name",
+    "create_group": "Create new group",
+    "deny_create_group": "You can't create a new group with the current settings.",
+    "group_name": "Group name",
     "group_example": "e.g. : Group1",
     "add_modal": {
-      "add_user": "Add a User to the Created Group",
-      "search_option": "Search Option",
+      "add_user": "Add a user to the created group",
+      "search_option": "Search option",
       "enable_option": "Enable {{option}}",
-      "forward_match": "forword match",
-      "partial_match": "partial match",
-      "backward_match": "backward match"
+      "forward_match": "Forword match",
+      "partial_match": "Partial match",
+      "backward_match": "Backward match"
     },
-    "group_list": "Group List",
-    "back_to_list": "Go Back to Group List",
-    "basic_info": "Basic Info",
-    "user_list": "User List",
+    "group_list": "Group list",
+    "back_to_list": "Go back to group list",
+    "basic_info": "Basic info",
+    "user_list": "User list",
     "created_group": "Group was created",
-    "is_loading_data": "fetch data...",
-    "no_pages": "There are no pages the group has view permission",
+    "is_loading_data": "Fetch data...",
+    "no_pages": "There are no pages the group has view permission.",
     "remove_from_group": "Remove this user",
     "delete_modal": {
-      "header": "Delete Group",
-      "desc": "Once deleted, the deleted group and its private pages cannot be retrieved",
+      "header": "Delete group",
+      "desc": "Once deleted, the deleted group and its private pages cannot be retrieved.",
       "dropdown_desc": "Choose an action for private pages",
       "select_group": "Select a group",
       "no_groups": "No groups to select",
-      "publish_pages": "Publish All",
-      "delete_pages": "Delete All",
+      "publish_pages": "Publish all",
+      "delete_pages": "Delete all",
       "transfer_pages": "Transfer to another group"
     }
   }

+ 82 - 73
resource/locales/en-US/translation.json

@@ -2,7 +2,7 @@
   "Help": "Help",
   "Edit": "Edit",
   "Delete": "Delete",
-  "Delete All": "Delete All",
+  "delete_all": "Delete all",
   "Duplicate": "Duplicate",
   "Copy": "Copy",
   "Click to copy": "Click to copy",
@@ -25,7 +25,7 @@
   "Undo": "Undo",
   "Article": "Article",
   "Page": "Page",
-  "Page Path": "Page Path",
+  "Page Path": "Page path",
   "Category": "Category",
   "User": "User",
   "status": "Status",
@@ -50,11 +50,11 @@
   "username": "Username",
   "Created": "Created",
   "Last updated": "Updated",
-  "Last_Login": "Last Login",
+  "Last_Login": "Last login",
   "Share": "Share",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
-  "Create/Edit Template": "Create/Edit Template Page",
+  "Create/Edit Template": "Create/Edit template page",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "No diff": "No diff",
@@ -62,16 +62,16 @@
   "User ID": "User ID",
   "User's Home": "User's Home",
   "User Settings": "User Settings",
-  "User Information": "User Information",
-  "Basic Info": "Basic Info",
+  "User Information": "User information",
+  "Basic Info": "Basic info",
   "Name": "Name",
   "Email": "Email",
   "Language": "Language",
   "English": "English",
   "Japanese": "Japanese",
-  "Set Profile Image": "Set Profile Image",
-  "Upload Image": "Upload Image",
-  "Current Image": "Current Image",
+  "Set Profile Image": "Set profile image",
+  "Upload Image": "Upload image",
+  "Current Image": "Current image",
   "Delete Image": "Delete Image",
   "Delete this image?": "Delete this image?",
   "Updated": "Updated",
@@ -86,10 +86,10 @@
   "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
-  "New Page": "New Page",
+  "New Page": "New page",
   "Create under": "Create page under below:",
   "Table of Contents": "Table of Contents",
-  "Management Wiki Home": "Management Wiki Home",
+  "Wiki Management Home Page": "Wiki Management Home Page",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
   "Markdown Settings": "Markdown Settings",
@@ -108,7 +108,7 @@
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Specified users only": "Specified users only",
-  "Just me": "Just me",
+  "Only me": "Only me",
   "Only inside the group": "Only inside the group",
   "page_list_and_search_results": "Page list / Search results",
   "scope_of_page_disclosure": "Scope of page disclosure",
@@ -163,29 +163,29 @@
   },
   "page_me_apitoken": {
     "notice": {
-      "apitoken_issued": "API Token is not issued.",
-      "update_token1": "You can update to generate a new API Token.",
-      "update_token2": "You will need to update the API Token in any existing processes."
+      "apitoken_issued": "API token is not issued.",
+      "update_token1": "You can update to generate a new API token.",
+      "update_token2": "You will need to update the API token in any existing processes."
     },
     "form_help": {}
   },
   "Password": "Password",
-  "Password Settings": "Password Settings",
+  "Password Settings": "Password settings",
     "personal_settings": {
     "disassociate_external_account": "Disassociate External Account",
     "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
     "set_new_password": "Set new Password",
-    "update_password": "Update Password",
+    "update_password": "Update password",
       "current_password": "Current password",
       "new_password": "New password",
       "new_password_confirm": "Re-enter new password",
       "password_is_not_set": "Password is not set"
     },
-  "security_settings": "Security Settings",
-  "API Settings": "API Settings",
-  "API Token Settings": "API Token Settings",
-  "Current API Token": "Current API Token",
-  "Update API Token": "Update API Token",
+  "security_settings": "Security settings",
+  "API Settings": "API settings",
+  "API Token Settings": "API token settings",
+  "Current API Token": "Current API token",
+  "Update API Token": "Update API token",
   "header_search_box": {
     "label": {
       "All pages": "All pages",
@@ -273,15 +273,15 @@
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
   },
-  "Put Back": "Put Back",
-  "Delete Completely": "Delete Completely",
+  "Put Back": "Put back",
+  "Delete Completely": "Delete completely",
   "modal_delete": {
-    "delete_page": "Delete Page",
-    "deleting_page": "Deleting Page",
+    "delete_page": "Delete page",
+    "deleting_page": "Deleting page",
     "delete_recursively": "Delete child pages recursively.",
-    "delete_completely": "Delete Completely",
+    "delete_completely": "Delete completely",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
-    "recursively": "Delete children of under <code>%s</code> recursively.",
+    "recursively": "Delete children of <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
   "modal_empty":{
@@ -296,11 +296,11 @@
   },
   "modal_putback": {
     "label": {
-      "Put Back Page": "Put Back Page",
-      "recursively": "Put Back recursively"
+      "Put Back Page": "Put back page",
+      "recursively": "Put back recursively"
     },
     "help": {
-      "recursively": "Put Back children of under <code>%s</code> recursively"
+      "recursively": "Put back children of under <code>%s</code> recursively"
     }
   },
   "modal_shortcuts": {
@@ -336,11 +336,11 @@
   },
   "template": {
     "modal_label": {
-      "Create/Edit Template Page": "Create/Edit Template Page",
+      "Create/Edit Template Page": "Create/Edit template page",
       "Create template under": "Create template page under:<br /><code>%s</code>"
     },
     "option_label": {
-      "create/edit": "Create/Edit Template page..",
+      "create/edit": "Create/Edit template page..",
       "select": "Select template page type"
     },
     "children": {
@@ -400,33 +400,42 @@
     "someone_editing": "Someone editing this page on HackMD",
     "this_page_has_draft": "This page has a draft on HackMD"
   },
+  "search_result": {
+    "result_meta": "Found \"{{keyword}}\" in {{total}}.",
+    "deletion_mode_btn_lavel": "Select and delete page",
+    "cancel": "Cancel",
+    "delete": "Delete",
+    "check_all": "Check all",
+    "deletion_modal_header": "Delete page",
+    "delete_completely": "Delete completely"
+  },
   "security_setting": {
     "Security settings": "Security settings",
-    "Guest Users Access": "Guest Users Access",
+    "Guest Users Access": "Guest users access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
-    "Register limitation desc": "Restricts ways to register new user.",
+    "Register limitation desc": "Restriction of new users' registration",
     "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
     "users_without_account": "Users without account is not accessible",
     "example": "Example",
-    "restrict_emails": "You can restrict registerable e-mail address.",
-    "for_instance": " For instance, if you use growi within a company, you can write ",
-    "only_those": " Only those whose e-mail address including the company address can register.",
+    "restrict_emails": "You can restrict email registration to your wiki by writing an email domain (beginning with @). ",
+    "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
+    "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
     "insert_single": "Please insert single e-mail address per line.",
-    "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
-    "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",
+    "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
+    "page_listing_1_desc": "Show pages that are restricted by 'Only 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",
+    "admin_only": "Admin only",
+    "admin_and_author": "Admin and author",
     "anyone": "Anyone",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
-    "xss_prevent_setting_link": "Go to Markdown settings",
+    "xss_prevent_setting_link": "Go to Markdown Settings",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
@@ -437,7 +446,7 @@
     "updated_general_security_setting": "Succeeded to update security setting",
     "setup_not_completed_yet": "Setup not completed yet",
     "guest_mode": {
-      "deny": "Deny (Registered Users Only)",
+      "deny": "Deny (Registered users only)",
       "readonly": "Accept (Guests can read only)"
     },
     "registration_mode": {
@@ -457,10 +466,10 @@
     "Local": {
       "name": "ID/Password",
       "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}/code> .",
-      "enable_local": "enable ID/Password"
+      "enable_local": "Enable ID/Password"
     },
     "ldap": {
-      "enable_ldap": "enable LDAP",
+      "enable_ldap": "Enable LDAP",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
       "bind_manager": "Manager Bind",
@@ -494,7 +503,7 @@
     },
     "SAML": {
       "name": "SAML",
-      "enable_saml": "enable SAML",
+      "enable_saml": "Enable SAML",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
@@ -507,18 +516,18 @@
       "updated_saml": "Succeeded to update SAML setting"
     },
     "Basic": {
-      "enable_basic": "enable Basic",
+      "enable_basic": "Enable Basic",
       "name": "Basic Authentication",
       "desc_1": "Login with <code>username</code> in Authorization header.",
       "desc_2": "User will be automatically generated if not exist.",
       "updated_basic": "Succeeded to update Basic setting"
     },
     "OAuth": {
-      "enable_oidc": "enable OIDC",
+      "enable_oidc": "Enable OIDC",
       "register": "Register for %s",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "Google": {
-        "enable_google": "enable Google OAuth",
+        "enable_google": "Enable Google OAuth",
         "name": "Google OAuth",
         "register_1": "Access {{link}}",
         "register_2": "Create Project if no projects exist",
@@ -531,7 +540,7 @@
         "name": "Facebook OAuth"
       },
       "Twitter": {
-        "enable_twitter": "enable Twitter OAuth",
+        "enable_twitter": "Enable Twitter OAuth",
         "name": "Twitter OAuth",
         "register_1": "Access {{link}}",
         "register_2": "Sign in Twitter",
@@ -541,7 +550,7 @@
         "updated_twitter": "Succeeded to update Twitter OAuth setting"
       },
       "GitHub": {
-        "enable_github": "enable GitHub OAuth",
+        "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
         "register_1": "Access {{link}}",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
@@ -579,36 +588,36 @@
     }
   },
   "notification_setting": {
-    "slack_incoming_configuration": "Slack Incoming Webhooks Configuration",
-    "prioritize_webhook": "Prioritize Incoming Webhook than Slack App",
+    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-    "slack_app_configuration": "Slack App Configuration",
+    "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
     "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
-      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks Configuration page</a>.",
+      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
       "workspace_desc2": "Choose the default channel to post.",
       "workspace_desc3": "Add.",
       "at_growi": "(At GROWI admin page) Set Webhook URL",
       "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
     },
-    "user_trigger_notification_header": "Default Notification Settings for Patterns",
+    "user_trigger_notification_header": "Default notification settings for patterns",
     "pattern": "Pattern",
     "channel": "Channel",
     "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
     "channel_desc": "Slack channel name. Without <code>#</code>.",
-    "valid_page": "Enable/Disable Notification",
+    "valid_page": "Enable/disable Notification",
     "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
-    "just_me_notification_help": "<strong>The page that is restricted by 'only me'</strong> is notify when the page edited.",
+    "just_me_notification_help": "<strong>The page that is restricted by 'Only Me'</strong> is notify when the page edited.",
     "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
-    "notification_list": "List of Notification Settings",
-    "add_notification": "Add New",
-    "trigger_path": "Trigger Path",
+    "notification_list": "List of notification settings",
+    "add_notification": "Add new",
+    "trigger_path": "Trigger path",
     "trigger_path_help": "(expression with <code>*</code> is supported)",
-    "trigger_events": "Trigger Events",
-    "notify_to": "Notify To",
+    "trigger_events": "Trigger events",
+    "notify_to": "Notify to",
     "back_to_list": "Go back to list",
     "notification_detail": "Notification Setting Details",
     "event_pageCreate": "When new page is \"CREATED\"",
@@ -628,8 +637,8 @@
     "toggle_notification": "Updated setting of {{path}}"
   },
   "full_text_search_management": {
-    "elasticsearch_management": "Elasticsearch Management",
-    "connection_status": "Connection Status",
+    "elasticsearch_management": "Elasticsearch management",
+    "connection_status": "Connection status",
     "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_connected": "CONNECTED",
     "connection_status_label_disconnected": "DISCONNECTED",
@@ -637,15 +646,15 @@
     "indices_status": "Indices Status",
     "indices_status_label_normalized": "NORMALIZED",
     "indices_status_label_unnormalized": "REBUILDING or BROKEN",
-    "indices_summary": "Indices Summary",
+    "indices_summary": "Indices summary",
     "reconnect": "Reconnect",
-    "reconnect_button": "Try to Reconnect",
+    "reconnect_button": "Try to reconnect",
     "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
     "normalize": "Normalize",
-    "normalize_button": "Normalize Indices",
+    "normalize_button": "Normalize indices",
     "normalize_description": "Click the button to repair broken indices.",
     "rebuild": "Rebuild",
-    "rebuild_button": "Rebuild Index",
+    "rebuild_button": "Rebuild index",
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
   },

+ 2 - 2
resource/locales/en-US/welcome.md

@@ -25,5 +25,5 @@ Slack
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
-Please join Slack by all means to make GROWI better.
-In addition to discussing development, we also accept questions at the time of introduction.
+Let's join our Slack channel for all to help make GROWI better.
+In addition to discussing development, we also accept questions at the time of introduction.

+ 4 - 2
resource/locales/ja/admin/admin.json

@@ -76,8 +76,8 @@
     "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "xss_options": {
       "enable_xss_prevention": "XSSを抑制する",
-      "ignore_all_tags": "すべてのタグを抑制する",
-      "ignore_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
+      "remove_all_tags": "すべてのタグを抑制する",
+      "remove_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
       "recommended_setting": "おすすめ設定",
       "custom_whitelist": "カスタムホワイトリスト",
       "tag_names": "タグ名",
@@ -245,6 +245,8 @@
       "new_password": "新しいパスワード"
     },
     "external_account": "外部アカウントの管理",
+    "external_accounts": "外部アカウント",
+    "create_external_account":"外部アカウントの作成",
     "external_account_list": "外部アカウント一覧",
     "invite": "招待する",
     "invited": "ユーザーを招待しました",

+ 14 - 5
resource/locales/ja/translation.json

@@ -2,7 +2,7 @@
   "Help": "ヘルプ",
   "Edit": "編集",
   "Delete": "削除",
-  "Delete All": "全て削除",
+  "delete_all": "全て削除",
   "Duplicate": "複製",
   "Copy": "コピー",
   "Click to copy": "クリックでコピー",
@@ -89,7 +89,7 @@
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
   "Table of Contents": "目次",
-  "Management Wiki Home": "Wiki管理トップ",
+  "Wiki Management Home Page": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "Site URL settings": "サイトURL設定",
   "Markdown Settings": "マークダウン設定",
@@ -107,7 +107,7 @@
   "Public": "公開",
   "Anyone with the link": "リンクを知っている人のみ",
   "Specified users": "特定ユーザーのみ",
-  "Just me": "自分のみ",
+  "Only me": "自分のみ",
   "Only inside the group": "特定グループのみ",
   "page_list_and_search_results": "ページリスト・検索結果",
   "scope_of_page_disclosure": "ページの公開範囲",
@@ -398,6 +398,15 @@
     "someone_editing": "このページは、HackMD で編集されています。",
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
   },
+  "search_result": {
+    "result_meta": "{{total}}件のページが見つかりました。検索ワード: \"{{keyword}}\"",
+    "deletion_mode_btn_lavel": "ページを指定して削除",
+    "cancel": "キャンセル",
+    "delete": "削除",
+    "check_all": "すべてチェック",
+    "deletion_modal_header": "以下のページを削除",
+    "delete_completely": "完全に削除する"
+  },
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
@@ -407,8 +416,8 @@
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
-    "for_instance": "例えば、",
-    "only_those": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
+    "for_example": "例えば、",
+    "in_this_case": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
     "insert_single": "1行に1メールアドレス入力してください。",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",

+ 14 - 8
src/client/js/admin.jsx

@@ -5,6 +5,8 @@ import { I18nextProvider } from 'react-i18next';
 
 import loggerFactory from '@alias/logger';
 
+import ErrorBoundary from './components/ErrorBoudary';
+
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
@@ -99,9 +101,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       elem,
     );
@@ -124,11 +128,13 @@ if (adminSecuritySettingElem != null) {
     adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
   ];
   ReactDOM.render(
-    <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <I18nextProvider i18n={i18n}>
-        <SecurityManagement />
-      </I18nextProvider>
-    </Provider>,
+    <I18nextProvider i18n={i18n}>
+      <ErrorBoundary>
+        <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
+          <SecurityManagement />
+        </Provider>
+      </ErrorBoundary>
+    </I18nextProvider>,
     adminSecuritySettingElem,
   );
 }

+ 9 - 4
src/client/js/app.jsx

@@ -5,6 +5,7 @@ import { I18nextProvider } from 'react-i18next';
 
 import loggerFactory from '@alias/logger';
 
+import ErrorBoundary from './components/ErrorBoudary';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
@@ -111,9 +112,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       elem,
     );
@@ -124,7 +127,9 @@ Object.keys(componentMappings).forEach((key) => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
-      <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      <ErrorBoundary>
+        <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      </ErrorBoundary>
     </I18nextProvider>, document.getElementById('revision-history'),
   );
 });

+ 2 - 2
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -103,7 +103,7 @@ class AwsSetting extends React.Component {
 
         <div className="row form-group">
           <label className="text-left text-md-right col-md-3 col-form-label">
-            Access Key ID
+            Access key ID
           </label>
           <div className="col-md-6">
             <input
@@ -119,7 +119,7 @@ class AwsSetting extends React.Component {
 
         <div className="row form-group">
           <label className="text-left text-md-right col-md-3 col-form-label">
-            Secret Access Key
+            Secret access key
           </label>
           <div className="col-md-6">
             <input

+ 2 - 2
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -20,7 +20,7 @@ class CustomizeLayoutOptions extends React.Component {
             layoutType="crowi-plus"
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
             onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-            labelHtml={`GROWI Enhanced Layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
+            labelHtml={`GROWI enhanced layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
           >
             <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
             <div className="text-justify d-inline-block">
@@ -38,7 +38,7 @@ class CustomizeLayoutOptions extends React.Component {
             layoutType="kibela"
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
             onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
-            labelHtml="Kibela Like Layout"
+            labelHtml="Kibela like layout"
           >
             <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
             <div className="text-justify d-inline-block">

+ 1 - 1
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -67,7 +67,7 @@ class CustomizeScriptSetting extends React.Component {
                 </tr>
                 <tr>
                   <th className="text-right"><code>appContainer</code></th>
-                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">Unstated Container</a></td>
+                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
                 </tr>
                 <tr>
                   <th className="text-right"><code>growiRenderer</code></th>

+ 1 - 1
src/client/js/components/Admin/Customize/CustomizeTitle.jsx

@@ -56,7 +56,7 @@ class CustomizeTitle extends React.Component {
           <div className="form-text text-muted col-12">
             Default Value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
             <br />
-            Default Output: <pre><code className="xml">&lt;title&gt;/Sandbox - {'GROWI'}&lt;&#047;title&gt;</code></pre>
+            Default Output: <code className="xml">&lt;title&gt;/Somewhere/Page - {'GROWI'}&lt;&#047;title&gt;</code>
           </div>
           <div className="form-group col-12">
             <input

+ 2 - 2
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -105,10 +105,10 @@ class ImportCollectionConfigurationModal extends React.Component {
             onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
           />
           <label htmlFor="cbOpt2" className="custom-control-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Just me') })}
+            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
             <p
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Just me') }) }}
+              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }}
             />
           </label>
         </div>

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx

@@ -127,7 +127,7 @@ class ImportForm extends React.Component {
         isImported: true,
       });
 
-      toastSuccess(undefined, 'Import process has terminated.');
+      toastSuccess(undefined, 'Import process has completed.');
     });
 
     // websocket event

+ 2 - 2
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -55,9 +55,9 @@ class XssForm extends React.Component {
                 onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
               />
               <label className="custom-control-label w-100" htmlFor="xssOption1">
-                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.ignore_all_tags')}</p>
+                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.remove_all_tags')}</p>
                 <div className="mt-4">
-                  {t('admin:markdown_setting.xss_options.ignore_all_tags_desc')}
+                  {t('admin:markdown_setting.xss_options.remove_all_tags_desc')}
                 </div>
               </label>
             </div>

+ 3 - 3
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -65,7 +65,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('slack-configuration') }}
               href="#slack-configuration"
             >
-              <i className="icon-settings"></i> Slack Configuration
+              <i className="icon-settings"></i> Slack configuration
             </NavLink>
           </NavItem>
           <NavItem>
@@ -74,7 +74,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('user-trigger-notification') }}
               href="#user-trigger-notification"
             >
-              <i className="icon-settings"></i> User Trigger Notification
+              <i className="icon-settings"></i> User trigger notification
             </NavLink>
           </NavItem>
           <NavItem>
@@ -83,7 +83,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('global-notification') }}
               href="#global-notification"
             >
-              <i className="icon-settings"></i> Global Notification
+              <i className="icon-settings"></i> Global notification
             </NavLink>
           </NavItem>
         </Nav>

+ 1 - 1
src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx

@@ -116,7 +116,7 @@ class SlackAppConfiguration extends React.Component {
               </div>
 
               <div className="row mb-5">
-                <label className="col-md-3 text-left text-md-right">OAuth Access Token</label>
+                <label className="col-md-3 text-left text-md-right">OAuth access token</label>
                 <div className="col-md-6">
                   <input
                     className="form-control"

+ 2 - 2
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -147,8 +147,8 @@ class LocalSecuritySetting extends React.Component {
                   defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
                 />
-                <p className="form-text text-muted small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_instance')}
-                  <code>@growi.org</code>{t('security_setting.only_those')}<br />
+                <p className="form-text text-muted small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_example')}
+                  <code>@growi.org</code>{t('security_setting.in_this_case')}<br />
                   {t('security_setting.insert_single')}
                 </p>
               </div>

+ 2 - 2
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -31,14 +31,14 @@ class UserGroupUserModal extends React.Component {
             <div className="col-6">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
-                  option="Mail"
+                  option="mail"
                   checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                 />
               </div>
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
-                  option="Name"
+                  option="name"
                   checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                 />

+ 56 - 0
src/client/js/components/ErrorBoudary.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * @see https://reactjs.org/docs/error-boundaries.html
+ */
+class ErrorBoundary extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = { error: null, errorInfo: null };
+  }
+
+  componentDidCatch(error, errorInfo) {
+    // You can also log the error to an error reporting service
+    // logErrorToMyService(error, errorInfo);
+
+    // Catch errors in any components below and re-render with error message
+    this.setState({
+      error,
+      errorInfo,
+    });
+  }
+
+  render() {
+    const { error, errorInfo } = this.state;
+    if (errorInfo != null) {
+
+      // split componetStack
+      // see https://regex101.com/r/Uc448G/1
+      const firstStack = errorInfo.componentStack.split(/\s*in\s/)[1];
+
+      return (
+        <div className="card border-danger">
+          <div className="card-header">Error occured in {firstStack}</div>
+          <div className="card-body">
+            <h5>{error && error.toString()}</h5>
+            <details className="card well small mb-0" style={{ whiteSpace: 'pre-wrap' }}>
+              {errorInfo.componentStack}
+            </details>
+          </div>
+        </div>
+      );
+    }
+
+    // Normally, just render children
+    return this.props.children;
+  }
+
+}
+
+ErrorBoundary.propTypes = {
+  children: PropTypes.object,
+};
+
+export default ErrorBoundary;

+ 1 - 1
src/client/js/components/Me/AssociateModal.jsx

@@ -72,7 +72,7 @@ class AssociateModal extends React.Component {
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
         <ModalHeader className="bg-info text-light" toggle={this.props.onClose}>
-          { t('Create External Account') }
+          { t('admin:user_management.create_external_account') }
         </ModalHeader>
         <ModalBody>
           <ul className="nav nav-tabs passport-settings mb-2" role="tablist">

+ 2 - 2
src/client/js/components/Me/ExternalAccountLinkedMe.jsx

@@ -73,14 +73,14 @@ class ExternalAccountLinkedMe extends React.Component {
               <i className="icon-plus" aria-hidden="true" />
             Add
             </button>
-            { t('External Accounts') }
+            { t('admin:user_management.external_accounts') }
           </h2>
         </div>
 
         <table className="table table-bordered table-user-list">
           <thead>
             <tr>
-              <th width="120px">Authentication Provider</th>
+              <th width="120px">{ t('admin:user_management.authentication_provider') }</th>
               <th>
                 <code>accountId</code>
               </th>

+ 4 - 1
src/client/js/components/Me/PersonalSettings.jsx

@@ -23,7 +23,10 @@ class PersonalSettings extends React.Component {
                 <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
               </li>
               <li className="nav-item">
-                <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab"><i className="icon-share-alt"></i> { t('External Accounts') }</a>
+                <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
+                  <i className="icon-share-alt"></i>
+                  { t('admin:user_management.external_accounts') }
+                </a>
               </li>
               <li className="nav-item">
                 <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>

+ 1 - 1
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -143,7 +143,7 @@ class MyDraftList extends React.Component {
               <div className="align-self-center">
                 <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.clearAllDrafts}>
                   <i className="icon-fw icon-fire text-danger"></i>
-                  {t('Delete All')}
+                  {t('delete_all')}
                 </button>
               </div>
             </div>

+ 3 - 3
src/client/js/components/Page/CopyDropdown.jsx

@@ -45,7 +45,7 @@ class CopyDropdown extends React.Component {
       search, hash,
     } = window.location;
 
-    return decodeURI(`${pagePath}${search}${hash}`);
+    return `${pagePath}${search}${hash}`;
   }
 
   generatePagePathUrl() {
@@ -64,7 +64,7 @@ class CopyDropdown extends React.Component {
     const {
       origin, search, hash,
     } = location;
-    return decodeURI(`${origin}/${pageId}${search}${hash}`);
+    return `${origin}/${pageId}${search}${hash}`;
   }
 
   generateMarkdownLink() {
@@ -76,7 +76,7 @@ class CopyDropdown extends React.Component {
     const label = `${pagePath}${search}${hash}`;
     const permalink = this.generatePermalink();
 
-    return decodeURI(`[${label}](${permalink})`);
+    return `[${label}](${permalink})`;
   }
 
   DropdownItemContents = ({ title, contents }) => (

+ 1 - 1
src/client/js/components/PageEditor/Editor.jsx

@@ -261,7 +261,7 @@ export default class Editor extends AbstractEditor {
     return (
       <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <span className="text-white"><i className="icon-fw icon-question" />Markdown Help</span>
+          <span className="text-white"><i className="icon-fw icon-question" />Markdown help</span>
         </ModalHeader>
         <ModalBody>
           <Cheatsheet />

+ 2 - 2
src/client/js/components/PageList/Page.jsx

@@ -12,8 +12,8 @@ export default class Page extends React.Component {
       page, noLink,
     } = this.props;
 
-    let pagePathElem = <PagePathLabel page={page} />;
-    if (!noLink != null) {
+    let pagePathElem = <PagePathLabel page={page} additionalClassNames={['mx-1']} />;
+    if (!noLink) {
       pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
     }
 

+ 3 - 3
src/client/js/components/PageList/PagePathLabel.jsx

@@ -7,14 +7,14 @@ const PagePathLabel = (props) => {
 
   const dPagePath = new DevidedPagePath(props.page.path, false, true);
 
-  let classNames = ['page-path'];
+  let classNames = [''];
   classNames = classNames.concat(props.additionalClassNames);
 
   if (props.isLatterOnly) {
     return <span className={classNames.join(' ')}>{dPagePath.latter}</span>;
   }
 
-  const textElem = (dPagePath.former == null && dPagePath.latter == null)
+  const textElem = dPagePath.isRoot
     ? <><strong>/</strong></>
     : <>{dPagePath.former}/<strong>{dPagePath.latter}</strong></>;
 
@@ -24,7 +24,7 @@ const PagePathLabel = (props) => {
 PagePathLabel.propTypes = {
   page: PropTypes.object.isRequired,
   isLatterOnly: PropTypes.bool,
-  additionalClassNames: PropTypes.array,
+  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
 };
 
 PagePathLabel.defaultProps = {

+ 1 - 10
src/client/js/components/PageTimeline.jsx

@@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 
-import * as entities from 'entities';
-
 import AppContainer from '../services/AppContainer';
 import { createSubscribedElement } from './UnstatedUtils';
 
@@ -77,14 +75,7 @@ class PageTimeline extends React.Component {
       return null;
     }
 
-    let pages = JSON.parse(pageIdsElm.text);
-    // decode path
-    pages = pages.map((page) => {
-      page.path = decodeURIComponent(entities.decodeHTML(page.path));
-      return page;
-    });
-
-    return pages;
+    return JSON.parse(pageIdsElm.text);
   }
 
   render() {

+ 1 - 1
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -37,7 +37,7 @@ class GrantSelector extends React.Component {
       },
       // { grant: 3, iconClass: '', label: 'Specified users only' },
       {
-        grant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Just me',
+        grant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Only me',
       },
       {
         grant: 5, iconClass: 'icon-options', styleClass: '', label: 'Only inside the group', reselectLabel: 'Reselect the group',

+ 12 - 4
src/client/js/components/SearchPage/DeletePageListModal.jsx

@@ -1,12 +1,14 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import { withTranslation } from 'react-i18next';
+
 import {
   Button,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 
-export default class DeletePageListModal extends React.Component {
+class DeletePageListModal extends React.Component {
 
   /*
    * the threshold for omitting body
@@ -17,6 +19,7 @@ export default class DeletePageListModal extends React.Component {
   }
 
   render() {
+    const { t } = this.props;
     if (this.props.pages == null || this.props.pages.length === 0) {
       return <div></div>;
     }
@@ -30,7 +33,7 @@ export default class DeletePageListModal extends React.Component {
     return (
       <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-list-delete-modal">
         <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
-          Deleting pages:
+          {t('search_result.deletion_modal_header')}
         </ModalHeader>
         <ModalBody>
           <ul>
@@ -53,11 +56,12 @@ export default class DeletePageListModal extends React.Component {
                   className="custom-control-label text-danger"
                   htmlFor="customCheck-delete-completely"
                 >
-                  Delete completely
+                  {t('search_result.delete_completely')}
                 </label>
               </div>
               <Button color={this.props.isDeleteCompletely ? 'danger' : 'light'} onClick={this.props.confirmedToDelete}>
-                <i className="icon-trash"></i>Delete
+                <i className="icon-trash"></i>
+                {t('search_result.delete')}
               </Button>
             </span>
           </div>
@@ -73,6 +77,8 @@ DeletePageListModal.defaultProps = {
 };
 
 DeletePageListModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
   isShown: PropTypes.bool.isRequired,
   pages: PropTypes.array,
   errorMessage: PropTypes.string,
@@ -81,3 +87,5 @@ DeletePageListModal.propTypes = {
   confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
   toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
 };
+
+export default withTranslation()(DeletePageListModal);

+ 11 - 6
src/client/js/components/SearchPage/SearchResult.jsx

@@ -2,6 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 
+import { withTranslation } from 'react-i18next';
+
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
 import DeletePageListModal from './DeletePageListModal';
@@ -195,7 +197,7 @@ class SearchResult extends React.Component {
                 )
               }
               <div className="page-list-option">
-                <a href={page.path}><i className="icon-login" /></a>
+                <button type="button" className="btn btn-link p-0" href={page.path}><i className="icon-login" /></button>
               </div>
             </div>
           </a>
@@ -205,6 +207,8 @@ class SearchResult extends React.Component {
   }
 
   render() {
+    const { t } = this.props;
+
     if (this.isError()) {
       return (
         <div className="content-main">
@@ -237,7 +241,7 @@ class SearchResult extends React.Component {
       deletionModeButtons = (
         <div className="btn-group">
           <button type="button" className="btn btn-outline-secondary btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
-            <i className="icon-ban" /> Cancel
+            <i className="icon-ban" /> {t('search_result.cancel')}
           </button>
           <button
             type="button"
@@ -245,7 +249,7 @@ class SearchResult extends React.Component {
             onClick={() => { return this.showDeleteConfirmModal() }}
             disabled={this.state.selectedPages.size === 0}
           >
-            <i className="icon-trash" /> Delete
+            <i className="icon-trash" /> {t('search_result.delete')}
           </button>
         </div>
       );
@@ -258,7 +262,7 @@ class SearchResult extends React.Component {
             onChange={() => { return this.handleAllSelect() }}
             checked={this.isAllSelected()}
           />
-          <label className="custom-control-label" htmlFor="all-select-check">&nbsp;Check All</label>
+          <label className="custom-control-label" htmlFor="all-select-check">&nbsp;{t('search_result.check_all')}</label>
         </div>
       );
     }
@@ -266,7 +270,7 @@ class SearchResult extends React.Component {
       deletionModeButtons = (
         <div className="btn-group">
           <button type="button" className="btn btn-outline-secondary rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
-            <i className="ti-check-box" /> DeletionMode
+            <i className="ti-check-box" /> {t('search_result.deletion_mode_btn_lavel')}
           </button>
         </div>
       );
@@ -326,6 +330,7 @@ const SearchResultWrapper = (props) => {
 
 SearchResult.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  t: PropTypes.func.isRequired, // i18next
 
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
@@ -337,4 +342,4 @@ SearchResult.defaultProps = {
   searchError: null,
 };
 
-export default SearchResultWrapper;
+export default withTranslation()(SearchResultWrapper);

+ 1 - 1
src/client/js/components/SearchTypeahead.jsx

@@ -164,7 +164,7 @@ class SearchTypeahead extends React.Component {
     return (
       <span>
         <UserPicture user={page.lastUpdateUser} size="sm" noLink />
-        <PagePathLabel page={page} />
+        <span className="ml-1"><PagePathLabel page={page} /></span>
         <PageListMeta page={page} />
       </span>
     );

+ 1 - 1
src/client/js/components/StaffCredit/Contributor.js

@@ -111,7 +111,7 @@ const contributors = [
         ],
       },
       {
-        additionalClass: 'col-md-12 staff-credit-mt-10',
+        additionalClass: 'col-12 staff-credit-mt-10rem',
         members: [
           { name: 'AND YOU' },
         ],

+ 6 - 6
src/client/js/components/StaffCredit/StaffCredit.jsx

@@ -8,8 +8,8 @@ import {
 
 import contributors from './Contributor';
 
-// Unit is px / milli sec
-const scrollSpeed = 0.3;
+// px / sec
+const scrollSpeed = 200;
 
 /**
  * Page staff credit component
@@ -47,7 +47,7 @@ export default class StaffCredit extends React.Component {
         });
         const target = $('.credit-curtain');
         const scrollTargetHeight = target.children().innerHeight();
-        const duration = scrollTargetHeight / scrollSpeed;
+        const duration = scrollTargetHeight / scrollSpeed * 1000;
         target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
 
         target.slimScroll({
@@ -105,7 +105,7 @@ export default class StaffCredit extends React.Component {
         return (
           <React.Fragment key={`${contributor.sectionName}-fragment`}>
             <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
-              <h2 className="col-md-12 dev-team mt-5 staff-credit-mb-10" key={contributor.sectionName}>{contributor.sectionName}</h2>
+              <h2 className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem" key={contributor.sectionName}>{contributor.sectionName}</h2>
               {memberGroups}
             </div>
             <div className="clearfix"></div>
@@ -113,8 +113,8 @@ export default class StaffCredit extends React.Component {
         );
       });
       return (
-        <div className="text-center staff-credit-pb-10" onClick={this.deleteCredit}>
-          <h1 className="staff-credit-mb-10">GROWI Contributors</h1>
+        <div className="text-center staff-credit-content" onClick={this.deleteCredit}>
+          <h1 className="staff-credit-mb-6rem">GROWI Contributors</h1>
           <div className="clearfix"></div>
           {credit}
         </div>

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

@@ -255,6 +255,7 @@ $(() => {
     if (name.match(/.+\/$/)) {
       name = name.substr(0, name.length - 1);
     }
+    // TODO: remove by GW-2278
     window.location.href = `${pathUtils.encodePagePath(name)}#edit`;
     return false;
   });

+ 2 - 1
src/client/js/services/PageContainer.js

@@ -31,6 +31,7 @@ export default class PageContainer extends Container {
     }
 
     const revisionId = mainContent.getAttribute('data-page-revision-id');
+    const path = decodeURI(mainContent.getAttribute('data-path'));
 
     this.state = {
       // local page data
@@ -39,7 +40,7 @@ export default class PageContainer extends Container {
       revisionId,
       revisionCreatedAt: +mainContent.getAttribute('data-page-revision-created'),
       revisionAuthor: JSON.parse(mainContent.getAttribute('data-page-revision-author')),
-      path: mainContent.getAttribute('data-path'),
+      path,
       tocHtml: '',
       isLiked: JSON.parse(mainContent.getAttribute('data-page-is-liked')),
       seenUserIds: [],

+ 16 - 18
src/client/styles/scss/_page_list.scss

@@ -1,4 +1,4 @@
-.page-list {
+body .page-list {
   .page-list-container {
     font-size: 15px;
     line-height: 1.6em;
@@ -6,15 +6,12 @@
 
   .page-list-ul {
     padding-left: 0;
+    margin: 0;
 
     > li {
+      margin: 0;
       list-style: none;
 
-      .picture {
-        width: 16px;
-        height: 16px;
-      }
-
       > a {
         padding: 0px;
         color: inherit;
@@ -22,22 +19,23 @@
         &:hover {
           color: inherit;
         }
+      }
+    }
 
-        span.page-path {
-          padding: 0 4px;
-        }
+    .picture {
+      width: 16px;
+      height: 16px;
+    }
 
-        > span.page-list-meta {
-          font-size: 0.9em;
+    .page-list-meta {
+      font-size: 0.9em;
 
-          > span {
-            margin-right: 0.3rem;
-          }
+      > span {
+        margin-right: 0.3rem;
+      }
 
-          i {
-            margin-right: 2px;
-          }
-        }
+      i {
+        margin-right: 2px;
       }
     }
 

+ 11 - 11
src/client/styles/scss/_staff_credit.scss

@@ -2,8 +2,13 @@
 .staff-credit {
   // attached !important for updating from .modal-dialog class style
   width: 80vw !important;
+  max-width: unset !important;
+
   height: 80vh !important;
-  max-width: initial !important;
+  max-height: unset !important;
+
+  margin: 10vh 10vw !important;
+
   // see https://css-tricks.com/old-timey-terminal-styling/
   @mixin old-timey-terminal-styling() {
     text-shadow: 0 0 10px #c8c8c8;
@@ -48,20 +53,15 @@
     font-size: 1.8em;
   }
 
-  .staff-credit-mt-10 {
-    margin-top: 6rem;
+  .staff-credit-mt-10rem {
+    margin-top: 10rem;
   }
 
-  .staff-credit-mb-10 {
+  .staff-credit-mb-6rem {
     margin-bottom: 6rem;
   }
 
-  .staff-credit-my-10 {
-    @extend .staff-credit-mt-10;
-    @extend .staff-credit-mb-10;
-  }
-
-  .staff-credit-pb-10 {
-    padding-bottom: 6rem;
+  .staff-credit-content {
+    padding-bottom: 40vh;
   }
 }

+ 0 - 2
src/server/crowi/express-init.js

@@ -10,7 +10,6 @@ module.exports = function(crowi, app) {
   const methodOverride = require('method-override');
   const passport = require('passport');
   const expressSession = require('express-session');
-  const sanitizer = require('express-sanitizer');
   const flash = require('connect-flash');
   const swig = require('swig-templates');
   const webpackAssets = require('express-webpack-assets');
@@ -93,7 +92,6 @@ module.exports = function(crowi, app) {
   app.use(methodOverride());
   app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
   app.use(bodyParser.json({ limit: '50mb' }));
-  app.use(sanitizer());
   app.use(cookieParser());
 
   // configure express-session

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

@@ -107,7 +107,7 @@ module.exports = function(crowi) {
       'customize:highlightJsStyle' : 'github',
       'customize:highlightJsStyleBorder' : false,
       'customize:theme' : 'default',
-      'customize:layout' : 'crowi',
+      'customize:layout' : 'growi',
       'customize:isEnabledTimeline' : true,
       'customize:isSavedStatesOfTabChanges' : true,
       'customize:isEnabledAttachTitleHeader' : false,

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

@@ -509,7 +509,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     // grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
     grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
-    grantLabels[GRANT_OWNER] = 'Just me'; // 自分のみ
+    grantLabels[GRANT_OWNER] = 'Only me'; // 自分のみ
 
     return grantLabels;
   };

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

@@ -485,7 +485,7 @@ module.exports = function(crowi, app) {
    */
   api.add = async function(req, res) {
     let pageId = req.body.page_id || null;
-    const pagePath = decodeURIComponent(req.body.path) || null;
+    const pagePath = req.body.path || null;
     let pageCreated = false;
 
     // check params

+ 16 - 15
src/server/routes/page.js

@@ -146,7 +146,7 @@ module.exports = function(crowi, app) {
   const ApiResponse = require('../util/apiResponse');
   const getToday = require('../util/getToday');
 
-  const { slackNotificationService } = crowi;
+  const { slackNotificationService, configManager } = crowi;
   const interceptorManager = crowi.getInterceptorManager();
   const globalNotificationService = crowi.getGlobalNotificationService();
 
@@ -252,7 +252,6 @@ module.exports = function(crowi, app) {
 
   function addRendarVarsForPage(renderVars, page) {
     renderVars.page = page;
-    renderVars.path = page.path;
     renderVars.revision = page.revision;
     renderVars.author = page.revision.author;
     renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
@@ -298,7 +297,7 @@ module.exports = function(crowi, app) {
       seener_threshold: SEENER_THRESHOLD,
     };
     renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
-    renderVars.pages = pathUtils.encodePagesPath(result.pages);
+    renderVars.pages = result.pages;
   }
 
   function replacePlaceholdersOfTemplate(template, req) {
@@ -337,8 +336,9 @@ module.exports = function(crowi, app) {
   async function showTopPage(req, res, next) {
     const portalPath = req.path;
     const revisionId = req.query.revision;
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
-    const view = 'customlayout-selector/page_list';
+    const view = `layout-${layoutName}/page_list`;
     const renderVars = { path: portalPath };
 
     let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
@@ -362,6 +362,7 @@ module.exports = function(crowi, app) {
   async function showPageForGrowiBehavior(req, res, next) {
     const path = getPathFromRequest(req);
     const revisionId = req.query.revision;
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     let page = await Page.findByPathAndViewer(path, req.user);
 
@@ -372,7 +373,7 @@ module.exports = function(crowi, app) {
     }
     if (page.redirectTo) {
       debug(`Redirect to '${page.redirectTo}'`);
-      return res.redirect(encodeURI(`${page.redirectTo}?redirectFrom=${pathUtils.encodePagePath(path)}`));
+      return res.redirect(`${encodeURI(page.redirectTo)}?redirectFrom=${encodeURIComponent(path)}`);
     }
 
     logger.debug('Page is found when processing pageShowForGrowiBehavior', page._id, page.path);
@@ -381,7 +382,7 @@ module.exports = function(crowi, app) {
     const offset = parseInt(req.query.offset) || 0;
     const renderVars = {};
 
-    let view = 'customlayout-selector/page';
+    let view = `layout-${layoutName}/page`;
 
     page.initLatestRevisionField(revisionId);
 
@@ -395,7 +396,7 @@ module.exports = function(crowi, app) {
 
     if (isUserPage(page.path)) {
       // change template
-      view = 'customlayout-selector/user_page';
+      view = `layout-${layoutName}/user_page`;
       await addRenderVarsForUserPage(renderVars, page, req.user);
     }
 
@@ -472,18 +473,19 @@ module.exports = function(crowi, app) {
     const path = getPathFromRequest(req);
 
     const isCreatable = Page.isCreatableName(path);
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     let view;
     const renderVars = { path };
 
     if (!isCreatable) {
-      view = 'customlayout-selector/not_creatable';
+      view = `layout-${layoutName}/not_creatable`;
     }
     else if (req.isForbidden) {
-      view = 'customlayout-selector/forbidden';
+      view = `layout-${layoutName}/forbidden`;
     }
     else {
-      view = 'customlayout-selector/not_found';
+      view = `layout-${layoutName}/not_found`;
 
       // retrieve templates
       if (req.user != null) {
@@ -514,6 +516,7 @@ module.exports = function(crowi, app) {
   actions.deletedPageListShow = async function(req, res) {
     // normalizePath makes '/trash/' -> '/trash'
     const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
+    const layoutName = configManager.getConfig('crowi', 'customize:layout');
 
     const limit = 50;
     const offset = parseInt(req.query.offset) || 0;
@@ -537,8 +540,8 @@ module.exports = function(crowi, app) {
     }
 
     renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
-    renderVars.pages = pathUtils.encodePagesPath(result.pages);
-    res.render('customlayout-selector/page_list', renderVars);
+    renderVars.pages = result.pages;
+    res.render(`layout-${layoutName}/page_list`, renderVars);
   };
 
   /**
@@ -550,7 +553,7 @@ module.exports = function(crowi, app) {
     const page = await Page.findByIdAndViewer(id, req.user);
 
     if (page != null) {
-      return res.redirect(pathUtils.encodePagePath(page.path));
+      return res.redirect(encodeURI(page.path));
     }
 
     return res.redirect('/');
@@ -646,7 +649,6 @@ module.exports = function(crowi, app) {
         result.pages.pop();
       }
 
-      result.pages = pathUtils.encodePagesPath(result.pages);
       return res.json(ApiResponse.success(result));
     }
     catch (err) {
@@ -1609,7 +1611,6 @@ module.exports = function(crowi, app) {
 
     try {
       const result = await Page.findListByCreator(page.creator, req.user, queryOptions);
-      result.pages = pathUtils.encodePagesPath(result.pages);
 
       return res.json(ApiResponse.success(result));
     }

+ 1 - 1
src/server/service/import.js

@@ -253,7 +253,7 @@ class ImportService {
           callback();
         },
         final(callback) {
-          logger.info(`Importing ${collectionName} has terminated.`);
+          logger.info(`Importing ${collectionName} has completed.`);
           callback();
         },
       });

+ 1 - 1
src/server/service/search-delegator/elasticsearch.js

@@ -468,7 +468,7 @@ class ElasticsearchDelegator {
         callback();
       },
       final(callback) {
-        logger.info(`Adding pages has terminated: (totalCount=${totalCount}, skipped=${skipped})`);
+        logger.info(`Adding pages has completed: (totalCount=${totalCount}, skipped=${skipped})`);
 
         if (isEmittingProgressEvent) {
           searchEvent.emit('finishAddPage', totalCount, count, skipped);

+ 1 - 2
src/server/util/swigFunctions.js

@@ -1,7 +1,6 @@
 module.exports = function(crowi, req, locals) {
   const debug = require('debug')('growi:lib:swigFunctions');
   const stringWidth = require('string-width');
-  const entities = require('entities');
 
   const { pathUtils } = require('growi-commons');
 
@@ -179,7 +178,7 @@ module.exports = function(crowi, req, locals) {
     return pages.map((page) => {
       return {
         id: page.id,
-        path: entities.encodeHTML(page.path),
+        path: page.path,
         revision: page.revision,
       };
     });

+ 0 - 317
src/server/views/admin/Users_reserve.html

@@ -1,317 +0,0 @@
-{% extends '../layout/admin.html' %}
-
-{% block html_title %}{{ customizeService.generateCustomTitle(t('User_Management')) }}{% endblock %}
-
-{% block content_header %}
-<h1 class="title">{{ t('User_Management') }}</h1>
-{% endblock %}
-
-{% block content_main %}
-<div class="content-main">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user'} %}
-    </div>
-
-    <div class="col-lg-9">
-      <p>
-        <button data-toggle="collapse" class="btn btn-default" href="#inviteUserForm" {% if isUserCountExceedsUpperLimit %}disabled{% endif %}>
-          {{ t("user_management.invite_users") }}
-        </button>
-        <a class="btn btn-default btn-outline" href="/admin/users/external-accounts">
-          <i class="icon-user-follow" aria-hidden="true"></i>
-          {{ t("user_management.external_account") }}
-        </a>
-      </p>
-      <form role="form" action="/admin/user/invite" method="post">
-        <div id="inviteUserForm" class="collapse">
-          <div class="form-group">
-            <label for="inviteForm[emailList]">{{ t('user_management.emails') }}</label>
-            <textarea class="form-control" name="inviteForm[emailList]" placeholder="{{ t('eg') }} user@growi.org"></textarea>
-          </div>
-          <div class="checkbox checkbox-info">
-            <input type="checkbox" id="inviteWithEmail" name="inviteForm[sendEmail]" checked>
-            <label for="inviteWithEmail">{{ t('user_management.invite_thru_email') }}</label>
-          </div>
-          <button type="submit" class="btn btn-primary">{{ t('user_management.invite') }}</button>
-        </div>
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-      </form>
-
-      {% if isUserCountExceedsUpperLimit === true %}
-      <label>{{ t('user_management.cannot_invite_maximum_users') }}</label>
-      {% endif %}
-      {% if userUpperLimit !== 0 %}
-      <label>{{ t('user_management.current_users') }}{{ activeUsers }}</label>
-      {% endif %}
-
-      {% set createdUser = req.flash('createdUser') %}
-      {% if createdUser.length %}
-      <div class="modal fade" id="createdUserModal">
-        <div class="modal-dialog">
-          <div class="modal-content">
-
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.invited') }}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.temporary_password') }}<br>
-                {{ t('user_management.password_never_seen') }}<span class="text-danger">{{ t('user_management.send_temporary_password') }}</span>
-              </p>
-
-              <pre>{% for cUser in createdUser %}{% if cUser.user %}{{ cUser.email }} {{ cUser.password }}<br>{% else %}{{ cUser.email }} 作成失敗<br>{% endif %}{% endfor %}</pre>
-            </div>
-
-          </div><!-- /.modal-content -->
-        </div><!-- /.modal-dialog -->
-      </div><!-- /.modal -->
-      {% endif %}
-
-      {# FIXME とりあえずクソ実装。React化はやくしたいなー(チラッチラッ #}
-      <div class="modal fade" id="admin-password-reset-modal">
-        <div class="modal-dialog">
-          <div class="modal-content">
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.reset_password')}}</div>
-            </div>
-
-            <div class="modal-body">
-              <p>
-                {{ t('user_management.password_never_seen') }}<br>
-              <span class="text-danger">{{ t('user_management.send_new_password') }}</span>
-              </p>
-              <p>
-              {{ t('user_management.target_user') }}: <code id="admin-password-reset-user"></code>
-              </p>
-
-              <form method="post" id="admin-users-reset-password">
-                <input type="hidden" name="user_id" value="">
-                <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                <button type="submit" value="" class="btn btn-primary">
-                  {{ t('user_management.reset_password')}}
-                </button>
-              </form>
-
-            </div>
-
-          </div><!-- /.modal-content -->
-        </div>/.modal-dialog
-      </div>
-      <div class="modal fade" id="admin-password-reset-modal-done">
-        <div class="modal-dialog">
-          <div class="modal-content">
-
-            <div class="modal-header">
-              <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-              <div class="modal-title">{{ t('user_management.reset_password') }}</div>
-            </div>
-
-            <div class="modal-body">
-              <p class="alert alert-danger">Let the user know the new password below and strongly recommend to change another one immediately. </p>
-              <p>
-              Reset user: <code id="admin-password-reset-done-user"></code>
-              </p>
-              <p>
-              New password: <code id="admin-password-reset-done-password"></code>
-              </p>
-            </div>
-            <div class="modal-footer">
-              <button class="btn btn-primary" data-dismiss="modal">OK</button>
-            </div>
-          </div><!-- /.modal-content -->
-        </div><!-- /.modal-dialog -->
-      </div>
-
-      <h2>{{ t("User_Management") }}</h2>
-
-      <table class="table table-default table-bordered table-user-list">
-        <thead>
-          <tr>
-            <th width="100px">#</th>
-            <th>{{ t('status') }}</th>
-            <th><code>{{ t('User') }}</code></th>
-            <th>{{ t('Name') }}</th>
-            <th>{{ t('Email') }}</th>
-            <th width="100px">{{ t('Created') }}</th>
-            <th width="150px">{{ t('Last_Login') }}</th>
-            <th width="70px"></th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for sUser in users %}
-          {% set sUserId = sUser._id.toString() %}
-          <tr>
-            <td>
-              <img src="{{ sUser|picture }}" class="picture rounded-circle" />
-              {% if sUser.admin %}
-              <span class="badge badge-dark label-admin">
-              {{ t('administrator') }}
-              </span>
-              {% endif %}
-            </td>
-            <td>
-              <span class="label {{ css.userStatus(sUser) }}">
-                {{ consts.userStatus[sUser.status] }}
-              </span>
-            </td>
-            <td>
-              <strong>{{ sUser.username }}</strong>
-            </td>
-            <td>{{ sUser.name }}</td>
-            <td>{{ sUser.email }}</td>
-            <td>{{ sUser.createdAt|date('Y-m-d', sUser.createdAt.getTimezoneOffset()) }}</td>
-            <td>
-              {% if sUser.lastLoginAt %}
-                {{ sUser.lastLoginAt|date('Y-m-d H:i', sUser.createdAt.getTimezoneOffset()) }}
-              {% endif %}
-            </td>
-            <td>
-              <div class="btn-group admin-user-menu">
-                <button type="button" class="btn btn-sm btn-default dropdown-toggle" data-toggle="dropdown">
-                  <i class="icon-settings"></i> <span class="caret"></span>
-                </button>
-                <ul class="dropdown-menu" role="menu">
-                  <li class="dropdown-header">{{ t('user_management.edit_menu') }}</li>
-                  <li>
-                    <a href="#"
-                        data-user-id="{{ sUserId }}"
-                        data-user-email="{{ sUser.email }}"
-                        data-target="#admin-password-reset-modal"
-                        data-toggle="modal">
-                      <i class="icon-fw icon-key"></i>
-                      {{ t('user_management.reset_password') }}
-                    </a>
-                  </li>
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('status') }}</li>
-
-                  {% if sUser.status == 1 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-user-following"></i> {{ t('user_management.accept') }}
-                    </a>
-                  </li>
-                  {% endif  %}
-
-                  {% if sUser.status == 2 %}
-                  <form id="form_suspend_{{ sUserId }}" action="/admin/user/{{ sUserId }}/suspend" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                    <a href="javascript:form_suspend_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    {% else %}
-                    <a disabled>
-                      <i class="icon-fw icon-ban"></i>
-                      {{ t('user_management.deactivate_account') }}
-                    </a>
-                    <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.your_own") }}</p>
-                    {% endif %}
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 3 %}
-                  <form id="form_activate_{{ sUserId }}" action="/admin/user/{{ sUserId }}/activate" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <form id="form_remove_{{ sUserId }}" action="/admin/user/{{ sUserId }}/remove" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_activate_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-action-redo"></i> {{ t('Undo') }}
-                    </a>
-                  </li>
-                  <li>
-                    {# label は同じだけど、こっちは論理削除 #}
-                    <a href="javascript:form_remove_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 1 || sUser.status == 5 %}
-                  <form id="form_removeCompletely_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeCompletely" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li class="dropdown-button">
-                    {# label は同じだけど、こっちは物理削除 #}
-                    <a href="javascript:form_removeCompletely_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% if sUser.status == 2 %} {# activated な人だけこのメニューを表示 #}
-                  <li class="divider"></li>
-                  <li class="dropdown-header">{{ t('user_management.administrator_menu') }}</li>
-
-                  {% if sUser.admin %}
-                  <form id="form_removeFromAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/removeFromAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    {% if sUser.username != user.username %}
-                      <a href="javascript:form_removeFromAdmin_{{ sUserId }}.submit()">
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                    {% else %}
-                      <a disabled>
-                        <i class="icon-fw icon-user-unfollow"></i> {{ t("user_management.remove_admin_access") }}
-                      </a>
-                      <p class="alert alert-danger m-l-10 m-r-10 p-10">{{ t("user_management.cannot_remove") }}</p>
-                    {% endif %}
-                  </li>
-                  {% else %}
-                  <form id="form_makeAdmin_{{ sUserId }}" action="/admin/user/{{ sUser._id.toString() }}/makeAdmin" method="post">
-                    <input type="hidden" name="_csrf" value="{{ csrf() }}">
-                  </form>
-                  <li>
-                    <a href="javascript:form_makeAdmin_{{ sUserId }}.submit()">
-                      <i class="icon-fw icon-magic-wand"></i> {{ t("user_management.give_admin_access") }}
-                    </a>
-                  </li>
-                  {% endif %}
-
-                  {% endif %}
-                </ul>
-              </div>
-            </td>
-          </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-
-      {% include '../widget/pager.html' with {path: "/admin/users", pager: pager} %}
-
-    </div>
-  </div>
-</div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %} -->

+ 2 - 2
src/server/views/admin/index.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Management Wiki Home')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Wiki Management Home Page')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title"> {{ t('Management Wiki Home') }}</h1>
+<h1 class="title"> {{ t('Wiki Management Home Page') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 2
src/server/views/admin/markdown.html

@@ -1,7 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Markdown settings')) }}
- · {{ path }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Markdown settings')) }}{% endblock %}
 
 {% block content_header %}
 <h1 class="title">{{ t('Markdown Settings') }}</h1>

+ 0 - 3
src/server/views/admin/search.html

@@ -13,9 +13,6 @@
       class="col-lg-9"
       id ="admin-full-text-search-management"
     >
-      <!-- Reactify Paginator start -->
-      <!-- {% include '../widget/pager.html' with {path: "/admin/search", pager: pager} %} -->
-      <!-- Reactify Paginator end -->
     </div>
 </div>
 

+ 0 - 7
src/server/views/customlayout-selector/forbidden.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/forbidden.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/forbidden.html' %}
-{% else %}
-  {% include '../layout-growi/forbidden.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/not_creatable.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/not_creatable.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/not_creatable.html' %}
-{% else %}
-  {% include '../layout-growi/not_creatable.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/not_found.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/not_found.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/not_found.html' %}
-{% else %}
-  {% include '../layout-growi/not_found.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/page.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/page.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/page.html' %}
-{% else %}
-  {% include '../layout-growi/page.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/page_list.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/page_list.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/page_list.html' %}
-{% else %}
-  {% include '../layout-growi/page_list.html' %}
-{% endif %}

+ 0 - 7
src/server/views/customlayout-selector/user_page.html

@@ -1,7 +0,0 @@
-{% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}
-  {% include '../layout-crowi/user_page.html' %}
-{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout')%}
-  {% include '../layout-kibela/user_page.html' %}
-{% else %}
-  {% include '../layout-growi/user_page.html' %}
-{% endif %}

+ 1 - 1
src/server/views/modal/delete.html

@@ -56,7 +56,7 @@
                 <input type="hidden" name="completely" value="true">
                 <button type="submit" class="m-l-10 btn btn-danger delete-button">
                   <i class="icon-fire" aria-hidden="true"></i>
-                  {{ t('delete_completely') }}
+                  {{ t('modal_delete.delete_completely') }}
                 </button>
               {% else %}
                 <button type="submit" class="m-l-10 btn btn-primary delete-button">

+ 1 - 1
src/server/views/page_presentation.html

@@ -28,7 +28,7 @@
     <script src="{{ webpack_asset('js/legacy-presentation.js') }}" defer></script>
     <link rel="stylesheet" href="{{ webpack_asset('styles/style-presentation.css') }}">
 
-    <title>{{ path|path2name }} | {{ path }}</title>
+    <title>{{ page.path | path2name | preventXss }} | {{ page.path | preventXss }}</title>
 
     {{ cdnStyleTagsByGroup('basis') }}
     {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}

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

@@ -8,7 +8,7 @@
 </div>
 
 <div id="content-main" class="content-main page-list"
-  data-path="{{ path | preventXss }}"
+  data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
 
@@ -33,7 +33,7 @@
     <div class="pt-2 active tab-pane page-list-container" id="revision-body">
       {% if pages.length == 0 %}
         <div class="mt-2">
-          There are no pages under <strong>{{ path }}</strong>.
+          There are no pages under <strong>{{ path | preventXss }}</strong>.
         </div>
       {% endif  %}
 

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

@@ -8,7 +8,7 @@
 </div>
 
 <div id="content-main" class="content-main page-list"
-  data-path="{{ path | preventXss }}"
+  data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   >
 
@@ -25,7 +25,7 @@
     <div class="pt-2 active tab-pane page-list-container" id="revision-body">
       {% if pages.length == 0 %}
         <div class="mt-2">
-          There are no pages under <strong>{{ path }}</strong>.
+          There are no pages under <strong>{{ path | preventXss }}</strong>.
         </div>
       {% endif  %}
 

+ 13 - 4
src/server/views/widget/not_found_content.html

@@ -8,7 +8,7 @@
 </div>
 
 <div id="content-main" class="content-main page-list"
-  data-path="{{ path | preventXss }}"
+  data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   {% if templateTags %}
     data-template-tags="{{ templateTags }}"
@@ -18,22 +18,31 @@
   {% include 'not_found_tabs.html' %}
 
   <div class="tab-content">
+
+
+    {# TODO: should be removed and transplanted to PageContainer.initStateMarkdown ------ from here ------ #}
+
     {% if getConfig('crowi', 'customize:isEnabledAttachTitleHeader') %}
     {% if template %}
-    <script type="text/template" id="raw-text-original"># {{ path|path2name }}&NewLine;{{ template }}</script>
+    <script type="text/template" id="raw-text-original"># {{ path | path2name | preventXss }}&NewLine;{{ template }}</script>
     {% else %}
-    <script type="text/template" id="raw-text-original"># {{ path|path2name }}</script>
+    <script type="text/template" id="raw-text-original"># {{ path | path2name | preventXss }}</script>
     {% endif %}
     {% else %}
     {% if template %}
     <script type="text/template" id="raw-text-original">{{ template }}</script>
     {% endif %}
     {% endif %}
+
+    {# TODO: should be removed and transplanted to PageContainer.initStateMarkdown ------ to here ------ #}
+
+
+
     {# list view #}
     <div class="pt-2 active tab-pane page-list-container" id="revision-body">
       {% if pages.length == 0 %}
         <div class="mt-2">
-          There are no pages under <strong>{{ path }}</strong>.
+          There are no pages under <strong>{{ path | preventXss }}</strong>.
         </div>
       {% endif  %}
 

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

@@ -33,16 +33,17 @@
       <span>
         {% set fromPath = req.query.renamed or req.query.redirectFrom %}
         {% if redirectFrom or req.query.redirectFrom %}
-          <strong>{{ t('Redirected') }}:</strong> {{ t('page_page.notice.redirected', req.sanitize(fromPath)) }}
+          <strong>{{ t('Redirected') }}:</strong> {{ t('page_page.notice.redirected', fromPath | preventXss) }}
         {% endif %}
         {% if req.query.renamed %}
-          <strong>{{ t('Moved') }}:</strong> {{ t('page_page.notice.moved', req.sanitize(fromPath)) }}
+          <strong>{{ t('Moved') }}:</strong> {{ t('page_page.notice.moved', fromPath | preventXss) }}
         {% endif %}
       </span>
       {% if user and not page.isDeleted() %}
       <form role="form" id="unlink-page-form" onsubmit="return false;">
         <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        <input type="hidden" name="path" value="{{ path }}">
+        {# TODO: should be removed by GW-2283 #}
+        <input type="hidden" name="path" value="{{ page.path }}">
         <button type="submit" class="btn btn-outline-secondary btn-sm float-right">
           <i class="ti-unlink" aria-hidden="true"></i>
           Unlink
@@ -55,7 +56,7 @@
     {% if req.query.duplicated and not page.isDeleted() %}
     <div class="alert alert-success py-3 px-4">
       <span>
-        <strong>{{ t('Duplicated') }}: </strong> {{ t('page_page.notice.duplicated', req.sanitize(req.query.duplicated)) }}
+        <strong>{{ t('Duplicated') }}: </strong> {{ t('page_page.notice.duplicated', req.query.duplicated | preventXss) }}
       </span>
     </div>
     {% endif %}
@@ -69,7 +70,7 @@
     {% if page and not page.isLatestRevision() %}
     <div class="alert alert-warning">
       <strong>{{ t('Warning') }}: </strong> {{ t('page_page.notice.version') }}
-      <a href="{{ page.path }}"><i class="icon-fw icon-arrow-right-circle"></i>{{ t('Show latest') }}</a>
+      <a href="{{ encodeURI(page.path) }}"><i class="icon-fw icon-arrow-right-circle"></i>{{ t('Show latest') }}</a>
     </div>
     {% endif %}
 

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

@@ -1,6 +1,6 @@
 {% if page %}
 <div id="content-main" class="content-main"
-  data-path="{{ path }}"
+  data-path="{{ encodeURI(page.path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-page-id="{% if page %}{{ page._id.toString() }}{% endif %}"
   data-page-revision-id="{% if revision %}{{ revision._id.toString() }}{% endif %}"
@@ -18,7 +18,7 @@
   >
 {% else %}
 <div id="content-main" class="content-main"
-  data-path="{{ path }}"
+  data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   >

+ 8 - 5
src/server/views/widget/page_list.html

@@ -9,9 +9,8 @@
 
 <li>
   <img src="{{ page.lastUpdateUser|picture }}" class="picture rounded-circle">
-  <a href="{{ page.path }}"
-    class="text-break"
-    data-path="{{ page.path }}">{{ decodeURIComponent(page.path) }}
+  <a href="{{ encodeURI(page.path) }}" class="text-break ml-1">
+    {{ page.path | preventXss }}
   </a>
   <span class="page-list-meta">
     {% if page.isTopPage() %}
@@ -53,10 +52,14 @@
 {% if pager %}
 <ul class="pagination">
   {% if pager.prev !== null %}
-    <li class="prev"><a href="{{ path }}?offset={{ pager.prev }}"><i class="fa fa-arrow-left"></i> Prev</a></li>
+    <li class="prev">
+      <a href="{{ encodeURI(path) }}?offset={{ pager.prev }}" class="btn btn-outline-secondary"><i class="icon-arrow-left"></i> Prev</a>
+    </li>
   {% endif %}
   {% if pager.next %}
-    <li class="next"><a href="{{ path }}?offset={{ pager.next }}">Next <i class="fa fa-arrow-right"></i></a></li>
+    <li class="next">
+      <a href="{{ encodeURI(path) }}?offset={{ pager.next }}" class="btn btn-outline-secondary">Next <i class="icon-arrow-right"></i></a>
+    </li>
   {% endif %}
 </ul>
 {% endif %}

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

@@ -19,7 +19,7 @@
           {% if isTrashPage() %}
           No deleted pages.
           {% else %}
-          There are no pages under <strong>{{ path }}</strong>.
+          There are no pages under <strong>{{ path | preventXss }}</strong>.
           {% endif %}
         </div>
       {% else %}
@@ -30,7 +30,7 @@
     {# timeline view #}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
       <div class="tab-pane mt-5" id="view-timeline">
-        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) }}</script>
+        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) | preventXss }}</script>
         {# render React Component PageTimeline #}
         <div id="page-timeline"></div>
       </div>

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

@@ -18,7 +18,7 @@
           {% if isTrashPage() %}
           No deleted pages.
           {% else %}
-          There are no pages under <strong>{{ path }}</strong>.
+          There are no pages under <strong>{{ path | preventXss }}</strong>.
           {% endif %}
         </div>
       {% else %}
@@ -29,7 +29,7 @@
     {# timeline view #}
     {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
       <div class="tab-pane mt-5" id="view-timeline">
-        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) }}</script>
+        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) | preventXss }}</script>
         {# render React Component PageTimeline #}
         <div id="page-timeline"></div>
       </div>

+ 0 - 32
src/server/views/widget/pager.html

@@ -1,32 +0,0 @@
-<ul class="pagination">
-
-  <li {% if pager.page == 1 %}class="disabled"{% endif %}>
-    <a href="{{ path }}?page={{ pager.previous|default(1) }}">&laquo;</a>
-  </li>
-  {% if pager.previousDots %}
-    {% if pager.page !== 1 %}
-    <li>
-      <a href="{{ path }}?page=1">1</a>
-    </li>
-    {% endif %}
-  <li><a href="#">...</a></li>
-  {% endif  %}
-
-  {% for page in pager.pages %}
-  <li {% if pager.page == page %}class="active"{% endif %}>
-    <a href="{{ path }}?page={{ page }}">{{ page }}</a>
-  </li>
-  {% endfor %}
-
-  {% if pager.nextDots %}
-  <li><a href="#">...</a></li>
-    {% if pager.page !== pager.pagesCount %}
-    <li>
-      <a href="{{ path }}?page={{ pager.pagesCount }}">{{ pager.pagesCount }}</a>
-    </li>
-    {% endif %}
-  {% endif  %}
-  <li {% if pager.page == pager.pagesCount %}class="disabled"{% endif %}>
-    <a href="{{ path }}?page={{ pager.next|default(pager.pagesCount) }}">&raquo;</a>
-  </li>
-</ul>

+ 2 - 2
src/test/models/page.test.js

@@ -267,14 +267,14 @@ describe('Page', () => {
         expect(page.path).toEqual(expectedPage.path);
       });
 
-      test('should find page (just me)', async() => {
+      test('should find page (only me)', async() => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
       });
 
-      test('should not be found by grant (just me)', async() => {
+      test('should not be found by grant (only me)', async() => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         expect(page).toBeNull();

+ 0 - 15
yarn.lock

@@ -5707,13 +5707,6 @@ express-form@~0.12.0:
     object-additions "^0.5.1"
     validator "^2.1.0"
 
-express-sanitizer@^1.0.4:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/express-sanitizer/-/express-sanitizer-1.0.4.tgz#5331a12de6577582901a6581e91e38a8b99a6ee2"
-  dependencies:
-    sanitizer "0.1.3"
-    underscore "1.8.3"
-
 express-session@^1.16.1:
   version "1.16.1"
   resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.1.tgz#251ff9776c59382301de6c8c33411af357ed439c"
@@ -12664,10 +12657,6 @@ sane@^4.0.3:
     minimist "^1.1.1"
     walker "~1.0.5"
 
-sanitizer@0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
-
 saslprep@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.2.tgz#da5ab936e6ea0bbae911ffec77534be370c9f52d"
@@ -14426,10 +14415,6 @@ ultron@~1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
 
-underscore@1.8.3:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
-
 unherit@^1.0.4:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.2.tgz#14f1f397253ee4ec95cec167762e77df83678449"