blob: e387d3257251ab0152474032ebc3bf2fba5084bc [file] [log] [blame]
// 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 (
"context"
"fmt"
"html/template"
"net/http"
"strings"
"golang.org/x/discovery/internal"
"golang.org/x/discovery/internal/derrors"
"golang.org/x/discovery/internal/license"
"golang.org/x/discovery/internal/log"
"golang.org/x/discovery/internal/stdlib"
"golang.org/x/discovery/internal/thirdparty/module"
"golang.org/x/discovery/internal/thirdparty/semver"
"golang.org/x/xerrors"
)
// DetailsPage contains data for a package of module details template.
type DetailsPage struct {
basePage
Title string
CanShowDetails bool
Settings TabSettings
Details interface{}
Header interface{}
BreadcrumbPath template.HTML
Tabs []TabSettings
// PageType is either "mod", "dir", or "pkg" depending on the details
// handler.
PageType string
}
func (s *Server) handleDetails(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
s.staticPageHandler("index.tmpl", "go.dev")(w, r)
return
}
parts := strings.SplitN(strings.TrimPrefix(r.URL.Path, "/"), "@", 2)
if stdlib.Contains(parts[0]) {
s.handleStdLib(w, r)
return
}
s.handlePackageDetails(w, r)
}
// handlePackageDetails handles requests for package details pages. It expects
// paths of the form "/<path>[@<version>?tab=<tab>]".
func (s *Server) handlePackageDetails(w http.ResponseWriter, r *http.Request) {
pkgPath, modulePath, version, err := parseDetailsURLPath(r.URL.Path)
if err != nil {
log.Errorf("handlePackageDetails: %v", err)
s.serveErrorPage(w, r, http.StatusBadRequest, nil)
return
}
s.servePackagePage(w, r, pkgPath, modulePath, version)
}
// handlePackageDetailsRedirect redirects all redirects to "/pkg" to "/".
func (s *Server) handlePackageDetailsRedirect(w http.ResponseWriter, r *http.Request) {
urlPath := strings.TrimPrefix(r.URL.Path, "/pkg")
http.Redirect(w, r, urlPath, http.StatusMovedPermanently)
}
// handleModuleDetails handles requests for non-stdlib module details pages. It
// expects paths of the form "/mod/<module-path>[@<version>?tab=<tab>]".
// stdlib module pages are handled at "/std".
func (s *Server) handleModuleDetails(w http.ResponseWriter, r *http.Request) {
urlPath := strings.TrimPrefix(r.URL.Path, "/mod")
path, _, version, err := parseDetailsURLPath(urlPath)
if err != nil {
log.Infof("handleModuleDetails: %v", err)
s.serveErrorPage(w, r, http.StatusBadRequest, nil)
return
}
s.serveModulePage(w, r, path, version)
}
// servePackagePage serves details pages for the package with import path
// pkgPath, in the module specified by modulePath and version.
func (s *Server) servePackagePage(w http.ResponseWriter, r *http.Request, pkgPath, modulePath, version string) {
ctx := r.Context()
if code, epage := checkPathAndVersion(ctx, s.ds, pkgPath, version); code != http.StatusOK {
s.serveErrorPage(w, r, code, epage)
return
}
// This function handles top level behavior related to the existence of the
// requested pkgPath@version.
// 1. If a package exists at this version, serve it.
// 2. If there is a directory at this version, serve it.
// 3. If there is another version that contains this package path: serve a
// 404 and suggest these versions.
// 4. Just serve a 404
pkg, err := s.ds.GetPackage(ctx, pkgPath, modulePath, version)
if err == nil {
s.servePackagePageWithPackage(ctx, w, r, pkg, version)
return
}
if !xerrors.Is(err, derrors.NotFound) {
log.Error(err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
if version == internal.LatestVersion {
// If we've already checked the latest version, then we know that this path
// is not a package at any version, so just skip ahead and serve the
// directory page.
s.serveDirectoryPage(w, r, pkgPath, modulePath, version)
return
}
dir, err := s.ds.GetDirectory(ctx, pkgPath, modulePath, version)
if err == nil {
s.serveDirectoryPageWithDirectory(ctx, w, r, dir, version)
return
}
if !xerrors.Is(err, derrors.NotFound) {
// The only error we expect is NotFound, so serve an 500 here, otherwise
// whatever response we resolve below might be inconsistent or misleading.
log.Errorf("error checking for directory: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
_, err = s.ds.GetPackage(ctx, pkgPath, modulePath, internal.LatestVersion)
if err == nil {
epage := &errorPage{
Message: fmt.Sprintf("Package %s@%s is not available.", pkgPath, displayVersion(version, modulePath)),
SecondaryMessage: template.HTML(
fmt.Sprintf(`There are other versions of this package that are! To view them, `+
`<a href="/%s?tab=versions">click here</a>.</p>`,
pkgPath)),
}
s.serveErrorPage(w, r, http.StatusNotFound, epage)
return
}
if !xerrors.Is(err, derrors.NotFound) {
// Unlike the error handling for GetDirectory above, we don't serve an
// InternalServerError here. The reasoning for this is that regardless of
// the result of GetPackage(..., "latest"), we're going to serve a NotFound
// response code. So the semantics of the endpoint are the same whether or
// not we get an unexpected error from GetPackage -- we just don't serve a
// more informative error response.
log.Errorf("error checking for latest package: %v", err)
}
s.serveErrorPage(w, r, http.StatusNotFound, nil)
}
func (s *Server) servePackagePageWithPackage(ctx context.Context, w http.ResponseWriter, r *http.Request, pkg *internal.VersionedPackage, requestedVersion string) {
pkgHeader, err := createPackage(&pkg.Package, &pkg.VersionInfo, requestedVersion == internal.LatestVersion)
if err != nil {
log.Errorf("error creating package header for %s@%s: %v", pkg.Path, pkg.Version, err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
tab := r.FormValue("tab")
settings, ok := packageTabLookup[tab]
if !ok {
var tab string
if pkg.IsRedistributable() {
tab = "doc"
} else {
tab = "overview"
}
http.Redirect(w, r, fmt.Sprintf(r.URL.Path+"?tab=%s", tab), http.StatusFound)
return
}
canShowDetails := pkg.IsRedistributable() || settings.AlwaysShowDetails
var details interface{}
if canShowDetails {
var err error
details, err = fetchDetailsForPackage(ctx, r, tab, s.ds, pkg)
if err != nil {
log.Errorf("error fetching page for %q: %v", tab, err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
}
page := &DetailsPage{
basePage: newBasePage(r, packageHTMLTitle(&pkg.Package)),
Title: packageTitle(&pkg.Package),
Settings: settings,
Header: pkgHeader,
BreadcrumbPath: breadcrumbPath(pkgHeader.Path, pkgHeader.Module.ModulePath,
pkgHeader.Module.LinkVersion),
Details: details,
CanShowDetails: canShowDetails,
Tabs: packageTabSettings,
PageType: "pkg",
}
s.servePage(w, settings.TemplateName, page)
}
// serveModulePage serves details pages for the module specified by modulePath
// and version.
func (s *Server) serveModulePage(w http.ResponseWriter, r *http.Request, modulePath, version string) {
ctx := r.Context()
if code, epage := checkPathAndVersion(ctx, s.ds, modulePath, version); code != http.StatusOK {
s.serveErrorPage(w, r, code, epage)
return
}
// This function handles top level behavior related to the existence of the
// requested modulePath@version:
// TODO: fix
// 1. If the module version exists, serve it.
// 2. else if we got any unexpected error, serve a server error
// 3. else if the error is NotFound, serve the directory page
// 3. else, we didn't find the module so there are two cases:
// a. We don't know anything about this module: just serve a 404
// b. We have valid versions for this module path, but `version` isn't
// one of them. Serve a 404 but recommend the other versions.
vi, err := s.ds.GetVersionInfo(ctx, modulePath, version)
if err == nil {
s.serveModulePageWithModule(ctx, w, r, vi, version)
return
}
if !xerrors.Is(err, derrors.NotFound) {
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
if version != internal.LatestVersion {
if _, err := s.ds.GetVersionInfo(ctx, modulePath, internal.LatestVersion); err != nil {
log.Errorf("error checking for latest module: %v", err)
} else {
epage := &errorPage{
Message: fmt.Sprintf("Module %s@%s is not available.", modulePath, displayVersion(version, modulePath)),
SecondaryMessage: template.HTML(
fmt.Sprintf(`There are other versions of this module that are! To view them, `+
`<a href="/mod/%s?tab=versions">click here</a>.</p>`,
modulePath)),
}
s.serveErrorPage(w, r, http.StatusNotFound, epage)
return
}
}
s.serveErrorPage(w, r, http.StatusNotFound, nil)
}
func (s *Server) serveModulePageWithModule(ctx context.Context, w http.ResponseWriter, r *http.Request, vi *internal.VersionInfo, requestedVersion string) {
licenses, err := s.ds.GetModuleLicenses(ctx, vi.ModulePath, vi.Version)
if err != nil {
log.Errorf("error getting module licenses: %v", err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
modHeader := createModule(vi, license.ToMetadatas(licenses), requestedVersion == internal.LatestVersion)
tab := r.FormValue("tab")
settings, ok := moduleTabLookup[tab]
if !ok {
tab = "overview"
settings = moduleTabLookup["overview"]
}
canShowDetails := modHeader.IsRedistributable || settings.AlwaysShowDetails
var details interface{}
if canShowDetails {
var err error
details, err = fetchDetailsForModule(ctx, r, tab, s.ds, vi, licenses)
if err != nil {
log.Errorf("error fetching page for %q: %v", tab, err)
s.serveErrorPage(w, r, http.StatusInternalServerError, nil)
return
}
}
page := &DetailsPage{
basePage: newBasePage(r, moduleHTMLTitle(vi.ModulePath)),
Title: moduleTitle(vi.ModulePath),
Settings: settings,
Header: modHeader,
BreadcrumbPath: breadcrumbPath(modHeader.ModulePath, modHeader.ModulePath, modHeader.LinkVersion),
Details: details,
CanShowDetails: canShowDetails,
Tabs: moduleTabSettings,
PageType: "mod",
}
s.servePage(w, settings.TemplateName, page)
}
// checkPathAndVersion verifies that the requested path and version are
// acceptable. The given path may be a module or package path.
func checkPathAndVersion(ctx context.Context, ds internal.DataSource, path, version string) (int, *errorPage) {
if version != internal.LatestVersion && !semver.IsValid(version) {
return http.StatusBadRequest, &errorPage{
Message: fmt.Sprintf("%q is not a valid semantic version.", version),
SecondaryMessage: suggestedSearch(path),
}
}
excluded, err := ds.IsExcluded(ctx, path)
if err != nil {
log.Errorf("error checking excluded path: %v", err)
return http.StatusInternalServerError, nil
}
if excluded {
// Return NotFound; don't let the user know that the package was excluded.
return http.StatusNotFound, nil
}
return http.StatusOK, nil
}
// parseDetailsURLPath returns the modulePath (if known),
// pkgPath and version specified by urlPath.
// urlPath is assumed to be a valid path following the structure:
// /<module-path>[@<version>/<suffix>]
//
// If <version> is not specified, internal.LatestVersion is used for the
// version. modulePath can only be determined if <version> is specified.
//
// Leading and trailing slashes in the urlPath are trimmed.
func parseDetailsURLPath(urlPath string) (pkgPath, modulePath, version string, err error) {
defer derrors.Wrap(&err, "parseDetailsURLPath(%q)", urlPath)
// This splits urlPath into either:
// /<module-path>[/<suffix>]
// or
// /<module-path>, @<version>/<suffix>
// or
// /<module-path>/<suffix>, @<version>
// TODO(b/140191811) The last URL route should redirect.
parts := strings.SplitN(urlPath, "@", 2)
basePath := strings.TrimSuffix(strings.TrimPrefix(parts[0], "/"), "/")
if len(parts) == 1 {
modulePath = internal.UnknownModulePath
version = internal.LatestVersion
pkgPath = basePath
} else {
// Parse the version and suffix from parts[1].
endParts := strings.Split(parts[1], "/")
suffix := strings.Join(endParts[1:], "/")
version = endParts[0]
if version == internal.LatestVersion {
return "", "", "", fmt.Errorf("invalid version: %q", version)
}
if suffix == "" {
modulePath = internal.UnknownModulePath
pkgPath = basePath
} else {
modulePath = basePath
pkgPath = basePath + "/" + suffix
}
}
if err := module.CheckImportPath(pkgPath); err != nil {
return "", "", "", fmt.Errorf("malformed path %q: %v", pkgPath, err)
}
if stdlib.Contains(pkgPath) {
modulePath = stdlib.ModulePath
}
return pkgPath, modulePath, version, nil
}
// LatestVersion returns the latest version of the package or module.
// The linkable form of the version is returned.
// It returns the empty string on error.
// It is intended to be used as an argument to middleware.LatestVersion.
func (s *Server) LatestVersion(ctx context.Context, packagePath, modulePath, pageType string) string {
v, err := s.latestVersion(ctx, packagePath, modulePath, pageType)
if err != nil {
// We get NotFound errors from directories; they clutter the log.
if !xerrors.Is(err, derrors.NotFound) {
log.Errorf("GetLatestVersion: %v", err)
}
return ""
}
return v
}
func (s *Server) latestVersion(ctx context.Context, packagePath, modulePath, pageType string) (_ string, err error) {
defer derrors.Wrap(&err, "latestVersion(ctx, %q, %q)", modulePath, packagePath)
var vi *internal.VersionInfo
switch pageType {
case "mod":
vi, err = s.ds.GetVersionInfo(ctx, modulePath, internal.LatestVersion)
if err != nil {
return "", err
}
case "pkg":
pkg, err := s.ds.GetPackage(ctx, packagePath, modulePath, internal.LatestVersion)
if err != nil {
return "", err
}
vi = &pkg.VersionInfo
default:
// For directories we don't have a well-defined latest version.
return "", nil
}
return linkVersion(vi.Version, modulePath), nil
}