Merge pull request #735 from gsw945/master

add iframe support
pull/745/head
玖亖伍 2021-10-09 18:31:38 +08:00 committed by GitHub
commit c4d05d2a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 269 additions and 16 deletions

View File

@ -42,7 +42,9 @@ MinDoc 的前身是 [SmartWiki](https://github.com/lifei6671/SmartWiki) 文档
对于没有Golang使用经验的用户可以从 [https://github.com/mindoc-org/mindoc/releases](https://github.com/mindoc-org/mindoc/releases) 这里下载编译完的程序。
如果有Golang开发经验建议通过编译安装要求golang版本不小于1.13(需支持`CGO`和`go mod`)。
> 注意: CentOS7上GLibC版本低需要源码编译, 编译好的二进制文件无法运行。
## 常规编译
```bash
# 克隆源码
git clone https://github.com/mindoc-org/mindoc.git
@ -64,6 +66,31 @@ MinDoc 如果使用MySQL储存数据则编码必须是`utf8mb4_general_ci`。
**默认程序会自动初始化一个超级管理员用户admin 密码123456 。请登录后重新设置密码。**
## Linux系统中不依赖gLibC的编译方式
### 安装 musl-gcc
```bash
wget -c http://www.musl-libc.org/releases/musl-1.2.2.tar.gz
tar -xvf musl-1.2.2.tar.gz
cd musl-1.2.2
./configure
make
sudo make install
```
### 使用 musl-gcc 编译 mindoc
```bash
go mod tidy -v
export GOARCH=amd64
export GOOS=linux
# 设置使用musl-gcc
export CC=/usr/local/musl/bin/musl-gcc
# 设置版本
export TRAVIS_TAG=temp-musl-v`date +%y%m%d`
go build -o mindoc_linux_musl_amd64 --ldflags="-linkmode external -extldflags '-static' -w -X 'github.com/mindoc-org/mindoc/conf.VERSION=$TRAVIS_TAG' -X 'github.com/mindoc-org/mindoc/conf.BUILD_TIME=`date`' -X 'github.com/mindoc-org/mindoc/conf.GO_VERSION=`go version`'"
# 验证
./mindoc_linux_amd64 version
```
```bash

View File

@ -263,6 +263,12 @@ func RegisterFunction() {
logs.Error("注册函数 urlfor 出错 ->", err)
os.Exit(-1)
}
//读取配置值(未作任何转换)
err = web.AddFuncMap("conf", conf.CONF)
if err != nil {
logs.Error("注册函数 conf 出错 ->", err)
os.Exit(-1)
}
err = web.AddFuncMap("date_format", func(t time.Time, format string) string {
return t.Local().Format(format)
})

View File

@ -8,6 +8,7 @@ sessionon = true
sessionname = mindoc_id
copyrequestbody = true
enablexsrf = "${MINDOC_ENABLE_XSRF||false}"
enable_iframe = "${MINDOC_ENABLE_IFRAME||false}"
#系统完整URL(http://doc.iminho.me),如果该项不设置,会从请求头中获取地址。
baseurl="${MINDOC_BASE_URL}"

View File

@ -152,6 +152,11 @@ func GetEnableExport() bool {
return web.AppConfig.DefaultBool("enable_export", true)
}
//是否启用iframe
func GetEnableIframe() bool {
return web.AppConfig.DefaultBool("enable_iframe", false)
}
//同一项目导出线程的并发数
func GetExportProcessNum() int {
exportProcessNum := web.AppConfig.DefaultInt("export_process_num", 1)
@ -208,6 +213,15 @@ func IsAllowUploadFileExt(ext string) bool {
return false
}
//读取配置文件值
func CONF(key string, value ...string) string {
defaultValue := ""
if len(value) > 0 {
defaultValue = value[0]
}
return web.AppConfig.DefaultString(key, defaultValue)
}
//重写生成URL的方法加上完整的域名
func URLFor(endpoint string, values ...interface{}) string {
baseUrl := web.AppConfig.DefaultString("baseurl", "")

View File

@ -1,11 +1,82 @@
package routers
import (
"crypto/tls"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/beego/beego/v2/core/logs"
"github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/server/web/context"
"github.com/mindoc-org/mindoc/conf"
"github.com/mindoc-org/mindoc/controllers"
)
func rt(req *http.Request) (*http.Response, error) {
log.Printf("request received. url=%s", req.URL)
// req.Header.Set("Host", "httpbin.org") // <--- I set it here as well
defer log.Printf("request complete. url=%s", req.URL)
return http.DefaultTransport.RoundTrip(req)
}
// roundTripper makes func signature a http.RoundTripper
type roundTripper func(*http.Request) (*http.Response, error)
func (f roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }
type CorsTransport struct {
http.RoundTripper
}
func (t *CorsTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
// refer:
// - https://stackoverflow.com/questions/31535569/golang-how-to-read-response-body-of-reverseproxy/31536962#31536962
// - https://gist.github.com/simon-cj/b4da0b2bca793ec3b8a5abe04c8fca41
resp, err = t.RoundTripper.RoundTrip(req)
logs.Debug(resp)
if err != nil {
return nil, err
}
resp.Header.Del("Access-Control-Request-Method")
resp.Header.Set("Access-Control-Allow-Origin", "*")
return resp, nil
}
func init() {
web.Any("/cors-anywhere", func(ctx *context.Context) {
u, _ := url.PathUnescape(ctx.Input.Query("url"))
logs.Error("ReverseProxy: ", u)
if len(u) > 0 && strings.HasPrefix(u, "http") {
if strings.TrimRight(conf.BaseUrl, "/") == ctx.Input.Site() {
ctx.Redirect(302, u)
} else {
target, _ := url.Parse(u)
logs.Debug("target: ", target)
proxy := &httputil.ReverseProxy{
Transport: roundTripper(rt),
Director: func(req *http.Request) {
req.Header = ctx.Request.Header
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path
req.Header.Set("Host", target.Host)
},
}
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
proxy.ServeHTTP(ctx.ResponseWriter, ctx.Request)
}
} else {
ctx.ResponseWriter.WriteHeader(http.StatusBadRequest)
ctx.Output.Body([]byte("400 Bad Request"))
}
})
web.Router("/", &controllers.HomeController{}, "*:Index")
web.Router("/login", &controllers.AccountController{}, "*:Login")

View File

@ -96,6 +96,7 @@ body{
bottom: 0;
overflow: hidden;
border-top: 1px solid #DDDDDD;
z-index: 999;
}
.manual-editor-container .manual-editormd{
position: absolute;

View File

@ -3,6 +3,10 @@ $(function () {
js : window.katex.js,
css : window.katex.css
};
var htmlDecodeList = ["style","script","title","onmouseover","onmouseout","style"];
if (!window.IS_ENABLE_IFRAME) {
htmlDecodeList.unshift("iframe");
}
window.editor = editormd("docEditor", {
width: "100%",
height: "100%",
@ -18,8 +22,8 @@ $(function () {
taskList: true,
flowChart: true,
mermaid: true,
htmlDecode: "style,script,iframe,title,onmouseover,onmouseout,style",
lineNumbers: false,
htmlDecode: htmlDecodeList.join(','),
lineNumbers: true,
sequenceDiagram: true,
highlightStyle: window.highlightStyle ? window.highlightStyle : "github",
tocStartLevel: 1,

View File

@ -0,0 +1 @@
!function(P,H,k){"use strict";if(1==P.importNode.length&&!H.get("ungap-li")){var D="extends";try{var e={extends:"li"},t=HTMLLIElement,n=function(){return Reflect.construct(t,[],n)};if(n.prototype=k.create(t.prototype),H.define("ungap-li",n,e),!/is="ungap-li"/.test((new n).outerHTML))throw e}catch(e){!function(){var l="attributeChangedCallback",n="connectedCallback",r="disconnectedCallback",e=Element.prototype,i=k.assign,t=k.create,o=k.defineProperties,a=k.getOwnPropertyDescriptor,s=k.setPrototypeOf,u=H.define,c=H.get,f=H.upgrade,p=H.whenDefined,v=t(null),d=new WeakMap,g={childList:!0,subtree:!0};Reflect.ownKeys(self).filter(function(e){return"string"==typeof e&&/^HTML(?!Element)/.test(e)}).forEach(function(e){function t(){}var n=self[e];s(t,n),(t.prototype=n.prototype).constructor=t,(n={})[e]={value:t},o(self,n)}),new MutationObserver(m).observe(P,g),O(Document.prototype,"importNode"),O(Node.prototype,"cloneNode"),o(H,{define:{value:function(e,t,n){if(e=e.toLowerCase(),n&&D in n){v[e]=i({},n,{Class:t});for(var e=n[D]+'[is="'+e+'"]',r=P.querySelectorAll(e),o=0,a=r.length;o<a;o++)A(r[o])}else u.apply(H,arguments)}},get:{value:function(e){return e in v?v[e].Class:c.call(H,e)}},upgrade:{value:function(e){var t=L(e);!t||e instanceof t.Class?f.call(H,e):N(e,t)}},whenDefined:{value:function(e){return e in v?Promise.resolve():p.call(H,e)}}});var h=P.createElement;o(P,{createElement:{value:function(e,t){e=h.call(P,e);return t&&"is"in t&&(e.setAttribute("is",t.is),H.upgrade(e)),e}}});var b=a(e,"attachShadow").value,y=a(e,"innerHTML");function m(e){for(var t=0,n=e.length;t<n;t++){for(var r=e[t],o=r.addedNodes,a=r.removedNodes,i=0,l=o.length;i<l;i++)A(o[i]);for(i=0,l=a.length;i<l;i++)C(a[i])}}function w(e){for(var t=0,n=e.length;t<n;t++){var r=e[t],o=r.attributeName,a=r.oldValue,i=r.target,r=i.getAttribute(o);l in i&&(a!=r||null!=r)&&i[l](o,a,i.getAttribute(o),null)}}function C(e){var t;1===e.nodeType&&((t=L(e))&&e instanceof t.Class&&r in e&&d.get(e)!==r&&(d.set(e,r),Promise.resolve(e).then(T)),E(e,C))}function L(e){e=e.getAttribute("is");return e&&(e=e.toLowerCase())in v?v[e]:null}function M(e){e[n]()}function T(e){e[r]()}function N(e,t){var t=t.Class,n=t.observedAttributes||[];if(s(e,t.prototype),n.length){new MutationObserver(w).observe(e,{attributes:!0,attributeFilter:n,attributeOldValue:!0});for(var r=[],o=0,a=n.length;o<a;o++)r.push({attributeName:n[o],oldValue:null,target:e});w(r)}}function A(e){var t;1===e.nodeType&&((t=L(e))&&(e instanceof t.Class||N(e,t),n in e&&e.isConnected&&d.get(e)!==n&&(d.set(e,n),Promise.resolve(e).then(M))),E(e,A))}function E(e,t){for(var n=e.content,r=(n&&11==n.nodeType?n:e).querySelectorAll("[is]"),o=0,a=r.length;o<a;o++)t(r[o])}function O(e,t){var n=e[t],r={};r[t]={value:function(){var e=n.apply(this,arguments);switch(e.nodeType){case 1:case 11:E(e,A)}return e}},o(e,r)}o(e,{attachShadow:{value:function(){var e=b.apply(this,arguments);return new MutationObserver(m).observe(e,g),e}},innerHTML:{get:y.get,set:function(e){y.set.call(this,e),/\bis=("|')?[a-z0-9_-]+\1/i.test(e)&&E(this,A)}}})}()}}}(document,customElements,Object);

View File

@ -54,7 +54,10 @@ $(function () {
remark: 'Remark',
}
};
var htmlDecodeList = ["style","script","title","onmouseover","onmouseout","style"];
if (!window.IS_ENABLE_IFRAME) {
htmlDecodeList.unshift("iframe");
}
window.editor = editormd("docEditor", {
width: "100%",
height: "100%",
@ -69,8 +72,8 @@ $(function () {
fileUploadURL: window.fileUploadURL,
taskList: true,
flowChart: true,
htmlDecode: "style,script,iframe,title,onmouseover,onmouseout,style",
lineNumbers: false,
htmlDecode: htmlDecodeList.join(','),
lineNumbers: true,
sequenceDiagram: true,
tocStartLevel: 1,
tocm: true,
@ -193,7 +196,7 @@ $(function () {
} else {
var action = window.editor.toolbarHandlers[name];
if (action !== "undefined") {
if (!!action && action !== "undefined") {
$.proxy(action, window.editor)();
window.editor.focus();
}

View File

@ -0,0 +1,87 @@
customElements.define('x-frame-bypass', class extends HTMLIFrameElement {
static get observedAttributes() {
return ['src']
}
constructor () {
super()
}
attributeChangedCallback () {
this.load(this.src)
}
connectedCallback () {
this.sandbox = '' + this.sandbox || 'allow-forms allow-modals allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation' // all except allow-top-navigation
}
load (url, options) {
if (!url || !url.startsWith('http'))
throw new Error(`X-Frame-Bypass src ${url} does not start with http(s)://`)
console.log('X-Frame-Bypass loading:', url)
this.srcdoc = `<html>
<head>
<style>
.loader {
position: absolute;
top: calc(50% - 25px);
left: calc(50% - 25px);
width: 50px;
height: 50px;
background-color: #333;
border-radius: 50%;
animation: loader 1s infinite ease-in-out;
}
@keyframes loader {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
opacity: 0;
}
}
</style>
</head>
<body>
<div class="loader"></div>
</body>
</html>`
this.fetchProxy(url, options, 0).then(res => res.text()).then(data => {
if (data)
this.srcdoc = data.replace(/<head([^>]*)>/i, `<head$1>
<base href="${url}">
<script>
// X-Frame-Bypass navigation event handlers
document.addEventListener('click', e => {
if (frameElement && document.activeElement && document.activeElement.href) {
e.preventDefault()
frameElement.load(document.activeElement.href)
}
})
document.addEventListener('submit', e => {
if (frameElement && document.activeElement && document.activeElement.form && document.activeElement.form.action) {
e.preventDefault()
if (document.activeElement.form.method === 'post')
frameElement.load(document.activeElement.form.action, {method: 'post', body: new FormData(document.activeElement.form)})
else
frameElement.load(document.activeElement.form.action + '?' + new URLSearchParams(new FormData(document.activeElement.form)))
}
})
</script>`)
}).catch(e => console.error('Cannot load X-Frame-Bypass:', e))
}
fetchProxy (url, options, i) {
const proxies = (options || {}).proxies || [
window.BASE_URL + 'cors-anywhere?url=',
'https://cors-anywhere.herokuapp.com/',
'https://yacdn.org/proxy/',
'https://api.codetabs.com/v1/proxy/?quest='
]
return fetch(proxies[i] + url, options).then(res => {
if (!res.ok)
throw new Error(`${res.status} ${res.statusText}`);
return res
}).catch(error => {
if (i === proxies.length - 1)
throw error
return this.fetchProxy(url, options, i + 1)
})
}
}, {extends: 'iframe'})

View File

@ -9,7 +9,7 @@ import (
"github.com/mindoc-org/mindoc/conf"
)
func StripTags(s string) string {
func StripTags(s string) string {
//将HTML标签全转换成小写
re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
@ -33,8 +33,9 @@ func StripTags(s string) string {
return src
}
//自动提取文章摘要
func AutoSummary(body string,l int) string {
func AutoSummary(body string, l int) string {
//匹配图片,如果图片语法是在代码块中,这里同样会处理
re := regexp.MustCompile(`<p>(.*?)</p>`)
@ -42,11 +43,11 @@ func AutoSummary(body string,l int) string {
contents := re.FindAllString(body, -1)
if len(contents) <= 0 {
return ""
return ""
}
content := ""
for _,s := range contents {
b := strings.Replace(StripTags(s),"\n","", -1)
for _, s := range contents {
b := strings.Replace(StripTags(s), "\n", "", -1)
if l <= 0 {
break
@ -70,7 +71,9 @@ func SafetyProcessor(html string) string {
docQuery.Find("applet").Remove()
docQuery.Find("frame").Remove()
docQuery.Find("meta").Remove()
docQuery.Find("iframe").Remove()
if !conf.GetEnableIframe() {
docQuery.Find("iframe").Remove()
}
docQuery.Find("*").Each(func(i int, selection *goquery.Selection) {
if href, ok := selection.Attr("href"); ok && strings.HasPrefix(href, "javascript:") {
@ -117,10 +120,9 @@ func SafetyProcessor(html string) string {
}
}
if html, err := docQuery.Html(); err == nil {
return strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(html), "<html><head></head><body>"), "</body></html>")
return strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(html), "<html><head></head><body>"), "</body></html>")
}
}
return html
}
}

View File

@ -29,6 +29,10 @@
<link href="{{cdncss "/static/katex/katex.min.css"}}" rel="stylesheet">
<link href="{{cdncss "/static/css/print.css" "version"}}" media="print" rel="stylesheet">
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
<script type="text/javascript">window.book={"identify":"{{.Model.Identify}}"};</script>
<style>
.btn-mobile {
@ -251,6 +255,8 @@
<script src="{{cdnjs "/static/js/jquery.highlight.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/kancloud.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/splitbar.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$("#searchList").on("click","a",function () {

View File

@ -92,6 +92,10 @@
border: 1px solid #357ebd;
}
</style>
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
</head>
<body>
<div class="auth_form">
@ -109,6 +113,8 @@
</form>
</div>
</div>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$("#auth_form").ajaxForm({
beforeSerialize: function () {

View File

@ -11,7 +11,10 @@
color: #44B036 !important;
}
</style>
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
<script type="text/javascript">
window.editor = null;
window.imageUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
@ -207,6 +210,8 @@
<script src="{{cdnjs "/static/js/jquery.form.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/editor.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/html-editor.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
lang = {{i18n $.Lang "common.js_lang"}};

View File

@ -19,6 +19,10 @@
<script src="/static/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="/static/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
</head>
<body>
<div class="m-manual manual-reader">
@ -173,6 +177,8 @@
<script src="{{cdnjs "/static/bootstrap/js/bootstrap.min.js"}}"></script>
<script src="{{cdnjs "/static/jstree/3.3.4/jstree.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/jquery/plugins/imgbox/jquery.imgbox.pack.js"}}"></script>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
$("#sidebar").jstree({

View File

@ -6,6 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{i18n .Lang "doc.edit_doc"}} - Powered by MinDoc</title>
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
<script type="text/javascript">
window.treeCatalog = null;
window.baseUrl = "{{.BaseUrl}}";
@ -109,6 +113,7 @@
</div>
<div class="editormd-group pull-right">
<a target="_blank" href="{{urlfor "DocumentController.Read" ":key" .Model.Identify ":id" ""}}" data-toggle="tooltip" data-title="{{i18n .Lang "blog.preview"}}"><i class="fa fa-external-link" name="preview-open" aria-hidden="true"></i></a>
<a href="javascript:;" data-toggle="tooltip" data-title="{{i18n .Lang "doc.publish"}}"><i class="fa fa-cloud-upload" name="release" aria-hidden="true"></i></a>
</div>
@ -451,6 +456,8 @@
<script src="{{cdnjs "/static/js/array.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/editor.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/markdown.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
editLangPath = {{cdnjs "/static/editor.md/languages/"}} + lang

View File

@ -6,6 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{i18n .Lang "doc.edit_doc"}} - Powered by MinDoc</title>
<script type="text/javascript">
window.IS_ENABLE_IFRAME = '{{conf "enable_iframe" }}' === 'true';
window.BASE_URL = '{{urlfor "HomeController.Index" }}';
</script>
<script type="text/javascript">
window.editor = null;
window.imageUploadURL = "{{urlfor "DocumentController.Upload" "identify" .Model.Identify}}";
@ -384,6 +388,8 @@
<script src="{{cdnjs "/static/js/array.js" "version"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/editor.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/quill.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/custom-elements-builtin-0.6.5.min.js"}}" type="text/javascript"></script>
<script src="{{cdnjs "/static/js/x-frame-bypass-1.0.2.js"}}" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
lang = {{i18n $.Lang "common.js_lang"}};