Răsfoiți Sursa

Merge pull request #387 from weseek/master

release v3.0.12
Yuki Takei 8 ani în urmă
părinte
comite
fa44baa934

+ 14 - 2
.eslintrc.js

@@ -5,7 +5,10 @@ module.exports = {
     "es6": true,
     "node": true
   },
-  "extends": "eslint:recommended",
+  "extends": [
+    "eslint:recommended",
+    "plugin:react/recommended"
+  ],
   "parserOptions": {
     "ecmaFeatures": {
       "experimentalObjectRestSpread": true,
@@ -32,7 +35,11 @@ module.exports = {
     "indent": [
       "error",
       2,
-      { "SwitchCase": 1 }
+      {
+        "SwitchCase": 1,
+        "FunctionExpression": {"parameters": 2},
+        "CallExpression": {"parameters": 2}
+      }
     ],
     "key-spacing": [
       "error", { "beforeColon": false, "afterColon": true }
@@ -48,6 +55,11 @@ module.exports = {
       "error",
       "single"
     ],
+    "react/jsx-indent": [
+      "error",
+      4,
+      { "ignoredNodes": ["JSXElement *"] }
+    ],
     "semi": [
       "error",
       "always"

+ 1 - 0
.gitignore

@@ -14,6 +14,7 @@ Thumbs.db
 /bower_components/
 npm-debug.log
 /npm-debug.log.*
+package-lock.json
 
 # Dist #
 /report/

+ 10 - 1
CHANGES.md

@@ -1,9 +1,18 @@
 CHANGES
 ========
 
-## 3.0.11-RC
+## 3.0.12-RC
+
+* Feature: Support Vim/Emacs/Sublime-Text keybindings
+* Improvement: Dynamic loading for CodeMirror theme files from CDN
+* Improvement: Add some CodeMirror themes (Eclipse, Dracula)
+
+
+## 3.0.11
 
 * Fix: login.html is broken in iOS
+* Fix: Removing attachment is crashed
+* Fix: File-attaching error after new page creation
 * Support: Optimize development build
 * Support: Upgrade libs
     * env-cmd

+ 2 - 0
lib/crowi/express-init.js

@@ -9,6 +9,7 @@ module.exports = function(crowi, app) {
     , methodOverride = require('method-override')
     , passport       = require('passport')
     , session        = require('express-session')
+    , sanitizer      = require('express-sanitizer')
     , basicAuth      = require('basic-auth-connect')
     , flash          = require('connect-flash')
     , swig           = require('swig-templates')
@@ -94,6 +95,7 @@ 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());
   app.use(session(crowi.sessionConfig));
 

+ 3 - 3
lib/views/widget/page_alerts.html

@@ -29,7 +29,7 @@
     {% if req.query.renamed and not page.isDeleted() %}
     <div class="alert alert-info alert-moved">
       <span>
-        <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.query.renamed) }}
+        <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.sanitize(req.query.renamed)) }}
       </span>
     </div>
     {% endif %}
@@ -37,7 +37,7 @@
     {% if req.query.redirectFrom and not page.isDeleted() %}
     <div class="alert alert-info alert-moved d-flex align-items-center justify-content-between">
       <span>
-        <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.query.redirectFrom) }}
+        <strong>{{ t('Moved') }}: </strong> {{ t('page_page.notice.moved', req.sanitize(req.query.redirectFrom)) }}
       </span>
       {% if user %}
       <form role="form" id="unlink-page-form" onsubmit="return false;">
@@ -56,7 +56,7 @@
     {% if req.query.duplicated and not page.isDeleted() %}
     <div class="alert alert-success alert-moved">
       <span>
-        <strong>{{ t('Duplicated') }}: </strong> {{ t('page_page.notice.duplicated', req.query.duplicated) }}
+        <strong>{{ t('Duplicated') }}: </strong> {{ t('page_page.notice.duplicated', req.sanitize(req.query.duplicated)) }}
       </span>
     </div>
     {% endif %}

+ 4 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.0.11-RC",
+  "version": "3.0.12-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -83,6 +83,7 @@
     "express": "^4.16.1",
     "express-form": "~0.12.0",
     "express-pino-logger": "^3.0.1",
+    "express-sanitizer": "^1.0.4",
     "express-session": "~1.15.0",
     "express-webpack-assets": "^0.1.0",
     "extract-text-webpack-plugin": "^3.0.2",
@@ -97,6 +98,7 @@
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
+    "load-css-file": "^1.0.0",
     "markdown-it": "^8.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
