blob: e82df77efddb1be4d94204616c929895930a13c8 [file] [log] [blame]
// Copyright 2020 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 (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/safehtml"
"github.com/google/safehtml/uncheckedconversions"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/cookie"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/frontend/page"
"golang.org/x/pkgsite/internal/frontend/serrors"
"golang.org/x/pkgsite/internal/frontend/urlinfo"
"golang.org/x/pkgsite/internal/frontend/versions"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware/stats"
"golang.org/x/pkgsite/internal/stdlib"
"golang.org/x/pkgsite/internal/version"
"golang.org/x/pkgsite/internal/vuln"
)
// UnitPage contains data needed to render the unit template.
type UnitPage struct {
page.BasePage
// Unit is the unit for this page.
Unit *internal.UnitMeta
// Breadcrumb contains data used to render breadcrumb UI elements.
Breadcrumb breadcrumb
// Title is the title of the page.
Title string
// URLPath is the path suitable for links on the page.
// See the unitURLPath for details.
URLPath string
// CanonicalURLPath is a permanent representation of the URL path for a
// unit.
// It uses the resolved module path and version.
// For example, if the latest version of /my.module/pkg is version v1.5.2,
// the canonical URL path for that unit would be /my.module@v1.5.2/pkg
CanonicalURLPath string
// The version string formatted for display.
DisplayVersion string
// LinkVersion is version string suitable for links used to compute
// latest badges.
LinkVersion string
// LatestURL is a url pointing to the latest version of a unit.
LatestURL string
// IsLatestMinor is true if the version displayed is the latest minor of the unit.
// Used to determine the canonical URL for search engines and robots meta directives.
IsLatestMinor bool
// LatestMinorClass is the CSS class that describes the current unit's minor
// version in relationship to the latest version of the unit.
LatestMinorClass string
// Information about the latest major version of the module.
LatestMajorVersion string
LatestMajorVersionURL string
// PageType is the type of page (pkg, cmd, dir, std, or mod).
PageType string
// PageLabels are the labels that will be displayed
// for a given page.
PageLabels []string
// CanShowDetails indicates whether details can be shown or must be
// hidden due to issues like license restrictions.
CanShowDetails bool
// Settings contains settings for the selected tab.
SelectedTab TabSettings
// RedirectedFromPath is the path that redirected to the current page.
// If non-empty, a "redirected from" banner will be displayed
// (see static/frontend/unit/_header.tmpl).
RedirectedFromPath string
// Details contains data specific to the type of page being rendered.
Details any
// Vulns holds vulnerability information.
Vulns []vuln.Vuln
// DepsDevURL holds the full URL to this module version on deps.dev.
DepsDevURL string
// IsGoProject is true if the package is from the standard library or a
// golang.org sub-repository.
IsGoProject bool
}
// serveUnitPage serves a unit page for a path.
func (s *Server) serveUnitPage(ctx context.Context, w http.ResponseWriter, r *http.Request,
ds internal.DataSource, info *urlinfo.URLPathInfo) (err error) {
defer derrors.Wrap(&err, "serveUnitPage(ctx, w, r, ds, %v)", info)
defer stats.Elapsed(ctx, "serveUnitPage")()
tab := r.FormValue("tab")
if tab == "" {
// Default to details tab when there is no tab param.
tab = tabMain
}
// Redirect to clean URL path when tab param is invalid.
if _, ok := unitTabLookup[tab]; !ok {
http.Redirect(w, r, r.URL.Path, http.StatusFound)
return nil
}
um, err := ds.GetUnitMeta(ctx, info.FullPath, info.ModulePath, info.RequestedVersion)
if err != nil {
if !errors.Is(err, derrors.NotFound) {
return err
}
db, ok := ds.(internal.PostgresDB)
if !ok || s.fetchServer == nil {
return serrors.DatasourceNotSupportedError()
}
return s.fetchServer.ServePathNotFoundPage(w, r, db, info.FullPath, info.ModulePath, info.RequestedVersion)
}
makeDepsDevURL := depsDevURLGenerator(ctx, s.depsDevHTTPClient, um)
// Use GOOS and GOARCH query parameters to create a build context, which
// affects the documentation and synopsis. Omitting both results in an empty
// build context, which will match the first (and preferred) build context.
// It's also okay to provide just one (e.g. GOOS=windows), which will select
// the first doc with that value, ignoring the other one.
bc := internal.BuildContext{GOOS: r.FormValue("GOOS"), GOARCH: r.FormValue("GOARCH")}
d, err := fetchDetailsForUnit(ctx, r, tab, ds, um, info.RequestedVersion, bc, s.vulnClient)
if err != nil {
return err
}
if s.shouldServeJSON(r) {
return s.serveJSONPage(w, r, d)
}
if _, ok := internal.DefaultBranches[info.RequestedVersion]; ok {
// Since path@master is a moving target, we don't want it to be stale.
// As a result, we enqueue every request of path@master to the frontend
// task queue, which will initiate a fetch request depending on the
// last time we tried to fetch this module version.
//
// Use a separate context here to prevent the context from being canceled
// elsewhere before a task is enqueued.
if s.queue == nil {
return &serrors.ServerError{
Status: http.StatusBadRequest,
Err: err,
Epage: &page.ErrorPage{
MessageData: fmt.Sprintf(`Default branches like "@%s" are not supported. Omit to get the current version.`,
info.RequestedVersion),
},
}
}
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
log.Infof(ctx, "serveUnitPage: Scheduling %q@%q to be fetched", um.ModulePath, info.RequestedVersion)
if _, err := s.queue.ScheduleFetch(ctx, um.ModulePath, info.RequestedVersion, nil); err != nil {
log.Errorf(ctx, "serveUnitPage(%q): scheduling fetch for %q@%q: %v",
r.URL.Path, um.ModulePath, info.RequestedVersion, err)
}
}()
}
if !isValidTabForUnit(tab, um) {
// Redirect to clean URL path when tab param is invalid for the unit
// type.
http.Redirect(w, r, r.URL.Path, http.StatusFound)
return nil
}
// If we've already called GetUnitMeta for an unknown module path and the latest version, pass
// it to GetLatestInfo to avoid a redundant call.
var latestUnitMeta *internal.UnitMeta
if info.ModulePath == internal.UnknownModulePath && info.RequestedVersion == version.Latest {
latestUnitMeta = um
}
latestInfo := s.GetLatestInfo(ctx, um.Path, um.ModulePath, latestUnitMeta)
var redirectPath string
redirectPath, err = cookie.Extract(w, r, cookie.AlternativeModuleFlash)
if err != nil {
// Don't fail, but don't display a banner either.
log.Errorf(ctx, "extracting AlternativeModuleFlash cookie: %v", err)
}
title := pageTitle(um)
basePage := s.newBasePage(r, title)
tabSettings := unitTabLookup[tab]
basePage.AllowWideContent = true
if tabSettings.Name == "" {
basePage.UseResponsiveLayout = true
}
lv := versions.LinkVersion(um.ModulePath, info.RequestedVersion, um.Version)
page := UnitPage{
BasePage: basePage,
Unit: um,
Breadcrumb: displayBreadcrumb(um, info.RequestedVersion),
Title: title,
SelectedTab: tabSettings,
URLPath: versions.ConstructUnitURL(um.Path, um.ModulePath, info.RequestedVersion),
CanonicalURLPath: canonicalURLPath(um.Path, um.ModulePath, info.RequestedVersion, um.Version),
DisplayVersion: versions.DisplayVersion(um.ModulePath, info.RequestedVersion, um.Version),
LinkVersion: lv,
LatestURL: versions.ConstructUnitURL(um.Path, um.ModulePath, version.Latest),
LatestMinorClass: latestMinorClass(lv, latestInfo),
LatestMajorVersionURL: latestInfo.MajorUnitPath,
PageLabels: pageLabels(um),
PageType: pageType(um),
RedirectedFromPath: redirectPath,
DepsDevURL: makeDepsDevURL(),
IsGoProject: isGoProject(um.ModulePath),
IsLatestMinor: lv == latestInfo.MinorVersion,
}
// Show the banner if there was no error getting the latest major version,
// and it is different from the major version of the current module path.
latestMajor := internal.MajorVersionForModule(latestInfo.MajorModulePath)
if latestMajor != "" && latestMajor != internal.MajorVersionForModule(um.ModulePath) {
page.LatestMajorVersion = latestMajor
}
page.Details = d
main, ok := d.(*MainDetails)
if ok {
page.MetaDescription = metaDescription(main.DocSynopsis)
}
// Get vulnerability information.
page.Vulns = vuln.VulnsForPackage(ctx, um.ModulePath, um.Version, um.Path, s.vulnClient)
s.servePage(ctx, w, tabSettings.TemplateName, page)
return nil
}
func (s *Server) shouldServeJSON(r *http.Request) bool {
return s.serveStats && r.FormValue("content") == "json"
}
func (s *Server) serveJSONPage(w http.ResponseWriter, r *http.Request, d any) (err error) {
defer derrors.Wrap(&err, "serveJSONPage(ctx, w, r)")
if !s.shouldServeJSON(r) {
return derrors.NotFound
}
data, err := json.Marshal(d)
if err != nil {
return fmt.Errorf("json.Marshal: %v", err)
}
if _, err := w.Write(data); err != nil {
return fmt.Errorf("w.Write: %v", err)
}
return nil
}
func latestMinorClass(version string, latest internal.LatestInfo) string {
c := "DetailsHeader-badge"
switch {
case latest.MinorVersion == "":
c += "--unknown"
case latest.MinorVersion == version && !latest.UnitExistsAtMinor:
c += "--notAtLatest"
case latest.MinorVersion == version:
c += "--latest"
default:
c += "--goToLatest"
}
return c
}
// metaDescription uses a safehtml escape hatch to build HTML used
// to render the <meta name="Description"> for unit pages as a
// workaround for https://github.com/google/safehtml/issues/6.
func metaDescription(synopsis string) safehtml.HTML {
if synopsis == "" {
return safehtml.HTML{}
}
return safehtml.HTMLConcat(
uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(`<meta name="Description" content="`),
safehtml.HTMLEscaped(synopsis),
uncheckedconversions.HTMLFromStringKnownToSatisfyTypeContract(`">`),
)
}
// isValidTabForUnit reports whether the tab is valid for the given unit.
// It is assumed that tab is a key in unitTabLookup.
func isValidTabForUnit(tab string, um *internal.UnitMeta) bool {
if tab == tabLicenses && !um.IsRedistributable {
return false
}
if !um.IsPackage() && (tab == tabImports || tab == tabImportedBy) {
return false
}
return true
}
// canonicalURLPath constructs a URL path to the unit that always includes the
// resolved version.
func canonicalURLPath(fullPath, modulePath, requestedVersion, resolvedVersion string) string {
return versions.ConstructUnitURL(fullPath, modulePath,
versions.LinkVersion(modulePath, requestedVersion, resolvedVersion))
}
func isGoProject(modulePath string) bool {
return modulePath == stdlib.ModulePath || strings.HasPrefix(modulePath, "golang.org")
}