| // 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/mod/module" |
| "golang.org/x/mod/semver" |
| "golang.org/x/pkgsite/internal" |
| "golang.org/x/pkgsite/internal/derrors" |
| "golang.org/x/pkgsite/internal/experiment" |
| "golang.org/x/pkgsite/internal/stdlib" |
| ) |
| |
| // 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) serveDetails(w http.ResponseWriter, r *http.Request) error { |
| if r.URL.Path == "/" { |
| s.staticPageHandler("index.tmpl", "")(w, r) |
| return nil |
| } |
| parts := strings.SplitN(strings.TrimPrefix(r.URL.Path, "/"), "@", 2) |
| if stdlib.Contains(parts[0]) { |
| return s.serveStdLib(w, r) |
| } |
| return s.servePackageDetails(w, r) |
| } |
| |
| // parseDetailsURLPath parses a URL path that refers (or may refer) to something |
| // in the Go ecosystem. |
| // |
| // After trimming leading and trailing slashes, the path is expected to have one |
| // of three forms, and we divide it into three parts: a full path, a module |
| // path, and a version. |
| // |
| // 1. The path has no '@', like github.com/hashicorp/vault/api. |
| // This is the full path. The module path is unknown. So is the version, so we |
| // treat it as the latest version for whatever the path denotes. |
| // |
| // 2. The path has "@version" at the end, like github.com/hashicorp/vault/api@v1.2.3. |
| // We split this at the '@' into a full path (github.com/hashicorp/vault/api) |
| // and version (v1.2.3); the module path is still unknown. |
| // |
| // 3. The path has "@version" in the middle, like github.com/hashicorp/vault@v1.2.3/api. |
| // (We call this the "canonical" form of a path.) |
| // We remove the version to get the full path, which is again |
| // github.com/hashicorp/vault/api. The version is v1.2.3, and the module path is |
| // the part before the '@', github.com/hashicorp/vault. |
| // |
| // In one case, we do a little more than parse the urlPath into parts: if the full path |
| // could be a part of the standard library (because it has no '.'), we assume it |
| // is and set the modulePath to indicate the standard library. |
| func parseDetailsURLPath(urlPath string) (fullPath, 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 { // no '@' |
| modulePath = internal.UnknownModulePath |
| version = internal.LatestVersion |
| fullPath = basePath |
| } else { |
| // Parse the version and suffix from parts[1], the string after the '@'. |
| endParts := strings.Split(parts[1], "/") |
| suffix := strings.Join(endParts[1:], "/") |
| // The first path component after the '@' is the version. |
| version = endParts[0] |
| // You cannot explicitly write "latest" for the version. |
| if version == internal.LatestVersion { |
| return "", "", "", fmt.Errorf("invalid version: %q", version) |
| } |
| if suffix == "" { |
| // "@version" occurred at the end of the path; we don't know the module path. |
| modulePath = internal.UnknownModulePath |
| fullPath = basePath |
| } else { |
| // "@version" occurred in the middle of the path; the part before it |
| // is the module path. |
| modulePath = basePath |
| fullPath = basePath + "/" + suffix |
| } |
| } |
| // The full path must be a valid import path (that is, package path), even if it denotes |
| // a module, directory or collection. |
| if err := module.CheckImportPath(fullPath); err != nil { |
| return "", "", "", fmt.Errorf("malformed path %q: %v", fullPath, err) |
| } |
| |
| // If the full path is (or could be) in the standard library, change the |
| // module path to say so. But in that case, disallow versions in the middle, |
| // like "net@go1.14/http". That says that the module is "net", and it isn't. |
| if stdlib.Contains(fullPath) { |
| if modulePath != internal.UnknownModulePath { |
| return "", "", "", fmt.Errorf("non-final version in standard library path %q", urlPath) |
| } |
| modulePath = stdlib.ModulePath |
| } |
| return fullPath, modulePath, version, nil |
| } |
| |
| // 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) error { |
| if !isSupportedVersion(ctx, version) { |
| return &serverError{ |
| status: http.StatusBadRequest, |
| epage: &errorPage{ |
| Message: fmt.Sprintf("%q is not a valid semantic version.", version), |
| SecondaryMessage: suggestedSearch(path), |
| }, |
| } |
| } |
| excluded, err := ds.IsExcluded(ctx, path) |
| if err != nil { |
| return err |
| } |
| if excluded { |
| // Return NotFound; don't let the user know that the package was excluded. |
| return &serverError{status: http.StatusNotFound} |
| } |
| return nil |
| } |
| |
| // isSupportedVersion reports whether the version is supported by the frontend. |
| func isSupportedVersion(ctx context.Context, version string) bool { |
| if version == internal.LatestVersion || semver.IsValid(version) { |
| return true |
| } |
| if isActivePathAtMaster(ctx) { |
| return version == internal.MasterVersion |
| } |
| return false |
| } |
| |
| // isActveUseDirectories reports whether the experiment for reading from the |
| // paths-based data model is active. |
| func isActiveUseDirectories(ctx context.Context) bool { |
| return experiment.IsActive(ctx, internal.ExperimentInsertDirectories) && |
| experiment.IsActive(ctx, internal.ExperimentUseDirectories) |
| } |
| |
| // isActivePathAtMaster reports whether the experiment for viewing packages at |
| // master is active. |
| func isActivePathAtMaster(ctx context.Context) bool { |
| return experiment.IsActive(ctx, internal.ExperimentFrontendPackageAtMaster) && |
| isActiveFrontendFetch(ctx) |
| } |
| |
| // pathNotFoundError returns an error page with instructions on how to |
| // add a package or module to the site. pathType is always either the string |
| // "package" or "module". |
| func pathNotFoundError(ctx context.Context, pathType, fullPath, version string) error { |
| if isActiveFrontendFetch(ctx) { |
| return pathNotFoundErrorNew(fullPath, version) |
| } |
| return &serverError{ |
| status: http.StatusNotFound, |
| epage: &errorPage{ |
| Message: "404 Not Found", |
| SecondaryMessage: template.HTML(fmt.Sprintf(`If you think this is a valid %s path, you can try fetching it following the <a href="/about#adding-a-package">instructions here</a>.`, pathType)), |
| }, |
| } |
| } |
| |
| // pathNotFoundErrorNew returns an error page that provides the user with an |
| // option to fetch a path. |
| func pathNotFoundErrorNew(fullPath, version string) error { |
| path := fullPath |
| if version != internal.LatestVersion { |
| path = fmt.Sprintf("%s@%s", fullPath, version) |
| } |
| return &serverError{ |
| status: http.StatusNotFound, |
| epage: &errorPage{ |
| template: "notfound.tmpl", |
| Message: fmt.Sprintf("Oops! %q does not exist.", path), |
| SecondaryMessage: template.HTML("Check that you entered it correctly, or request to fetch it."), |
| }, |
| } |
| } |
| |
| // pathFoundAtLatestError returns an error page when the fullPath exists, but |
| // the version that is requested does not. |
| func pathFoundAtLatestError(ctx context.Context, pathType, fullPath, version string) error { |
| if isActiveFrontendFetch(ctx) { |
| return pathNotFoundErrorNew(fullPath, version) |
| } |
| return &serverError{ |
| status: http.StatusNotFound, |
| epage: &errorPage{ |
| Message: fmt.Sprintf("%s %s@%s is not available.", strings.Title(pathType), fullPath, displayVersion(version, fullPath)), |
| SecondaryMessage: template.HTML( |
| fmt.Sprintf(`There are other versions of this %s that are! To view them, `+ |
| `<a href="/%s?tab=versions">click here</a>.`, pathType, fullPath)), |
| }, |
| } |
| } |