@@ -136,6 +138,7 @@
     "reveal.js": "^3.5.0",
     "rimraf": "^2.6.1",
     "sass-loader": "^7.0.1",
+    "simple-load-script": "^1.0.2",
     "slack-node": "^0.1.8",
     "socket.io": "^2.0.3",
     "socket.io-client": "^2.0.3",

+ 0 - 2
resource/js/components/Admin/CustomCssEditor.js

@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
-require('codemirror/lib/codemirror.css');
 require('codemirror/addon/display/autorefresh');
 require('codemirror/addon/lint/css-lint');
 require('codemirror/addon/hint/css-hint');
@@ -10,7 +9,6 @@ require('codemirror/addon/hint/show-hint');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/closebrackets');
 require('codemirror/mode/css/css');
-require('codemirror/theme/eclipse.css');
 
 require('jquery-ui/ui/widgets/resizable');
 

+ 0 - 2
resource/js/components/Admin/CustomHeaderEditor.js

@@ -2,13 +2,11 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
-require('codemirror/lib/codemirror.css');
 require('codemirror/addon/display/autorefresh');
 require('codemirror/addon/hint/show-hint');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/closebrackets');
 require('codemirror/mode/htmlmixed/htmlmixed');
-require('codemirror/theme/eclipse.css');
 
 require('jquery-ui/ui/widgets/resizable');
 

+ 0 - 2
resource/js/components/Admin/CustomScriptEditor.js

@@ -2,7 +2,6 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { UnControlled as CodeMirror } from 'react-codemirror2';
-require('codemirror/lib/codemirror.css');
 require('codemirror/addon/display/autorefresh');
 require('codemirror/addon/lint/javascript-lint');
 require('codemirror/addon/hint/javascript-hint');
@@ -10,7 +9,6 @@ require('codemirror/addon/hint/show-hint');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/closebrackets');
 require('codemirror/mode/javascript/javascript');
-require('codemirror/theme/eclipse.css');
 
 require('jquery-ui/ui/widgets/resizable');
 

+ 100 - 17
resource/js/components/PageEditor/Editor.js

@@ -1,10 +1,13 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import urljoin from 'url-join';
+const loadScript = require('simple-load-script');
+const loadCssSync = require('load-css-file');
+
 import * as codemirror from 'codemirror';
 
 import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
-require('codemirror/lib/codemirror.css');
 require('codemirror/addon/display/autorefresh');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
@@ -23,13 +26,6 @@ require('codemirror/addon/fold/markdown-fold');
 require('codemirror/addon/fold/brace-fold');
 require('codemirror/mode/gfm/gfm');
 
-require('codemirror/theme/elegant.css');
-require('codemirror/theme/neo.css');
-require('codemirror/theme/mdn-like.css');
-require('codemirror/theme/material.css');
-require('codemirror/theme/monokai.css');
-require('codemirror/theme/twilight.css');
-
 
 import Dropzone from 'react-dropzone';
 
