blob: 13463fdd4010e6480321f00e73a163f2e215a563 [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"
"strings"
"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/middleware"
"golang.org/x/pkgsite/internal/stdlib"
)
// serveDetails handles requests for package/directory/module details pages. It
// expects paths of the form "/<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")()
ctx := r.Context()
if r.Method != http.MethodGet && r.Method != http.MethodHead {
return &serverError{status: http.StatusMethodNotAllowed}
}
if r.URL.Path == "/" {
s.serveHomepage(ctx, w, r)
return nil
}
if strings.HasSuffix(r.URL.Path, "/") {
url := *r.URL
url.Path = strings.TrimSuffix(r.URL.Path, "/")
http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
return
}
// If page statistics are enabled, use the "exp" query param to adjust
// the active experiments.
if s.serveStats {
ctx = setExperimentsFromQueryParam(ctx, r)
}
urlInfo, err := extractURLPathInfo(r.URL.Path)
if err != nil {
var epage *errorPage
if uerr := new(userError); errors.As(err, &uerr) {
epage = &errorPage{MessageData: uerr.userMessage}
}
return &serverError{
status: http.StatusBadRequest,
err: err,
epage: epage,
}
}
if !isSupportedVersion(urlInfo.fullPath, urlInfo.requestedVersion) {
return invalidVersionError(urlInfo.fullPath, urlInfo.requestedVersion)
}
if urlPath := stdlibRedirectURL(urlInfo.fullPath); urlPath != "" {
http.Redirect(w, r, urlPath, http.StatusMovedPermanently)
return
}
if err := checkExcluded(ctx, ds, urlInfo.fullPath); err != nil {
return err
}
return s.serveUnitPage(ctx, w, r, ds, urlInfo)
}
func stdlibRedirectURL(fullPath string) string {
if !strings.HasPrefix(fullPath, stdlib.GitHubRepo) {
return ""
}
if fullPath == stdlib.GitHubRepo || fullPath == stdlib.GitHubRepo+"/src" {
return "/std"
}
urlPath2 := strings.TrimPrefix(strings.TrimPrefix(fullPath, stdlib.GitHubRepo+"/"), "src/")
if fullPath == urlPath2 {
return ""
}
return "/" + urlPath2
}
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 datasourceNotSupportedErr() error {
return &serverError{
status: http.StatusFailedDependency,
epage: &errorPage{
messageTemplate: template.MakeTrustedTemplate(
`<h3 class="Error-message">This page is not supported by this datasource.</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))
}