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

Merge Merge official crowi v1.6.2

# Conflicts:
#	CHANGES.md
#	lib/crowi/express-init.js
#	lib/crowi/index.js
#	lib/models/config.js
#	lib/util/middlewares.js
#	npm-shrinkwrap.json
#	package.json
#	resource/js/components/Page/PageBody.js
#	resource/js/components/SearchPage/SearchResultList.js
#	resource/js/legacy/crowi-form.js
#	resource/js/legacy/crowi.js
#	resource/js/util/CrowiRenderer.js
Yuki Takei 8 лет назад
Родитель
Сommit
05274da9ce

+ 17 - 13
lib/crowi/express-init.js

@@ -79,19 +79,6 @@ module.exports = function(crowi, app) {
     next();
   });
 
-  // Set basic auth middleware
-  app.use(function(req, res, next) {
-    var config = crowi.getConfig();
-
-    if (config.crowi['security:basicName'] && config.crowi['security:basicSecret']) {
-      return basicAuth(
-        config.crowi['security:basicName'],
-        config.crowi['security:basicSecret'])(req, res, next);
-    } else {
-      next();
-    }
-  });
-
   app.set('port', crowi.port);
   app.use(express.static(crowi.publicDir));
   app.engine('html', cons.swig);
@@ -107,6 +94,23 @@ module.exports = function(crowi, app) {
   app.use(bodyParser.json({limit: '50mb'}));
   app.use(cookieParser());
   app.use(session(crowi.sessionConfig));
+
+  // Set basic auth middleware
+  app.use(function(req, res, next) {
+    var config = crowi.getConfig();
+    if (req.query.access_token || req.body.access_token) {
+      return next();
+    }
+
+    if (config.crowi['security:basicName'] && config.crowi['security:basicSecret']) {
+      return basicAuth(
+        config.crowi['security:basicName'],
+        config.crowi['security:basicSecret'])(req, res, next);
+    } else {
+      next();
+    }
+  });
+
   app.use(flash());
 
   app.use(middleware.swigFilters(app, swig));

+ 4 - 0
lib/crowi/index.js

@@ -118,6 +118,10 @@ Crowi.prototype.getConfig = function() {
   return this.config;
 };
 
+Crowi.prototype.getEnv = function() {
+  return this.env;
+};
+
 // getter/setter of model instance
 //
 Crowi.prototype.model = function(name, model) {

+ 5 - 0
lib/models/config.js

@@ -323,6 +323,7 @@ module.exports = function(crowi) {
   configSchema.statics.getLocalconfig = function(config)
   {
     const Config = this;
+    const env = crowi.getEnv();
 
     const local_config = {
       crowi: {
@@ -334,6 +335,10 @@ module.exports = function(crowi) {
         file: Config.fileUploadEnabled(config),
       },
       layoutType: Config.layoutType(config),
+      env: {
+        PLANTUML_URI: env.PLANTUML_URI || null,
+        MATHJAX: env.MATHJAX || null,
+      },
     };
 
     return local_config;

+ 14 - 15
lib/util/middlewares.js

@@ -223,26 +223,25 @@ exports.loginRequired = function(crowi, app, isStrictly = true) {
 
 exports.accessTokenParser = function(crowi, app) {
   return function(req, res, next) {
-    var accessToken = req.query.access_token || req.body.access_token || req.get('Authorization') || null;
-
-    debug(`accessToken=${accessToken}`);
-
+    // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
+    var accessToken = req.query.access_token || req.body.access_token || null;
     if (!accessToken) {
       return next();
     }
 
-    var User = crowi.model('User');
+    var User = crowi.model('User')
+
+    debug('accessToken is', accessToken);
     User.findUserByApiToken(accessToken)
-      .then((userData) => {
-        if (userData !== null) {
-          req.user = userData;
-          req.skipCsrfVerify = true;
-          debug('Access token parsed: skipCsrfVerify');
-        }
-        next();
-      }).catch(function(err) {
-        next();
-      });
+    .then(function(userData) {
+      req.user = userData;
+      req.skipCsrfVerify = true;
+      debug('Access token parsed: skipCsrfVerify');
+
+      next();
+    }).catch(function(err) {
+      next();
+    });
   };
 };
 

+ 3 - 1
package.json

@@ -88,7 +88,8 @@
     "i18next-express-middleware": "~1.0.2",
     "i18next-node-fs-backend": "~0.1.3",
     "i18next-sprintf-postprocessor": "~0.2.2",
-    "inline-attachment": "git+https://github.com/Rovak/InlineAttachment.git#2.0.3",
+    "inline-attachment": "~2.0.3",
+    "jquery": "~2.1.4",
     "jquery.cookie": "~1.4.1",
     "marked": "~0.3.6",
     "md5": "^2.2.1",
@@ -104,6 +105,7 @@
     "nodemailer-ses-transport": "~1.5.0",
     "normalize-path": "^2.1.1",
     "optimize-js-plugin": "0.0.4",
+    "plantuml-encoder": "^1.2.4",
     "react": "^15.5.0",
     "react-bootstrap": "^0.31.0",
     "react-bootstrap-typeahead": "^1.3.0",

+ 4 - 0
resource/css/_wiki.scss

@@ -132,6 +132,10 @@ div.body {
     border: solid 1px #ccc;
     max-width: 100%;
   }
+  .noborder img, .img.noborder {
+    box-shadow: none;
+    border: none;
+  }
 
   img.emoji {
     width: 0.95em;

+ 1 - 1
resource/js/app.js

@@ -46,7 +46,7 @@ window.crowi = crowi;
 crowi.setConfig(JSON.parse(document.getElementById('crowi-context-hydrate').textContent || '{}'));
 crowi.fetchUsers();
 
-const crowiRenderer = new CrowiRenderer();
+const crowiRenderer = new CrowiRenderer(crowi);
 window.crowiRenderer = crowiRenderer;
 
 // FIXME

+ 27 - 2
resource/js/components/Page/PageBody.js

@@ -8,6 +8,24 @@ export default class PageBody extends React.Component {
 
     this.crowiRenderer = window.crowiRenderer; // FIXME
     this.getMarkupHTML = this.getMarkupHTML.bind(this);
+    this.getHighlightBody = this.getHighlightBody.bind(this);
+  }
+
+  getHighlightBody(body, keywords) {
+    let returnBody = body;
+
+    keywords.replace(/"/g, '').split(' ').forEach((keyword) => {
+      if (keyword === '') {
+        return;
+      }
+      const k = keyword
+            .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
+            .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
+      const keywordExp = new RegExp(`(${k}(?!(.*?")))`, 'ig');
+      returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
+    });
+
+    return returnBody;
   }
 
   getMarkupHTML() {
@@ -16,11 +34,17 @@ export default class PageBody extends React.Component {
       body = this.props.page.revision.body;
     }
 
-    return { __html: this.crowiRenderer.render(body, this.props.rendererOptions) };
+    body = this.crowiRenderer.render(body, undefined, this.props.rendererOptions);
+
+    if (this.props.highlightKeywords) {
+      body = this.getHighlightBody(body, this.props.highlightKeywords);
+    }
+
+    return { __html: body };
   }
 
   render() {
-    const parsedBody = this.getMarkupHTML();
+    let parsedBody = this.getMarkupHTML();
 
     return (
       <div
@@ -33,6 +57,7 @@ export default class PageBody extends React.Component {
 
 PageBody.propTypes = {
   page: PropTypes.object.isRequired,
+  highlightKeywords: PropTypes.string,
   pageBody: PropTypes.string,
   rendererOptions: React.PropTypes.object,
 };

+ 8 - 22
resource/js/components/SearchPage/SearchResultList.js

@@ -7,26 +7,6 @@ export default class SearchResultList extends React.Component {
 
   constructor(props) {
     super(props);
-
-    this.getHighlightBody = this.getHighlightBody.bind(this);
-  }
-
-  getHighlightBody(body) {
-    let returnBody = body;
-
-    this.props.searchingKeyword.split(' ').forEach((keyword) => {
-      if (keyword === '') {
-        return;
-      }
-      const k = keyword
-            .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
-            .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-      const keywordExp = new RegExp(`(${k}(?!(.*?\]|.*?\\)|.*?"|.*?>)))`, 'ig');
-      returnBody = returnBody.replace(keywordExp, '<em class="highlighted">$&</em>');
-    });
-
-    //console.log(this.props.searchingKeyword, body);
-    return returnBody;
   }
 
   render() {
@@ -41,12 +21,18 @@ export default class SearchResultList extends React.Component {
     };
 
     const resultList = this.props.pages.map((page) => {
-      const pageBody = this.getHighlightBody(page.revision.body);
+      const pageBody = page.revision.body;
       return (
         <div id={page._id} key={page._id} className="search-result-page">
           <h2><a href={page.path}>{page.path}</a></h2>
           <div className="wiki">
-            <PageBody className="hige" page={page} pageBody={pageBody} rendererOptions={rendererOptions} />
+            <PageBody
+              className="hige"
+              page={page}
+              pageBody={pageBody}
+              highlightKeywords={this.props.searchingKeyword}
+              rendererOptions={rendererOptions}
+            />
           </div>
         </div>
       );

+ 4 - 2
resource/js/legacy/crowi-form.js

@@ -76,21 +76,23 @@ $(function() {
 
   function renderPreview() {
     var markdown = $('#form-body').val();
+    var dom = $('#preview-body');
 
     // create context object
     var context = {
       markdown,
+      dom,
       currentPagePath: decodeURIComponent(location.pathname)
     };
 
     crowi.interceptorManager.process('preRenderPreview', context)
       .then(() => crowi.interceptorManager.process('prePreProcess', context))
       .then(() => {
-        context.markdown = crowiRenderer.preProcess(context.markdown);
+        context.markdown = crowiRenderer.preProcess(context.markdown, context.dom);
       })
       .then(() => crowi.interceptorManager.process('postPreProcess', context))
       .then(() => {
-        var parsedHTML = crowiRenderer.render(context.markdown, rendererOptions);
+        var parsedHTML = crowiRenderer.render(context.markdown, context.dom, rendererOptions);
         context['parsedHTML'] = parsedHTML;
       })
       .then(() => crowi.interceptorManager.process('postRenderPreview', context))

+ 8 - 4
resource/js/legacy/crowi.js

@@ -366,11 +366,12 @@ $(function() {
         var id = $(this).attr('id');
         var contentId = '#' + id + ' > script';
         var revisionBody = '#' + id + ' .revision-body';
+        var $revisionBody = $(revisionBody);
         var revisionPath = '#' + id + ' .revision-path';
 
         var markdown = Crowi.unescape($(contentId).html());
-        var parsedHTML = crowiRenderer.render(markdown, rendererOptions);
-        $(revisionBody).html(parsedHTML);
+        var parsedHTML = crowiRenderer.render(markdown, $revisionBody.get(0), rendererOptions);
+        $revisionBody.html(parsedHTML);
 
         $('.template-create-button', revisionBody).on('click', function() {
           var path = $(this).data('path');
@@ -415,21 +416,24 @@ $(function() {
     var $rawTextOriginal = $('#raw-text-original');
     if ($rawTextOriginal.length > 0) {
       var markdown = Crowi.unescape($('#raw-text-original').html());
+      var dom = $('#revision-body-content').get(0);
 
       // create context object
       var context = {
         markdown,
+        dom,
         currentPagePath: decodeURIComponent(location.pathname)
       };
 
       crowi.interceptorManager.process('preRender', context)
         .then(() => crowi.interceptorManager.process('prePreProcess', context))
         .then(() => {
-          context.markdown = crowiRenderer.preProcess(context.markdown);
+          context.markdown = crowiRenderer.preProcess(context.markdown, context.dom);
         })
         .then(() => crowi.interceptorManager.process('postPreProcess', context))
         .then(() => {
-          var parsedHTML = crowiRenderer.render(context.markdown, rendererOptions);
+          var revisionBody = $('#revision-body-content');
+          var parsedHTML = crowiRenderer.render(context.markdown, context.dom, rendererOptions);
           context.parsedHTML = parsedHTML;
           Promise.resolve(context);
         })

+ 1 - 0
resource/js/util/Crowi.js

@@ -15,6 +15,7 @@ export default class Crowi {
     this.config = {};
     this.csrfToken = context.csrfToken;
 
+    this.window = window;
     this.location = window.location || {};
     this.document = window.document || {};
     this.localStorage = window.localStorage || {};

+ 20 - 15
resource/js/util/CrowiRenderer.js

@@ -6,50 +6,55 @@ import Linker        from './PreProcessor/Linker';
 import ImageExpander from './PreProcessor/ImageExpander';
 
 import Emoji         from './PostProcessor/Emoji';
+import Mathjax       from './PostProcessor/Mathjax';
 
 import Tsv2Table from './LangProcessor/Tsv2Table';
 import Template from './LangProcessor/Template';
+import PlantUML from './LangProcessor/PlantUML';
 
 export default class CrowiRenderer {
 
 
-  constructor(plugins) {
+  constructor(crowi) {
+    this.crowi = crowi;
 
     this.preProcessors = [
-      new MarkdownFixer(),
-      new Linker(),
-      new ImageExpander(),
+      new MarkdownFixer(crowi),
+      new Linker(crowi),
+      new ImageExpander(crowi),
     ];
     this.postProcessors = [
-      new Emoji(),
+      new Emoji(crowi),
+      new Mathjax(crowi),
     ];
 
     this.langProcessors = {
-      'tsv': new Tsv2Table(),
-      'tsv-h': new Tsv2Table({header: true}),
-      'template': new Template(),
+      'tsv': new Tsv2Table(crowi),
+      'tsv-h': new Tsv2Table(crowi, {header: true}),
+      'template': new Template(crowi),
+      'plantuml': new PlantUML(crowi),
     };
 
     this.parseMarkdown = this.parseMarkdown.bind(this);
     this.codeRenderer = this.codeRenderer.bind(this);
   }
 
-  preProcess(markdown) {
+  preProcess(markdown, dom) {
     for (let i = 0; i < this.preProcessors.length; i++) {
       if (!this.preProcessors[i].process) {
         continue;
       }
-      markdown = this.preProcessors[i].process(markdown);
+      markdown = this.preProcessors[i].process(markdown, dom);
     }
     return markdown;
   }
 
-  postProcess(html) {
+  postProcess(html, dom) {
     for (let i = 0; i < this.postProcessors.length; i++) {
       if (!this.postProcessors[i].process) {
         continue;
       }
-      html = this.postProcessors[i].process(html);
+      html = this.postProcessors[i].process(html, dom);
     }
 
     return html;
@@ -88,7 +93,7 @@ export default class CrowiRenderer {
 
   }
 
-  parseMarkdown(markdown, markedOpts) {
+  parseMarkdown(markdown, dom, markedOpts) {
     let parsed = '';
 
     const markedRenderer = new marked.Renderer();
@@ -142,10 +147,10 @@ export default class CrowiRenderer {
    *
    * @memberOf CrowiRenderer
    */
-  render(markdown, rendererOptions) {
+  render(markdown, dom, rendererOptions) {
     let html = '';
 
-    html = this.parseMarkdown(markdown, rendererOptions.marked || {});
+    html = this.parseMarkdown(markdown, dom, rendererOptions.marked || {});
     html = this.postProcess(html);
 
     return html;

+ 42 - 0
resource/js/util/LangProcessor/PlantUML.js

@@ -0,0 +1,42 @@
+import plantuml from 'plantuml-encoder';
+import crypto from 'crypto';
+
+export default class PlantUML {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+
+  }
+
+  generateId(token) {
+    const hasher = require('crypto').createHash('md5');
+    hasher.update(token);
+    return hasher.digest('hex');
+  }
+
+  process(code, lang) {
+    const config = crowi.getConfig();
+    if (!config.env.PLANTUML_URI) {
+      return `<pre class="wiki-code"><code>${Crowi.escape(code, true)}\n</code></pre>`;
+    }
+
+    let plantumlUri = config.env.PLANTUML_URI;
+    if (plantumlUri.substr(-1) !== '/') {
+      plantumlUri += '/';
+    }
+    const id = this.generateId(code + lang);
+    const encoded = plantuml.encode(`@startuml
+
+skinparam monochrome true
+
+${code}
+@enduml`);
+
+    return `
+      <div id="${id}" class="plantuml noborder">
+        <img src="${plantumlUri}svg/${encoded}">
+      </div>
+    `;
+  }
+}
+

+ 1 - 1
resource/js/util/LangProcessor/Template.js

@@ -2,7 +2,7 @@ import moment from 'moment';
 
 export default class Template {
 
-  constructor() {
+  constructor(crowi) {
     this.templatePattern = {
       'year': this.getYear,
       'month': this.getMonth,

+ 1 - 1
resource/js/util/LangProcessor/Tsv2Table.js

@@ -1,7 +1,7 @@
 
 export default class Tsv2Table {
 
-  constructor(option) {
+  constructor(crowi, option) {
     if (!option) {
       option = {};
     }

+ 66 - 0
resource/js/util/PostProcessor/Mathjax.js

@@ -0,0 +1,66 @@
+
+export default class Mathjax {
+
+  constructor(crowi) {
+    this.crowi = crowi;
+    this.defaultUrl = '//cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?skipStartupTypeset=true';
+
+    this.mathJaxConfigured = false;
+
+    const config = crowi.getConfig();
+
+    if (config.env.MATHJAX) {
+      this.mathJaxConfigured = true;
+
+      if (crowi.window.MathJax) {
+        return ;
+      }
+
+      const document = crowi.document;
+      const head = document.getElementsByTagName('head')[0];
+
+      const mathJaxConfig= document.createElement('script');
+      mathJaxConfig.type = 'text/x-mathjax-config';
+      mathJaxConfig.text = `MathJax.Hub.Config({
+      extensions: ["tex2jax.js"],
+      jax: ["input/TeX", "output/SVG"],
+      tex2jax: {
+        inlineMath: [ ['$','$'], ["\\(","\\)"] ],
+        displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
+        processEscapes: true
+      },
+      showMathMenu: false,
+      showMathMenuMSIE: false,
+      showProcessingMessages: false,
+      messageStyle: "none",
+      skipStartupTypeset: true
+    });`;
+      head.appendChild(mathJaxConfig);
+
+      const script = document.createElement('script');
+      script.type = 'text/javascript';
+      script.src = this.defaultUrl;
+
+      head.appendChild(script);
+    }
+
+    this.process = this.process.bind(this);
+  }
+
+  process(html, dom) {
+    if (!this.mathJaxConfigured) {
+      return html;
+    }
+
+    const intervalId = setInterval(() => {
+      if (this.crowi.window.MathJax) {
+        const MathJax = this.crowi.window.MathJax;
+
+        MathJax.Hub.Queue(["Typeset", MathJax.Hub, dom.id]);
+        clearInterval(intervalId);
+      }
+    }, 100);
+
+    return html;
+  }
+}

+ 21 - 2
yarn.lock

@@ -2915,9 +2915,9 @@ ini@~1.3.0:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
 
-"inline-attachment@git+https://github.com/Rovak/InlineAttachment.git#2.0.3":
+inline-attachment@~2.0.3:
   version "2.0.3"
-  resolved "git+https://github.com/Rovak/InlineAttachment.git#3aeb84a42976e88eaa030ed94f1cfd11b018b134"
+  resolved "https://registry.yarnpkg.com/inline-attachment/-/inline-attachment-2.0.3.tgz#5ee32374583fabd3b7206df2e20f251ba20c4306"
 
 interpret@^1.0.0:
   version "1.0.3"
@@ -3130,6 +3130,10 @@ jquery.cookie@~1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jquery.cookie/-/jquery.cookie-1.4.1.tgz#d63dce209eab691fe63316db08ca9e47e0f9385b"
 
+jquery@~2.1.4:
+  version "2.1.4"
+  resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.1.4.tgz#228bde698a0c61431dc2630a6a154f15890d2317"
+
 js-base64@^2.1.8, js-base64@^2.1.9:
   version "2.1.9"
   resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.1.9.tgz#f0e80ae039a4bd654b5f281fc93f04a914a7fcce"
@@ -4290,6 +4294,10 @@ osenv@0, osenv@^0.1.4:
     os-homedir "^1.0.0"
     os-tmpdir "^1.0.0"
 
+pako@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf"
+
 pako@~0.2.0:
   version "0.2.9"
   resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@@ -4407,6 +4415,13 @@ pkg-dir@^1.0.0:
   dependencies:
     find-up "^1.0.0"
 
+plantuml-encoder@^1.2.4:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/plantuml-encoder/-/plantuml-encoder-1.2.4.tgz#5f0056f7c04bd76aeef420bfcddef339e9f44081"
+  dependencies:
+    pako "1.0.3"
+    utf8-bytes "0.0.1"
+
 postcss-calc@^5.2.0:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
@@ -5912,6 +5927,10 @@ url@^0.11.0:
     punycode "1.3.2"
     querystring "0.2.0"
 
+utf8-bytes@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/utf8-bytes/-/utf8-bytes-0.0.1.tgz#116b025448c9b500081cdfbf1f4d6c6c37d8837d"
+
 util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"