@@ -46,8 +42,7 @@ export default class Editor extends React.Component {
   constructor(props) {
     super(props);
 
-    // https://regex101.com/r/7BN2fR/2
-    this.indentAndMarkPattern = /^([ \t]*)(?:>|\-|\+|\*|\d+\.) /;
+    this.cmCdnRoot = 'https://cdn.jsdelivr.net/npm/codemirror@5.37.0';
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
@@ -61,9 +56,15 @@ export default class Editor extends React.Component {
       isUploading: false,
     };
 
+    this.loadedThemeSet = new Set(['eclipse', 'elegant']);   // themes imported in _vendor.scss
+    this.loadedKeymapSet = new Set();
+
     this.getCodeMirror = this.getCodeMirror.bind(this);
     this.setCaretLine = this.setCaretLine.bind(this);
     this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
+    this.loadTheme = this.loadTheme.bind(this);
+    this.loadKeymapMode = this.loadKeymapMode.bind(this);
+    this.setKeymapMode = this.setKeymapMode.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
     this.handleEnterKey = this.handleEnterKey.bind(this);
@@ -80,17 +81,39 @@ export default class Editor extends React.Component {
     this.renderOverlay = this.renderOverlay.bind(this);
   }
 
+
   componentDidMount() {
     // initialize caret line
     this.setCaretLine(0);
     // set save handler
     codemirror.commands.save = this.dispatchSave;
+
+    // set CodeMirror instance as 'CodeMirror' so that CDN addons can reference
+    window.CodeMirror = require('codemirror');
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // load theme
+    const theme = nextProps.editorOptions.theme;
+    this.loadTheme(theme);
+
+    // set keymap
+    const prevKeymapMode = this.props.editorOptions.keymapMode;
+    const keymapMode = nextProps.editorOptions.keymapMode;
+    this.setKeymapMode(keymapMode);
   }
 
   getCodeMirror() {
     return this.refs.cm.editor;
   }
 
+  loadCss(source) {
+    return new Promise((resolve) => {
+      loadCssSync(source);
+      resolve();
+    });
+  }
+
   forceToFocus() {
     const editor = this.getCodeMirror();
     // use setInterval with reluctance -- 2018.01.11 Yuki Takei
@@ -133,6 +156,66 @@ export default class Editor extends React.Component {
     editor.scrollTo(null, top);
   }
 
+  /**
+   * load Theme
+   * @see https://codemirror.net/doc/manual.html#config
+   *
+   * @param {string} theme
+   */
+  loadTheme(theme) {
+    // load theme
+    let cssList = [];
+    if (!this.loadedThemeSet.has(theme)) {
+      this.loadCss(urljoin(this.cmCdnRoot, `theme/${theme}.min.css`));
+
+      // update Set
+      this.loadedThemeSet.add(theme);
+    }
+  }
+
+  /**
+   * load assets for Key Maps
+   * @param {*} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  loadKeymapMode(keymapMode) {
+    const loadCss = this.loadCss;
+    let scriptList = [];
+    let cssList = [];
+
+    // add dependencies
+    if (this.loadedKeymapSet.size == 0) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.js')));
+      cssList.push(loadCss(urljoin(this.cmCdnRoot, 'addon/dialog/dialog.min.css')));
+    }
+    // load keymap
+    if (!this.loadedKeymapSet.has(keymapMode)) {
+      scriptList.push(loadScript(urljoin(this.cmCdnRoot, `keymap/${keymapMode}.min.js`)));
+      // update Set
+      this.loadedKeymapSet.add(keymapMode);
+    }
+
+    return Promise.all(scriptList.concat(cssList));
+  }
+
+  /**
+   * set Key Maps
+   * @see https://codemirror.net/doc/manual.html#keymaps
+   *
+   * @param {string} keymapMode 'default' or 'vim' or 'emacs' or 'sublime'
+   */
+  setKeymapMode(keymapMode) {
+    if (!keymapMode.match(/^(vim|emacs|sublime)$/)) {
+      // reset
+      this.getCodeMirror().setOption('keyMap', 'default');
+      return;
+    }
+
+    this.loadKeymapMode(keymapMode)
+    .then(() => {
+      this.getCodeMirror().setOption('keyMap', keymapMode);
+    });
+  }
+
   /**
    * remove overlay and set isUploading to false
    */
@@ -321,7 +404,7 @@ export default class Editor extends React.Component {
       height: '100%',
       display: 'flex',
       flexDirection: 'column',
-    }
+    };
 
     const theme = this.props.editorOptions.theme || 'elegant';
     const styleActiveLine = this.props.editorOptions.styleActiveLine || undefined;
@@ -363,17 +446,17 @@ export default class Editor extends React.Component {
               matchTags: {bothTags: true},
               // folding
               foldGutter: true,
-              gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+              gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
               // match-highlighter, matchesonscrollbar, annotatescrollbar options
               highlightSelectionMatches: {annotateScrollbar: true},
               // markdown mode options
               highlightFormatting: true,
               // continuelist, indentlist
               extraKeys: {
-                "Enter": this.handleEnterKey,
-                "Tab": "indentMore",
-                "Shift-Tab": "indentLess",
-                "Ctrl-Q": (cm) => { cm.foldCode(cm.getCursor()) },
+                'Enter': this.handleEnterKey,
+                'Tab': 'indentMore',
+                'Shift-Tab': 'indentLess',
+                'Ctrl-Q': (cm) => { cm.foldCode(cm.getCursor()) },
               }
             }}
             onScroll={(editor, data) => {
@@ -405,7 +488,7 @@ export default class Editor extends React.Component {
           or pasting from the clipboard.
         </button>
       </div>
-    )
+    );
   }
 
 }

+ 44 - 2
resource/js/components/PageEditor/OptionsSelector.js

@@ -28,10 +28,17 @@ export default class OptionsSelector extends React.Component {
     }
 
     this.availableThemes = [
-      'elegant', 'neo', 'mdn-like', 'material', 'monokai', 'twilight'
-    ]
+      'eclipse', 'elegant', 'neo', 'mdn-like', 'material', 'dracula', 'monokai', 'twilight'
+    ];
+    this.keymapModes = {
+      default: 'Default',
+      vim: 'Vim',
+      emacs: 'Emacs',
+      sublime: 'Sublime Text',
+    }
 
     this.onChangeTheme = this.onChangeTheme.bind(this);
