pkgsite/internal/frontend
..
details.go
details_test.go
imports.go
imports_test.go
license.go
paginate.go
paginate_test.go
readme.go
readme_test.go
search.go
search_test.go
server.go
server_test.go
versions.go
versions_test.go

readme.go

// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package frontend

import (
	"bytes"
	"context"
	"fmt"
	"html"
	"html/template"
	"net/url"
	"path"
	"path/filepath"
	"strings"

	"github.com/microcosm-cc/bluemonday"
	"github.com/russross/blackfriday/v2"
	"golang.org/x/discovery/internal"
	"golang.org/x/discovery/internal/postgres"
)

// ReadMeDetails contains all of the data that the readme template
// needs to populate.
type ReadMeDetails struct {
	ModulePath string
	ReadMe     template.HTML
}

// fetchReadMeDetails fetches data for the module version specified by path and version
// from the database and returns a ReadMeDetails.
func fetchReadMeDetails(ctx context.Context, db *postgres.DB, vi *internal.VersionInfo) (*ReadMeDetails, error) {
	return &ReadMeDetails{
		ModulePath: vi.ModulePath,
		ReadMe:     readmeHTML(vi),
	}, nil
}

// readmeHTML sanitizes readmeContents based on bluemondy.UGCPolicy and returns
// a template.HTML. If readmeFilePath indicates that this is a markdown file,
// it will also render the markdown contents using blackfriday.
func readmeHTML(vi *internal.VersionInfo) template.HTML {
	if filepath.Ext(vi.ReadmeFilePath) != ".md" {
		return template.HTML(fmt.Sprintf(`<pre class="readme">%s</pre>`, html.EscapeString(string(vi.ReadmeContents))))
	}

	// bluemonday.UGCPolicy allows a broad selection of HTML elements and
	// attributes that are safe for user generated content. This policy does
	// not whitelist iframes, object, embed, styles, script, etc.
	p := bluemonday.UGCPolicy()

	// Allow width and align attributes on img. This is used to size README
	// images appropriately where used, like the gin-gonic/logo/color.png
	// image in the github.com/gin-gonic/gin README.
	p.AllowAttrs("width", "align").OnElements("img")

	// blackfriday.Run() uses CommonHTMLFlags and CommonExtensions by default.
	renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{Flags: blackfriday.CommonHTMLFlags})
	parser := blackfriday.New(blackfriday.WithExtensions(blackfriday.CommonExtensions))

	// Render HTML similar to blackfriday.Run(), but here we implement a custom
	// Walk function in order to modify image paths in the rendered HTML.
	b := &bytes.Buffer{}
	rootNode := parser.Parse(vi.ReadmeContents)
	rootNode.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
		if node.Type == blackfriday.Image {
			translateRelativeLink(node, vi)
		}
		return renderer.RenderNode(b, node, entering)
	})
	return template.HTML(p.SanitizeReader(b).String())
}

// translateRelativeLink modifies a blackfriday.Node to convert relative image
// paths to absolute paths.
//
// Markdown files, such as the Go README, sometimes use relative image paths to
// image files inside the repository. As the discovery site doesn't host the
// full repository content, in order for the image to render, we need to
// convert the relative path to an absolute URL to a hosted image.
func translateRelativeLink(node *blackfriday.Node, vi *internal.VersionInfo) {
	repo, err := url.Parse(vi.RepositoryURL)
	if err != nil {
		return
	}
	imageURL, err := url.Parse(string(node.LinkData.Destination))
	if err != nil || imageURL.IsAbs() {
		return
	}
	ref := "master"
	switch vi.VersionType {
	case internal.VersionTypeRelease, internal.VersionTypePrerelease:
		ref = vi.Version
		if internal.IsStandardLibraryModule(vi.ModulePath) {
			ref, err = internal.GoVersionForSemanticVersion(ref)
			if err != nil {
				ref = "master"
			}
		}
	case internal.VersionTypePseudo:
		if segs := strings.SplitAfter(vi.Version, "-"); len(segs) != 0 {
			ref = segs[len(segs)-1]
		}
	}
	var abs *url.URL
	switch repo.Hostname() {
	case "github.com":
		abs = &url.URL{Scheme: "https", Host: "raw.githubusercontent.com", Path: path.Join(repo.Path, ref, path.Clean(imageURL.Path))}
	case "gitlab.com":
		abs = &url.URL{Scheme: "https", Host: "gitlab.com", Path: path.Join(repo.Path, "raw", ref, path.Clean(imageURL.Path))}
	default:
		return
	}
	node.LinkData.Destination = []byte(abs.String())
}