diff --git a/app/views/common/markdown.scala.html b/app/views/common/markdown.scala.html index d174c5bb2..9688e31bd 100644 --- a/app/views/common/markdown.scala.html +++ b/app/views/common/markdown.scala.html @@ -34,6 +34,7 @@ htOptions.sIssuesUrl = "@routes.IssueApp.issues(project.owner, project.name)"; htOptions.sProjectUrl = "@routes.ProjectApp.project(project.owner, project.name)"; htOptions.sApplicationContext = "@play.Configuration.root().getString("application.context")"; + htOptions.bNoReferrer = "@play.Configuration.root().getString("application.noreferrer")" } // Reusable markdown renderer diff --git a/public/javascripts/common/yobi.Markdown.js b/public/javascripts/common/yobi.Markdown.js index ed1d77709..4fe026a40 100644 --- a/public/javascripts/common/yobi.Markdown.js +++ b/public/javascripts/common/yobi.Markdown.js @@ -31,6 +31,7 @@ yobi.Markdown = (function(htOptions){ _initVar(htOptions); _enableMarkdown(htOptions.aTarget); + _initializeMarkdownRenderer(); } /** @@ -42,11 +43,13 @@ yobi.Markdown = (function(htOptions){ htVar.sProjectUrl = htOptions.sProjectUrl; htVar.bBreaks = htOptions.bBreaks; htVar.sApplicationContext = htOptions.sApplicationContext; + htVar.bNoReferrer = htOptions.bNoReferrer; htVar.sUserRules = '[a-zA-Z0-9_\\-\\.\\/]'; htVar.sProjecRules = '[a-zA-Z0-9_\\-\\.]'; htVar.sIssueRules = '\\d'; htVar.sSha1Rules = '[a-f0-9]{7,40}'; htVar.htFilter = new Filter(); + htVar.htMarkedOption = { "gfm" : true, "tables" : true, @@ -68,6 +71,72 @@ yobi.Markdown = (function(htOptions){ }; } + function _initializeMarkdownRenderer() { + var renderer = new marked.Renderer(); + renderer.link = function(href, title, text) { + + var link = $('', { + href : _removeJavascript2Href(href), + title : title, + text: text + }); + + if(htVar.bNoReferrer && !isInternalLink(href)) link.attr('rel', "noreferrer"); + + return $('
').append(link.clone()).remove().html(); + }; + + renderer.html = function(html) { + var tag = $(html); + + if (tag.prop("href")) { + + tag.attr("href", _removeJavascript2Href(tag.attr("href"))); + + if(htVar.bNoReferrer && !isInternalLink(tag.attr("href"))) { + tag.attr("rel", function(i, val) { + return _addUniqeValue2Attr(val, "noreferrer"); + }); + } + + html = $('
').append(tag.clone()).remove().html().split(">")[0]+">"; + } + + return html; + }; + + htVar.htMarkedOption.renderer = renderer; + } + + function _addUniqeValue2Attr(origin, value) { + if(!origin) return value; + + var origins = origin.split(" "); + var values = [value]; + + $.each(origins, function(i, val){ + if(val !== value) values.push(val); + }); + + return values.join(" "); + } + + function _removeJavascript2Href(href) { + try { + var wordString = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + href = '#'; + } + + if (wordString.indexOf('javascript:') === 0) { + href = '#'; + } + + return href; + } + /** * Render as Markdown document * @@ -265,6 +334,11 @@ yobi.Markdown = (function(htOptions){ return (sTagName === "TEXTAREA" || sTagName === "INPUT" || elTarget.contentEditable == "true"); } + function isInternalLink(url) { + var linkHostname = $('').attr('href', url).prop('hostname'); + return (linkHostname === document.location.hostname); + } + // public interface return { "init" : _init, diff --git a/public/javascripts/lib/marked.js b/public/javascripts/lib/marked.js index 8d7c21e50..b6a392d7b 100644 --- a/public/javascripts/lib/marked.js +++ b/public/javascripts/lib/marked.js @@ -647,7 +647,7 @@ InlineLexer.prototype.output = function(src) { src = src.substring(cap[0].length); out += this.options.sanitize ? escape(cap[0]) - : cap[0]; + : this.renderer.html(cap[0]); continue; }