From 49b8716c40723d5262bcff588bb9670c97de6d42 Mon Sep 17 00:00:00 2001
From: Sergey Sharybin <sergey.vfx@gmail.com>
Date: Fri, 21 Jun 2024 20:23:54 +0200
Subject: [PATCH] Support relative paths to videos from Wiki pages (#31061)

This change fixes cases when a Wiki page refers to a video stored in the
Wiki repository using relative path. It follows the similar case which
has been already implemented for images.

Test plan:
- Create repository and Wiki page
- Clone the Wiki repository
- Add video to it, say `video.mp4`
- Modify the markdown file to refer to the video using `<video
src="video.mp4">`
- Commit the Wiki page
- Observe that the video is properly displayed

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
---
 modules/markup/html.go      | 53 +++++++------------------------
 modules/markup/html_node.go | 62 +++++++++++++++++++++++++++++++++++++
 modules/markup/html_test.go | 11 ++++++-
 3 files changed, 83 insertions(+), 43 deletions(-)
 create mode 100644 modules/markup/html_node.go

diff --git a/modules/markup/html.go b/modules/markup/html.go
index c312d3cba0..b8069d459a 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -88,6 +88,10 @@ func IsFullURLString(link string) bool {
 	return fullURLPattern.MatchString(link)
 }
 
+func IsNonEmptyRelativePath(link string) bool {
+	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
+}
+
 // regexp for full links to issues/pulls
 var issueFullPattern *regexp.Regexp
 
@@ -358,41 +362,6 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
 	return nil
 }
 
-func handleNodeImg(ctx *RenderContext, img *html.Node) {
-	for i, attr := range img.Attr {
-		if attr.Key != "src" {
-			continue
-		}
-
-		if attr.Val != "" && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "/") {
-			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
-
-			// By default, the "<img>" tag should also be clickable,
-			// because frontend use `<img>` to paste the re-scaled image into the markdown,
-			// so it must match the default markdown image behavior.
-			hasParentAnchor := false
-			for p := img.Parent; p != nil; p = p.Parent {
-				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
-					break
-				}
-			}
-			if !hasParentAnchor {
-				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
-					{Key: "href", Val: attr.Val},
-					{Key: "target", Val: "_blank"},
-				}}
-				parent := img.Parent
-				imgNext := img.NextSibling
-				parent.RemoveChild(img)
-				parent.InsertBefore(imgA, imgNext)
-				imgA.AppendChild(img)
-			}
-		}
-		attr.Val = camoHandleLink(attr.Val)
-		img.Attr[i] = attr
-	}
-}
-
 func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
 	// Add user-content- to IDs and "#" links if they don't already have them
 	for idx, attr := range node.Attr {
@@ -412,20 +381,20 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
 		}
 	}
 
-	// We ignore code and pre.
 	switch node.Type {
 	case html.TextNode:
 		processTextNodes(ctx, procs, node)
 	case html.ElementNode:
-		if node.Data == "img" {
-			next := node.NextSibling
-			handleNodeImg(ctx, node)
-			return next
+		if node.Data == "code" || node.Data == "pre" {
+			// ignore code and pre nodes
+			return node.NextSibling
+		} else if node.Data == "img" {
+			return visitNodeImg(ctx, node)
+		} else if node.Data == "video" {
+			return visitNodeVideo(ctx, node)
 		} else if node.Data == "a" {
 			// Restrict text in links to emojis
 			procs = emojiProcessors
-		} else if node.Data == "code" || node.Data == "pre" {
-			return node.NextSibling
 		} else if node.Data == "i" {
 			for _, attr := range node.Attr {
 				if attr.Key != "class" {
diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go
new file mode 100644
index 0000000000..6d784975b9
--- /dev/null
+++ b/modules/markup/html_node.go
@@ -0,0 +1,62 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+	"code.gitea.io/gitea/modules/util"
+
+	"golang.org/x/net/html"
+)
+
+func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
+	next = img.NextSibling
+	for i, attr := range img.Attr {
+		if attr.Key != "src" {
+			continue
+		}
+
+		if IsNonEmptyRelativePath(attr.Val) {
+			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
+
+			// By default, the "<img>" tag should also be clickable,
+			// because frontend use `<img>` to paste the re-scaled image into the markdown,
+			// so it must match the default markdown image behavior.
+			hasParentAnchor := false
+			for p := img.Parent; p != nil; p = p.Parent {
+				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
+					break
+				}
+			}
+			if !hasParentAnchor {
+				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
+					{Key: "href", Val: attr.Val},
+					{Key: "target", Val: "_blank"},
+				}}
+				parent := img.Parent
+				imgNext := img.NextSibling
+				parent.RemoveChild(img)
+				parent.InsertBefore(imgA, imgNext)
+				imgA.AppendChild(img)
+			}
+		}
+		attr.Val = camoHandleLink(attr.Val)
+		img.Attr[i] = attr
+	}
+	return next
+}
+
+func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
+	next = node.NextSibling
+	for i, attr := range node.Attr {
+		if attr.Key != "src" {
+			continue
+		}
+		if IsNonEmptyRelativePath(attr.Val) {
+			attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
+		}
+		attr.Val = camoHandleLink(attr.Val)
+		node.Attr[i] = attr
+	}
+	return next
+}
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 399488912e..c69f3ddd64 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -522,7 +522,7 @@ func TestRender_ShortLinks(t *testing.T) {
 		`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`)
 }
 
-func TestRender_RelativeImages(t *testing.T) {
+func TestRender_RelativeMedias(t *testing.T) {
 	render := func(input string, isWiki bool, links markup.Links) string {
 		buffer, err := markdown.RenderString(&markup.RenderContext{
 			Ctx:    git.DefaultContext,
@@ -548,6 +548,15 @@ func TestRender_RelativeImages(t *testing.T) {
 
 	out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
 	assert.Equal(t, `<img src="/LINK"/>`, out)
+
+	out = render(`<video src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
+	assert.Equal(t, `<video src="/test-owner/test-repo/LINK"></video>`, out)
+
+	out = render(`<video src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
+	assert.Equal(t, `<video src="/test-owner/test-repo/wiki/raw/LINK"></video>`, out)
+
+	out = render(`<video src="/LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
+	assert.Equal(t, `<video src="/LINK"></video>`, out)
 }
 
 func Test_ParseClusterFuzz(t *testing.T) {