blob: e4ecb70171d6ddf6d96551df0a08b09bc011a0d6 [file] [log] [blame]
// Copyright 2025 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"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/log"
)
const (
// depsDevBase is the base URL for requests to deps.dev.
// It should not include a trailing slash.
depsDevBase = "https://deps.dev"
// depsDevTimeout is the time budget for making requests to deps.dev.
depsDevTimeout = 250 * time.Millisecond
codeWikiPrefix = "/codewiki?"
attributionParams = "?utm_source=first_party_link&utm_medium=go_pkg_web&utm_campaign="
)
var (
codeWikiURLBase = "https://codewiki.google/"
codeWikiExistsURL = "https://codewiki.google/_/exists/"
codeWikiTimeout = 1 * time.Second
)
type fetcher func(context.Context, *http.Client) (string, error)
// newURLGenerator returns a function that will return a URL.
// If the URL can't be generated within the timeout then the empty string is returned.
func newURLGenerator(ctx context.Context, client *http.Client, serviceName string, timeout time.Duration, fetch fetcher) func() string {
ctx, cancel := context.WithTimeout(ctx, timeout)
url := make(chan string, 1)
go func() {
u, err := fetch(ctx, client)
switch {
case errors.Is(err, context.Canceled):
log.Warningf(ctx, "fetching url from %s: %v", serviceName, err)
case errors.Is(err, context.DeadlineExceeded):
log.Warningf(ctx, "fetching url from %s: %v", serviceName, err)
case err != nil:
log.Errorf(ctx, "fetching url from %s: %v", serviceName, err)
}
url <- u
}()
return func() string {
defer cancel()
return <-url
}
}
// depsDevURLGenerator returns a function that will return a URL for the given
// module version on deps.dev. If the URL can't be generated within
// depsDevTimeout then the empty string is returned instead.
func depsDevURLGenerator(ctx context.Context, client *http.Client, um *internal.UnitMeta) func() string {
fetch := func(ctx context.Context, client *http.Client) (string, error) {
return fetchDepsDevURL(ctx, client, um.ModulePath, um.Version)
}
return newURLGenerator(ctx, client, "deps.dev", depsDevTimeout, fetch)
}
// fetchDepsDevURL makes a request to deps.dev to check whether the given
// module version is known there, and if so it returns the link to that module
// version page on deps.dev.
func fetchDepsDevURL(ctx context.Context, client *http.Client, modulePath, version string) (string, error) {
u := depsDevBase + "/_/s/go" +
"/p/" + url.PathEscape(modulePath) +
"/v/" + url.PathEscape(version) +
"/exists"
req, err := http.NewRequestWithContext(ctx, "GET", u, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusNotFound:
return "", nil // No link to return.
case http.StatusOK:
// Handled below.
default:
return "", errors.New(resp.Status)
}
var r struct {
stem, Name, Version string
}
if err := json.NewDecoder(resp.Body).Decode(&r); err != nil {
return "", err
}
if r.Name == "" || r.Version == "" {
return "", errors.New("name or version unset in response")
}
return depsDevBase + "/go/" + url.PathEscape(r.Name) + "/" + url.PathEscape(r.Version), nil
}
// codeWikiURLGenerator returns a function that will return a URL for the given
// module version on codewiki. If the URL can't be generated within
// codeWikiTimeout then the empty string is returned instead.
func codeWikiURLGenerator(ctx context.Context, client *http.Client, um *internal.UnitMeta, recordClick bool) func() string {
fetch := func(ctx context.Context, client *http.Client) (string, error) {
return fetchCodeWikiURL(ctx, client, um, recordClick)
}
return newURLGenerator(ctx, client, "codewiki.google", codeWikiTimeout, fetch)
}
// fetchCodeWikiURL makes a request to codewiki to check whether the given
// path is known there, and if so it returns the link to that page.
func fetchCodeWikiURL(ctx context.Context, client *http.Client, um *internal.UnitMeta, recordClick bool) (string, error) {
path := um.ModulePath
if strings.HasPrefix(path, "golang.org/x/") {
path = strings.Replace(path, "golang.org/x/", "github.com/golang/", 1)
}
// TODO: Add support for other hosts as needed.
if !strings.HasPrefix(path, "github.com/") {
return "", nil
}
req, err := http.NewRequestWithContext(ctx, "GET", codeWikiExistsURL+path, nil)
if err != nil {
return "", err
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Handle 404 as a successful "not found" state rather than an error.
if resp.StatusCode == http.StatusNotFound {
return "", nil
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(resp.Status)
}
res := codeWikiURLBase + path + attributionParams + path
if recordClick {
v := url.Values{}
v.Set("url", res)
v.Set("module", um.ModulePath)
v.Set("package", um.Path)
res = codeWikiPrefix + v.Encode()
}
return res, nil
}