+    this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
     this.onClickStyleActiveLine = this.onClickStyleActiveLine.bind(this);
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
@@ -43,6 +50,7 @@ export default class OptionsSelector extends React.Component {
 
   init() {
     this.themeSelectorInputEl.value = this.state.editorOptions.theme;
+    this.keymapModeSelectorInputEl.value = this.state.editorOptions.keymapMode;
   }
 
   onChangeTheme() {
@@ -54,6 +62,15 @@ export default class OptionsSelector extends React.Component {
     this.dispatchOnChange();
   }
 
+  onChangeKeymapMode() {
+    const newValue = this.keymapModeSelectorInputEl.value;
+    const newOpts = Object.assign(this.state.editorOptions, {keymapMode: newValue});
+    this.setState({editorOptions: newOpts});
+
+    // dispatch event
+    this.dispatchOnChange();
+  }
+
   onClickStyleActiveLine(event) {
     // keep dropdown opened
     this._cddForceOpen = true;
@@ -121,6 +138,29 @@ export default class OptionsSelector extends React.Component {
     )
   }
 
+  renderKeymapModeSelector() {
+    const optionElems = [];
+    for (let mode in this.keymapModes) {
+      const label = this.keymapModes[mode];
+      optionElems.push(<option key={mode} value={mode}>{label}</option>);
+    }
+
+    const bsClassName = 'form-control-dummy'; // set form-control* to shrink width
+
+    return (
+      <FormGroup controlId="formControlsSelect">
+        <ControlLabel>Mode:</ControlLabel>
+        <FormControl componentClass="select" placeholder="select" bsClass={bsClassName} className="btn-group-sm selectpicker"
+            onChange={this.onChangeKeymapMode}
+            inputRef={ el => this.keymapModeSelectorInputEl=el }>
+
+          {optionElems}
+
+        </FormControl>
+      </FormGroup>
+    )
+  }
+
   renderConfigurationDropdown() {
     return (
       <FormGroup controlId="formControlsSelect">
@@ -188,6 +228,7 @@ export default class OptionsSelector extends React.Component {
   render() {
     return <span>
       <span className="m-l-5">{this.renderThemeSelector()}</span>
+      <span className="m-l-5">{this.renderKeymapModeSelector()}</span>
       <span className="m-l-5">{this.renderConfigurationDropdown()}</span>
     </span>
   }
@@ -196,6 +237,7 @@ export default class OptionsSelector extends React.Component {
 export class EditorOptions {
   constructor(props) {
     this.theme = 'elegant';
+    this.keymapMode = 'default';
     this.styleActiveLine = false;
 
     Object.assign(this, props);

+ 5 - 0
resource/styles/scss/_vendor.scss

@@ -13,3 +13,8 @@ $bootstrap-sass-asset-helper: true;
 
 // import bootstrap-select styles
 @import '~bootstrap-select/sass/bootstrap-select';
+
+// import CodeMirror styles
+@import '~codemirror/lib/codemirror.css';
+@import '~codemirror/theme/elegant.css';
+@import '~codemirror/theme/eclipse.css';

+ 23 - 0
yarn.lock

@@ -2694,6 +2694,13 @@ express-pino-logger@^3.0.1:
   dependencies:
     pino-http "^3.0.1"
 
+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.15.0:
   version "1.15.6"
   resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.6.tgz#47b4160c88f42ab70fe8a508e31cbff76757ab0a"
@@ -4039,6 +4046,10 @@ linkify-it@^2.0.0:
   dependencies:
     uc.micro "^1.0.1"
 
+load-css-file@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/load-css-file/-/load-css-file-1.0.0.tgz#dac097ead6470f4c3f23d4bc5b9ff2c3decb212f"
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -6494,6 +6505,10 @@ samsam@1.3.0, samsam@1.x:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50"
 
+sanitizer@0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
+
 sass-graph@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
@@ -6693,6 +6708,10 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
 
+simple-load-script@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/simple-load-script/-/simple-load-script-1.0.2.tgz#d92951fe7b601ad90af8c9429bd4b2ee127ab8a3"
+
 sinon-chai@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.0.0.tgz#d5cbd70fa71031edd96b528e0eed4038fcc99f29"
@@ -7300,6 +7319,10 @@ uncontrollable@^4.1.0:
   dependencies:
     invariant "^2.1.0"
 
+underscore@1.8.3:
+  version "1.8.3"
+  resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
+
 uniq@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"