blob: 0d97da6960f92a09e65d2ec0ad4d897bea17ca94 [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"
"errors"
"net/http"
"time"
"github.com/google/safehtml/template"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/log"
"golang.org/x/pkgsite/internal/middleware"
)
// DetailsPage contains data for a package of module details template.
type DetailsPage struct {
basePage
// Name is the name of the package or command name, or the full
// directory or module path.
Name string
// PageType is the type of page (pkg, cmd, dir, etc.).
PageType string
// CanShowDetails indicates whether details can be shown or must be
// hidden due to issues like license restrictions.
CanShowDetails bool
// Settings contains tab-specific metadata.
Settings TabSettings
// Details contains data specific to the type of page being rendered.
Details interface{}
// Header contains data to be rendered in the heading of all details pages.
Header interface{}
// Breadcrumb contains data used to render breadcrumb UI elements.
Breadcrumb breadcrumb
// Tabs contains data to render the varioius tabs on each details page.
Tabs []TabSettings
// CanonicalURLPath is the representation of the URL path for the details
// page, after the requested version and module path have been resolved.
// For example, if the latest version of /my.module/pkg is version v1.5.2,
// the canonical url for that path would be /my.module@v1.5.2/pkg
CanonicalURLPath string
}
// serveDetails handles requests for package/directory/module details pages. It
// expects paths of the form "[/mod]/<module-path>[@<version>?tab=<tab>]".
// stdlib module pages are handled at "/std", and requests to "/mod/std" will
// be redirected to that path.
func (s *Server) serveDetails(w http.ResponseWriter, r *http.Request, ds internal.DataSource) (err error) {
defer middleware.ElapsedStat(r.Context(), "serveDetails")()
if r.Method != http.MethodGet {
return &serverError{status: http.StatusMethodNotAllowed}
}
switch r.URL.Path {
case "/":
s.staticPageHandler("index.tmpl", "")(w, r)
return nil
case "/C":
// Package "C" is a special case: redirect to the Go Blog article on cgo.
// (This is what godoc.org does.)
http.Redirect(w, r, "https://golang.org/doc/articles/c_go_cgo.html", http.StatusMovedPermanently)
return nil
}
urlInfo, err := extractURLPathInfo(r.URL.Path)
if err != nil {
return &serverError{
status: http.StatusBadRequest,
err: err,
}
}
ctx := r.Context()
// If page statistics are enabled, use the "exp" query param to adjust
// the active experiments.
if s.serveStats {
ctx = setExperimentsFromQueryParam(ctx, r)
}
// Validate the fullPath and requestedVersion that were parsed.
if err := validatePathAndVersion(ctx, ds, urlInfo.fullPath, urlInfo.requestedVersion); err != nil {
return err
}
recordVersionTypeMetric(ctx, urlInfo.requestedVersion)
urlInfo.resolvedVersion = urlInfo.requestedVersion
um, err := ds.GetUnitMeta(ctx, urlInfo.fullPath, urlInfo.modulePath, urlInfo.requestedVersion)
if err != nil {
if !errors.Is(err, derrors.NotFound) {
return err
}
return s.servePathNotFoundPage(w, r, ds, urlInfo.fullPath, urlInfo.requestedVersion)
}
urlInfo.modulePath = um.ModulePath
urlInfo.resolvedVersion = um.Version
if urlInfo.requestedVersion == internal.MasterVersion {
// 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.
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
if _, err := s.queue.ScheduleFetch(ctx, urlInfo.modulePath, internal.MasterVersion, ""); err != nil {
log.Errorf(ctx, "serveDetails(%q): %v", r.URL.Path, err)
}
}()
}
return s.serveUnitPage(ctx, w, r, ds, um, urlInfo.requestedVersion)
}
func invalidVersionError(fullPath, requestedVersion string) error {
return &serverError{
status: http.StatusBadRequest,
epage: &errorPage{
messageTemplate: template.MakeTrustedTemplate(`
<h3 class="Error-message">{{.Version}} is not a valid semantic version.</h3>
<p class="Error-message">
To search for packages like {{.Path}}, <a href="/search?q={{.Path}}">click here</a>.
</p>`),
MessageData: struct{ Path, Version string }{fullPath, requestedVersion},
},
}
}
func proxydatasourceNotSupportedErr() error {
return &serverError{
status: http.StatusFailedDependency,
epage: &errorPage{
messageTemplate: template.MakeTrustedTemplate(
`<h3 class="Error-message">This page is not supported by the proxydatasource.</h3>`),
},
}
}
var (
keyVersionType = tag.MustNewKey("frontend.version_type")
versionTypeResults = stats.Int64(
"go-discovery/frontend_version_type_count",
"The version type of a request to package, module, or directory page.",
stats.UnitDimensionless,
)
VersionTypeCount = &view.View{
Name: "go-discovery/frontend_version_type/result_count",
Measure: versionTypeResults,
Aggregation: view.Count(),
Description: "version type results, by latest, master, or semver",
TagKeys: []tag.Key{keyVersionType},
}
)
func recordVersionTypeMetric(ctx context.Context, requestedVersion string) {
// Tag versions based on latest, master and semver.
v := requestedVersion
if semver.IsValid(v) {
v = "semver"
}
stats.RecordWithTags(ctx, []tag.Mutator{
tag.Upsert(keyVersionType, v),
}, versionTypeResults.M(1))
}