github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/main.go (about)

     1  // Copyright 2017 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"html/template"
    12  	"net/http"
    13  	"net/url"
    14  	"os"
    15  	"regexp"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"cloud.google.com/go/logging"
    22  	"cloud.google.com/go/logging/logadmin"
    23  	"github.com/google/syzkaller/dashboard/dashapi"
    24  	"github.com/google/syzkaller/pkg/debugtracer"
    25  	"github.com/google/syzkaller/pkg/email"
    26  	"github.com/google/syzkaller/pkg/hash"
    27  	"github.com/google/syzkaller/pkg/html"
    28  	"github.com/google/syzkaller/pkg/subsystem"
    29  	"github.com/google/syzkaller/pkg/vcs"
    30  	"golang.org/x/sync/errgroup"
    31  	"google.golang.org/api/iterator"
    32  	"google.golang.org/appengine/v2"
    33  	db "google.golang.org/appengine/v2/datastore"
    34  	"google.golang.org/appengine/v2/log"
    35  	"google.golang.org/appengine/v2/memcache"
    36  	"google.golang.org/appengine/v2/user"
    37  	proto "google.golang.org/genproto/googleapis/appengine/logging/v1"
    38  	ltype "google.golang.org/genproto/googleapis/logging/type"
    39  )
    40  
    41  // This file contains web UI http handlers.
    42  
    43  func initHTTPHandlers() {
    44  	http.Handle("/", handlerWrapper(handleMain))
    45  	http.Handle("/bug", handlerWrapper(handleBug))
    46  	http.Handle("/text", handlerWrapper(handleText))
    47  	http.Handle("/admin", handlerWrapper(handleAdmin))
    48  	http.Handle("/x/.config", handlerWrapper(handleTextX(textKernelConfig)))
    49  	http.Handle("/x/log.txt", handlerWrapper(handleTextX(textCrashLog)))
    50  	http.Handle("/x/report.txt", handlerWrapper(handleTextX(textCrashReport)))
    51  	http.Handle("/x/repro.syz", handlerWrapper(handleTextX(textReproSyz)))
    52  	http.Handle("/x/repro.c", handlerWrapper(handleTextX(textReproC)))
    53  	http.Handle("/x/repro.log", handlerWrapper(handleTextX(textReproLog)))
    54  	http.Handle("/x/patch.diff", handlerWrapper(handleTextX(textPatch)))
    55  	http.Handle("/x/bisect.txt", handlerWrapper(handleTextX(textLog)))
    56  	http.Handle("/x/error.txt", handlerWrapper(handleTextX(textError)))
    57  	http.Handle("/x/minfo.txt", handlerWrapper(handleTextX(textMachineInfo)))
    58  	for ns := range getConfig(context.Background()).Namespaces {
    59  		http.Handle("/"+ns, handlerWrapper(handleMain))
    60  		http.Handle("/"+ns+"/fixed", handlerWrapper(handleFixed)) // nolint: goconst // remove it with goconst 1.7.0+
    61  		http.Handle("/"+ns+"/invalid", handlerWrapper(handleInvalid))
    62  		http.Handle("/"+ns+"/graph/bugs", handlerWrapper(handleKernelHealthGraph))
    63  		http.Handle("/"+ns+"/graph/lifetimes", handlerWrapper(handleGraphLifetimes))
    64  		http.Handle("/"+ns+"/graph/fuzzing", handlerWrapper(handleGraphFuzzing))
    65  		http.Handle("/"+ns+"/graph/crashes", handlerWrapper(handleGraphCrashes))
    66  		http.Handle("/"+ns+"/repos", handlerWrapper(handleRepos))
    67  		http.Handle("/"+ns+"/bug-summaries", handlerWrapper(handleBugSummaries))
    68  		http.Handle("/"+ns+"/subsystems", handlerWrapper(handleSubsystemsList))
    69  		http.Handle("/"+ns+"/backports", handlerWrapper(handleBackports))
    70  		http.Handle("/"+ns+"/s/", handlerWrapper(handleSubsystemPage))
    71  		http.Handle("/"+ns+"/manager/", handlerWrapper(handleManagerPage))
    72  	}
    73  	http.HandleFunc("/cron/cache_update", cacheUpdate)
    74  	http.HandleFunc("/cron/minute_cache_update", handleMinuteCacheUpdate)
    75  	http.HandleFunc("/cron/deprecate_assets", handleDeprecateAssets)
    76  	http.HandleFunc("/cron/refresh_subsystems", handleRefreshSubsystems)
    77  	http.HandleFunc("/cron/subsystem_reports", handleSubsystemReports)
    78  }
    79  
    80  type uiMainPage struct {
    81  	Header         *uiHeader
    82  	Now            time.Time
    83  	Decommissioned bool
    84  	Managers       *uiManagerList
    85  	BugFilter      *uiBugFilter
    86  	Groups         []*uiBugGroup
    87  }
    88  
    89  type uiBugFilter struct {
    90  	Filter  *userBugFilter
    91  	DropURL func(string, string) string
    92  }
    93  
    94  func makeUIBugFilter(c context.Context, filter *userBugFilter) *uiBugFilter {
    95  	url := getCurrentURL(c)
    96  	return &uiBugFilter{
    97  		Filter: filter,
    98  		DropURL: func(name, value string) string {
    99  			return html.DropParam(url, name, value)
   100  		},
   101  	}
   102  }
   103  
   104  type uiManagerList struct {
   105  	RepoLink string
   106  	List     []*uiManager
   107  }
   108  
   109  func makeManagerList(managers []*uiManager, ns string) *uiManagerList {
   110  	return &uiManagerList{
   111  		RepoLink: fmt.Sprintf("/%s/repos", ns),
   112  		List:     managers,
   113  	}
   114  }
   115  
   116  type uiTerminalPage struct {
   117  	Header    *uiHeader
   118  	Now       time.Time
   119  	Bugs      *uiBugGroup
   120  	Stats     *uiBugStats
   121  	BugFilter *uiBugFilter
   122  }
   123  
   124  type uiBugStats struct {
   125  	Total          int
   126  	AutoObsoleted  int
   127  	ReproObsoleted int
   128  	UserObsoleted  int
   129  }
   130  
   131  func (stats *uiBugStats) Record(bug *Bug, bugReporting *BugReporting) {
   132  	stats.Total++
   133  	switch bug.Status {
   134  	case BugStatusInvalid:
   135  		if bugReporting.Auto {
   136  			stats.AutoObsoleted++
   137  		} else {
   138  			stats.UserObsoleted++
   139  		}
   140  		if bug.StatusReason == dashapi.InvalidatedByRevokedRepro {
   141  			stats.ReproObsoleted++
   142  		}
   143  	}
   144  }
   145  
   146  type uiReposPage struct {
   147  	Header *uiHeader
   148  	Repos  []*uiRepo
   149  }
   150  
   151  type uiRepo struct {
   152  	URL    string
   153  	Branch string
   154  	Alias  string
   155  }
   156  
   157  func (r uiRepo) String() string {
   158  	return r.URL + " " + r.Branch
   159  }
   160  
   161  func (r uiRepo) Equals(other *uiRepo) bool {
   162  	return r.String() == other.String()
   163  }
   164  
   165  type uiSubsystemPage struct {
   166  	Header   *uiHeader
   167  	Info     *uiSubsystem
   168  	Children []*uiSubsystem
   169  	Parents  []*uiSubsystem
   170  	Groups   []*uiBugGroup
   171  }
   172  
   173  type uiSubsystemsPage struct {
   174  	Header       *uiHeader
   175  	List         []*uiSubsystem
   176  	Unclassified *uiSubsystem
   177  	SomeHidden   bool
   178  	ShowAllURL   string
   179  }
   180  
   181  type uiSubsystem struct {
   182  	Name        string
   183  	Lists       string
   184  	Maintainers string
   185  	Open        uiSubsystemStats
   186  	Fixed       uiSubsystemStats
   187  }
   188  
   189  type uiSubsystemStats struct {
   190  	Count int
   191  	Link  string
   192  }
   193  
   194  type uiAdminPage struct {
   195  	Header              *uiHeader
   196  	Log                 []byte
   197  	Managers            *uiManagerList
   198  	RecentJobs          *uiJobList
   199  	PendingJobs         *uiJobList
   200  	RunningJobs         *uiJobList
   201  	TypeJobs            *uiJobList
   202  	FixBisectionsLink   string
   203  	CauseBisectionsLink string
   204  	JobOverviewLink     string
   205  	MemcacheStats       *memcache.Statistics
   206  	Stopped             bool
   207  	StopLink            string
   208  	MoreStopClicks      int
   209  }
   210  
   211  type uiManagerPage struct {
   212  	Header  *uiHeader
   213  	Manager *uiManager
   214  	Builds  []*uiBuild
   215  }
   216  
   217  type uiManager struct {
   218  	Now                   time.Time
   219  	Namespace             string
   220  	Name                  string
   221  	Link                  string // link to the syz-manager
   222  	PageLink              string // link to the manager page
   223  	CoverLink             string
   224  	CurrentBuild          *uiBuild
   225  	FailedBuildBugLink    string
   226  	FailedSyzBuildBugLink string
   227  	LastActive            time.Time
   228  	CurrentUpTime         time.Duration
   229  	MaxCorpus             int64
   230  	MaxCover              int64
   231  	TotalFuzzingTime      time.Duration
   232  	TotalCrashes          int64
   233  	TotalExecs            int64
   234  	TotalExecsBad         bool // highlight TotalExecs in red
   235  }
   236  
   237  type uiBuild struct {
   238  	Time                time.Time
   239  	SyzkallerCommit     string
   240  	SyzkallerCommitLink string
   241  	SyzkallerCommitDate time.Time
   242  	KernelRepo          string
   243  	KernelBranch        string
   244  	KernelAlias         string
   245  	KernelCommit        string
   246  	KernelCommitLink    string
   247  	KernelCommitTitle   string
   248  	KernelCommitDate    time.Time
   249  	KernelConfigLink    string
   250  	Assets              []*uiAsset
   251  }
   252  
   253  type uiBugDiscussion struct {
   254  	Subject  string
   255  	Link     string
   256  	Total    int
   257  	External int
   258  	Last     time.Time
   259  }
   260  
   261  type uiReproAttempt struct {
   262  	Time    time.Time
   263  	Manager string
   264  	LogLink string
   265  }
   266  
   267  type uiBugPage struct {
   268  	Header          *uiHeader
   269  	Now             time.Time
   270  	Bug             *uiBug
   271  	BisectCause     *uiJob
   272  	BisectFix       *uiJob
   273  	FixCandidate    *uiJob
   274  	Sections        []*uiCollapsible
   275  	SampleReport    template.HTML
   276  	Crashes         *uiCrashTable
   277  	TestPatchJobs   *uiJobList
   278  	LabelGroups     []*uiBugLabelGroup
   279  	DebugSubsystems string
   280  }
   281  
   282  type uiBugLabelGroup struct {
   283  	Name   string
   284  	Labels []*uiBugLabel
   285  }
   286  
   287  const (
   288  	sectionBugList        = "bug_list"
   289  	sectionJobList        = "job_list"
   290  	sectionDiscussionList = "discussion_list"
   291  	sectionTestResults    = "test_results"
   292  	sectionReproAttempts  = "repro_attempts"
   293  )
   294  
   295  type uiCollapsible struct {
   296  	Title string
   297  	Show  bool   // By default it's collapsed.
   298  	Type  string // Template system understands it.
   299  	Value interface{}
   300  }
   301  
   302  func makeCollapsibleBugJobs(title string, jobs []*uiJob) *uiCollapsible {
   303  	return &uiCollapsible{
   304  		Title: fmt.Sprintf("%s (%d)", title, len(jobs)),
   305  		Type:  sectionJobList,
   306  		Value: &uiJobList{
   307  			PerBug: true,
   308  			Jobs:   jobs,
   309  		},
   310  	}
   311  }
   312  
   313  type uiBugGroup struct {
   314  	Now           time.Time
   315  	Caption       string
   316  	Fragment      string
   317  	Namespace     string
   318  	ShowNamespace bool
   319  	ShowPatch     bool
   320  	ShowPatched   bool
   321  	ShowStatus    bool
   322  	ShowIndex     int
   323  	Bugs          []*uiBug
   324  	DispLastAct   bool
   325  	DispDiscuss   bool
   326  }
   327  
   328  type uiJobList struct {
   329  	Title  string
   330  	PerBug bool
   331  	Jobs   []*uiJob
   332  }
   333  
   334  type uiCommit struct {
   335  	Hash   string
   336  	Repo   string
   337  	Branch string
   338  	Title  string
   339  	Link   string
   340  	Author string
   341  	CC     []string
   342  	Date   time.Time
   343  }
   344  
   345  type uiBug struct {
   346  	Namespace      string
   347  	Title          string
   348  	NumCrashes     int64
   349  	NumCrashesBad  bool
   350  	BisectCause    BisectStatus
   351  	BisectFix      BisectStatus
   352  	FirstTime      time.Time
   353  	LastTime       time.Time
   354  	ReportedTime   time.Time
   355  	ClosedTime     time.Time
   356  	ReproLevel     dashapi.ReproLevel
   357  	ReportingIndex int
   358  	Status         string
   359  	Link           string
   360  	ExternalLink   string
   361  	CreditEmail    string
   362  	Commits        []*uiCommit
   363  	PatchedOn      []string
   364  	MissingOn      []string
   365  	NumManagers    int
   366  	LastActivity   time.Time
   367  	Labels         []*uiBugLabel
   368  	Discussions    DiscussionSummary
   369  	ID             string
   370  }
   371  
   372  type uiBugLabel struct {
   373  	Name string
   374  	Link string
   375  }
   376  
   377  type uiCrash struct {
   378  	Title           string
   379  	Manager         string
   380  	Time            time.Time
   381  	Maintainers     string
   382  	LogLink         string
   383  	LogHasStrace    bool
   384  	ReportLink      string
   385  	ReproSyzLink    string
   386  	ReproCLink      string
   387  	ReproIsRevoked  bool
   388  	MachineInfoLink string
   389  	Assets          []*uiAsset
   390  	*uiBuild
   391  }
   392  
   393  type uiAsset struct {
   394  	Title       string
   395  	DownloadURL string
   396  }
   397  
   398  type uiCrashTable struct {
   399  	Crashes []*uiCrash
   400  	Caption string
   401  }
   402  
   403  type uiJob struct {
   404  	*dashapi.JobInfo
   405  	Crash             *uiCrash
   406  	InvalidateJobLink string
   407  	RestartJobLink    string
   408  	FixCandidate      bool
   409  }
   410  
   411  type uiBackportGroup struct {
   412  	From       *uiRepo
   413  	To         *uiRepo
   414  	Namespaces []string
   415  	List       []*uiBackport
   416  }
   417  
   418  type uiBackport struct {
   419  	Commit *uiCommit
   420  	Bugs   map[string][]*uiBug // namespace -> list of related bugs in it
   421  }
   422  
   423  type uiBackportsPage struct {
   424  	Header           *uiHeader
   425  	Groups           []*uiBackportGroup
   426  	DisplayNamespace func(string) string
   427  }
   428  
   429  type userBugFilter struct {
   430  	Manager     string // show bugs that happened on the manager
   431  	OnlyManager string // show bugs that happened ONLY on the manager
   432  	Labels      []string
   433  	NoSubsystem bool
   434  }
   435  
   436  func MakeBugFilter(r *http.Request) (*userBugFilter, error) {
   437  	if err := r.ParseForm(); err != nil {
   438  		return nil, err
   439  	}
   440  	return &userBugFilter{
   441  		NoSubsystem: r.FormValue("no_subsystem") != "",
   442  		Manager:     r.FormValue("manager"),
   443  		OnlyManager: r.FormValue("only_manager"),
   444  		Labels:      r.Form["label"],
   445  	}, nil
   446  }
   447  
   448  func (filter *userBugFilter) MatchManagerName(name string) bool {
   449  	target := filter.ManagerName()
   450  	return target == "" || target == name
   451  }
   452  
   453  func (filter *userBugFilter) ManagerName() string {
   454  	if filter != nil && filter.OnlyManager != "" {
   455  		return filter.OnlyManager
   456  	}
   457  	if filter != nil && filter.Manager != "" {
   458  		return filter.Manager
   459  	}
   460  	return ""
   461  }
   462  
   463  func (filter *userBugFilter) MatchBug(bug *Bug) bool {
   464  	if filter == nil {
   465  		return true
   466  	}
   467  	if filter.OnlyManager != "" && (len(bug.HappenedOn) != 1 || bug.HappenedOn[0] != filter.OnlyManager) {
   468  		return false
   469  	}
   470  	if filter.Manager != "" && !stringInList(bug.HappenedOn, filter.Manager) {
   471  		return false
   472  	}
   473  	if filter.NoSubsystem && len(bug.LabelValues(SubsystemLabel)) > 0 {
   474  		return false
   475  	}
   476  	for _, rawLabel := range filter.Labels {
   477  		label, value := splitLabel(rawLabel)
   478  		if !bug.HasLabel(label, value) {
   479  			return false
   480  		}
   481  	}
   482  	return true
   483  }
   484  
   485  func (filter *userBugFilter) Hash() string {
   486  	return hash.String([]byte(fmt.Sprintf("%#v", filter)))
   487  }
   488  
   489  func splitLabel(rawLabel string) (BugLabelType, string) {
   490  	label, value, _ := strings.Cut(rawLabel, ":")
   491  	return BugLabelType(label), value
   492  }
   493  
   494  func (filter *userBugFilter) Any() bool {
   495  	if filter == nil {
   496  		return false
   497  	}
   498  	return len(filter.Labels) > 0 || filter.OnlyManager != "" || filter.Manager != "" || filter.NoSubsystem
   499  }
   500  
   501  // handleMain serves main page.
   502  func handleMain(c context.Context, w http.ResponseWriter, r *http.Request) error {
   503  	hdr, err := commonHeader(c, r, w, "")
   504  	if err != nil {
   505  		return err
   506  	}
   507  	accessLevel := accessLevel(c, r)
   508  	filter, err := MakeBugFilter(r)
   509  	if err != nil {
   510  		return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest)
   511  	}
   512  	managers, err := CachedUIManagers(c, accessLevel, hdr.Namespace, filter)
   513  	if err != nil {
   514  		return err
   515  	}
   516  	groups, err := fetchNamespaceBugs(c, accessLevel, hdr.Namespace, filter)
   517  	if err != nil {
   518  		return err
   519  	}
   520  	for _, group := range groups {
   521  		if getNsConfig(c, hdr.Namespace).DisplayDiscussions {
   522  			group.DispDiscuss = true
   523  		} else {
   524  			group.DispLastAct = true
   525  		}
   526  	}
   527  	data := &uiMainPage{
   528  		Header:         hdr,
   529  		Decommissioned: getNsConfig(c, hdr.Namespace).Decommissioned,
   530  		Now:            timeNow(c),
   531  		Groups:         groups,
   532  		Managers:       makeManagerList(managers, hdr.Namespace),
   533  		BugFilter:      makeUIBugFilter(c, filter),
   534  	}
   535  
   536  	if r.FormValue("json") == "1" {
   537  		w.Header().Set("Content-Type", "application/json")
   538  		return writeJSONVersionOf(w, data)
   539  	}
   540  
   541  	return serveTemplate(w, "main.html", data)
   542  }
   543  
   544  func handleFixed(c context.Context, w http.ResponseWriter, r *http.Request) error {
   545  	return handleTerminalBugList(c, w, r, &TerminalBug{
   546  		Status:      BugStatusFixed,
   547  		Subpage:     "/fixed", // nolint: goconst // TODO: remove it once goconst 1.7.0+ landed
   548  		ShowPatch:   true,
   549  		ShowPatched: true,
   550  	})
   551  }
   552  
   553  func handleInvalid(c context.Context, w http.ResponseWriter, r *http.Request) error {
   554  	return handleTerminalBugList(c, w, r, &TerminalBug{
   555  		Status:    BugStatusInvalid,
   556  		Subpage:   "/invalid",
   557  		ShowPatch: false,
   558  		ShowStats: true,
   559  	})
   560  }
   561  
   562  func handleManagerPage(c context.Context, w http.ResponseWriter, r *http.Request) error {
   563  	hdr, err := commonHeader(c, r, w, "")
   564  	if err != nil {
   565  		return err
   566  	}
   567  	managers, err := CachedUIManagers(c, accessLevel(c, r), hdr.Namespace, nil)
   568  	if err != nil {
   569  		return err
   570  	}
   571  	var manager *uiManager
   572  	if pos := strings.Index(r.URL.Path, "/manager/"); pos != -1 {
   573  		manager = findManager(managers, r.URL.Path[pos+len("/manager/"):])
   574  	}
   575  	if manager == nil {
   576  		return fmt.Errorf("%w: manager is unknown", ErrClientBadRequest)
   577  	}
   578  	builds, err := loadBuilds(c, hdr.Namespace, manager.Name, BuildNormal)
   579  	if err != nil {
   580  		return fmt.Errorf("failed to query builds: %w", err)
   581  	}
   582  	managerPage := &uiManagerPage{Manager: manager, Header: hdr}
   583  	for _, build := range builds {
   584  		managerPage.Builds = append(managerPage.Builds, makeUIBuild(c, build, false))
   585  	}
   586  	return serveTemplate(w, "manager.html", managerPage)
   587  }
   588  
   589  func findManager(managers []*uiManager, name string) *uiManager {
   590  	for _, mgr := range managers {
   591  		if mgr.Name == name {
   592  			return mgr
   593  		}
   594  	}
   595  	return nil
   596  }
   597  
   598  func handleSubsystemPage(c context.Context, w http.ResponseWriter, r *http.Request) error {
   599  	hdr, err := commonHeader(c, r, w, "")
   600  	if err != nil {
   601  		return err
   602  	}
   603  	service := getNsConfig(c, hdr.Namespace).Subsystems.Service
   604  	if service == nil {
   605  		return fmt.Errorf("%w: the namespace does not have subsystems", ErrClientBadRequest)
   606  	}
   607  	var subsystem *subsystem.Subsystem
   608  	if pos := strings.Index(r.URL.Path, "/s/"); pos != -1 {
   609  		name := r.URL.Path[pos+3:]
   610  		if newName := getNsConfig(c, hdr.Namespace).Subsystems.Redirect[name]; newName != "" {
   611  			http.Redirect(w, r, r.URL.Path[:pos+3]+newName, http.StatusMovedPermanently)
   612  			return nil
   613  		}
   614  		subsystem = service.ByName(name)
   615  	}
   616  	if subsystem == nil {
   617  		return fmt.Errorf("%w: the subsystem is not found in the path %v", ErrClientBadRequest, r.URL.Path)
   618  	}
   619  	groups, err := fetchNamespaceBugs(c, accessLevel(c, r),
   620  		hdr.Namespace, &userBugFilter{
   621  			Labels: []string{
   622  				BugLabel{
   623  					Label: SubsystemLabel,
   624  					Value: subsystem.Name,
   625  				}.String(),
   626  			},
   627  		})
   628  	if err != nil {
   629  		return err
   630  	}
   631  	for _, group := range groups {
   632  		group.DispDiscuss = getNsConfig(c, hdr.Namespace).DisplayDiscussions
   633  	}
   634  	cached, err := CacheGet(c, r, hdr.Namespace)
   635  	if err != nil {
   636  		return err
   637  	}
   638  	children := []*uiSubsystem{}
   639  	for _, item := range service.Children(subsystem) {
   640  		uiChild := createUISubsystem(hdr.Namespace, item, cached)
   641  		if uiChild.Open.Count+uiChild.Fixed.Count == 0 {
   642  			continue
   643  		}
   644  		children = append(children, uiChild)
   645  	}
   646  	parents := []*uiSubsystem{}
   647  	for _, item := range subsystem.Parents {
   648  		parents = append(parents, createUISubsystem(hdr.Namespace, item, cached))
   649  	}
   650  	sort.Slice(children, func(i, j int) bool { return children[i].Name < children[j].Name })
   651  	return serveTemplate(w, "subsystem_page.html", &uiSubsystemPage{
   652  		Header:   hdr,
   653  		Info:     createUISubsystem(hdr.Namespace, subsystem, cached),
   654  		Children: children,
   655  		Parents:  parents,
   656  		Groups:   groups,
   657  	})
   658  }
   659  
   660  func handleBackports(c context.Context, w http.ResponseWriter, r *http.Request) error {
   661  	hdr, err := commonHeader(c, r, w, "")
   662  	if err != nil {
   663  		return err
   664  	}
   665  	backports, err := loadAllBackports(c)
   666  	if err != nil {
   667  		return err
   668  	}
   669  	var groups []*uiBackportGroup
   670  	accessLevel := accessLevel(c, r)
   671  	for _, backport := range backports {
   672  		outgoing := stringInList(backport.FromNs, hdr.Namespace)
   673  		ui := &uiBackport{
   674  			Commit: backport.Commit,
   675  			Bugs:   map[string][]*uiBug{},
   676  		}
   677  		incoming := false
   678  		for _, bug := range backport.Bugs {
   679  			if accessLevel < bug.sanitizeAccess(c, accessLevel) {
   680  				continue
   681  			}
   682  			if !outgoing && bug.Namespace != hdr.Namespace {
   683  				// If it's an incoming backport, don't include other namespaces.
   684  				continue
   685  			}
   686  			if bug.Namespace == hdr.Namespace {
   687  				incoming = true
   688  			}
   689  			ui.Bugs[bug.Namespace] = append(ui.Bugs[bug.Namespace],
   690  				createUIBug(c, bug, nil, nil))
   691  		}
   692  		if len(ui.Bugs) == 0 {
   693  			continue
   694  		}
   695  
   696  		// Display either backports to/from repos of the namespace
   697  		// or the backports that affect bugs from the current namespace.
   698  		if !outgoing && !incoming {
   699  			continue
   700  		}
   701  		var group *uiBackportGroup
   702  		for _, existing := range groups {
   703  			if backport.From.Equals(existing.From) &&
   704  				backport.To.Equals(existing.To) {
   705  				group = existing
   706  				break
   707  			}
   708  		}
   709  		if group == nil {
   710  			group = &uiBackportGroup{
   711  				From: backport.From,
   712  				To:   backport.To,
   713  			}
   714  			groups = append(groups, group)
   715  		}
   716  		group.List = append(group.List, ui)
   717  	}
   718  	for _, group := range groups {
   719  		var nsList []string
   720  		for _, backport := range group.List {
   721  			for ns := range backport.Bugs {
   722  				nsList = append(nsList, ns)
   723  			}
   724  		}
   725  		nsList = unique(nsList)
   726  		sort.Strings(nsList)
   727  		group.Namespaces = nsList
   728  	}
   729  	sort.Slice(groups, func(i, j int) bool {
   730  		return groups[i].From.String()+groups[i].To.String() <
   731  			groups[j].From.String()+groups[j].To.String()
   732  	})
   733  	return serveTemplate(w, "backports.html", &uiBackportsPage{
   734  		Header: hdr,
   735  		Groups: groups,
   736  		DisplayNamespace: func(ns string) string {
   737  			return getNsConfig(c, ns).DisplayTitle
   738  		},
   739  	})
   740  }
   741  
   742  type rawBackport struct {
   743  	Commit *uiCommit
   744  	From   *uiRepo
   745  	FromNs []string // namespaces that correspond to From
   746  	To     *uiRepo
   747  	Bugs   []*Bug
   748  }
   749  
   750  func loadAllBackports(c context.Context) ([]*rawBackport, error) {
   751  	bugs, jobs, _, err := relevantBackportJobs(c)
   752  	if err != nil {
   753  		return nil, err
   754  	}
   755  	var ret []*rawBackport
   756  	perCommit := map[string]*rawBackport{}
   757  	for i, job := range jobs {
   758  		jobCommit := job.Commits[0]
   759  		to := &uiRepo{URL: job.MergeBaseRepo, Branch: job.MergeBaseBranch}
   760  		from := &uiRepo{URL: job.KernelRepo, Branch: job.KernelBranch}
   761  		commit := &uiCommit{
   762  			Hash:   jobCommit.Hash,
   763  			Title:  jobCommit.Title,
   764  			Link:   vcs.CommitLink(from.URL, jobCommit.Hash),
   765  			Repo:   from.URL,
   766  			Branch: from.Branch,
   767  		}
   768  
   769  		hash := from.String() + to.String() + commit.Hash
   770  		backport := perCommit[hash]
   771  		if backport == nil {
   772  			backport = &rawBackport{
   773  				From:   from,
   774  				FromNs: namespacesForRepo(c, from.URL, from.Branch),
   775  				To:     to,
   776  				Commit: commit}
   777  			ret = append(ret, backport)
   778  			perCommit[hash] = backport
   779  		}
   780  		backport.Bugs = append(backport.Bugs, bugs[i])
   781  	}
   782  	return ret, nil
   783  }
   784  
   785  func namespacesForRepo(c context.Context, url, branch string) []string {
   786  	var ret []string
   787  	for ns, cfg := range getConfig(c).Namespaces {
   788  		has := false
   789  		for _, repo := range cfg.Repos {
   790  			if repo.NoPoll {
   791  				continue
   792  			}
   793  			if repo.URL == url && repo.Branch == branch {
   794  				has = true
   795  				break
   796  			}
   797  		}
   798  		if has {
   799  			ret = append(ret, ns)
   800  		}
   801  	}
   802  	return ret
   803  }
   804  
   805  func handleRepos(c context.Context, w http.ResponseWriter, r *http.Request) error {
   806  	hdr, err := commonHeader(c, r, w, "")
   807  	if err != nil {
   808  		return err
   809  	}
   810  	repos, err := loadRepos(c, hdr.Namespace)
   811  	if err != nil {
   812  		return err
   813  	}
   814  	return serveTemplate(w, "repos.html", &uiReposPage{
   815  		Header: hdr,
   816  		Repos:  repos,
   817  	})
   818  }
   819  
   820  type TerminalBug struct {
   821  	Status      int
   822  	Subpage     string
   823  	ShowPatch   bool
   824  	ShowPatched bool
   825  	ShowStats   bool
   826  	Filter      *userBugFilter
   827  }
   828  
   829  func handleTerminalBugList(c context.Context, w http.ResponseWriter, r *http.Request, typ *TerminalBug) error {
   830  	accessLevel := accessLevel(c, r)
   831  	hdr, err := commonHeader(c, r, w, "")
   832  	if err != nil {
   833  		return err
   834  	}
   835  	hdr.Subpage = typ.Subpage
   836  	typ.Filter, err = MakeBugFilter(r)
   837  	if err != nil {
   838  		return fmt.Errorf("%w: failed to parse URL parameters", ErrClientBadRequest)
   839  	}
   840  	extraBugs := []*Bug{}
   841  	if typ.Status == BugStatusFixed {
   842  		// Mix in bugs that have pending fixes.
   843  		extraBugs, err = fetchFixPendingBugs(c, hdr.Namespace, typ.Filter.ManagerName())
   844  		if err != nil {
   845  			return err
   846  		}
   847  	}
   848  	bugs, stats, err := fetchTerminalBugs(c, accessLevel, hdr.Namespace, typ, extraBugs)
   849  	if err != nil {
   850  		return err
   851  	}
   852  	if !typ.ShowStats {
   853  		stats = nil
   854  	}
   855  	data := &uiTerminalPage{
   856  		Header:    hdr,
   857  		Now:       timeNow(c),
   858  		Bugs:      bugs,
   859  		Stats:     stats,
   860  		BugFilter: makeUIBugFilter(c, typ.Filter),
   861  	}
   862  
   863  	if r.FormValue("json") == "1" {
   864  		w.Header().Set("Content-Type", "application/json")
   865  		return writeJSONVersionOf(w, data)
   866  	}
   867  
   868  	return serveTemplate(w, "terminal.html", data)
   869  }
   870  
   871  func handleAdmin(c context.Context, w http.ResponseWriter, r *http.Request) error {
   872  	accessLevel := accessLevel(c, r)
   873  	if accessLevel != AccessAdmin {
   874  		return ErrAccess
   875  	}
   876  	switch action := r.FormValue("action"); action {
   877  	case "":
   878  	case "memcache_flush":
   879  		if err := memcache.Flush(c); err != nil {
   880  			return fmt.Errorf("failed to flush memcache: %w", err)
   881  		}
   882  	case "invalidate_bisection":
   883  		return handleInvalidateBisection(c, w, r)
   884  	case "emergency_stop":
   885  		if err := recordEmergencyStop(c); err != nil {
   886  			return fmt.Errorf("failed to record an emergency stop: %w", err)
   887  		}
   888  	default:
   889  		return fmt.Errorf("%w: unknown action %q", ErrClientBadRequest, action)
   890  	}
   891  	hdr, err := commonHeader(c, r, w, "")
   892  	if err != nil {
   893  		return err
   894  	}
   895  	var (
   896  		memcacheStats *memcache.Statistics
   897  		managers      []*uiManager
   898  		errorLog      []byte
   899  		recentJobs    []*uiJob
   900  		pendingJobs   []*uiJob
   901  		runningJobs   []*uiJob
   902  		typeJobs      []*uiJob
   903  	)
   904  	g, _ := errgroup.WithContext(context.Background())
   905  	g.Go(func() error {
   906  		var err error
   907  		memcacheStats, err = memcache.Stats(c)
   908  		return err
   909  	})
   910  	g.Go(func() error {
   911  		var err error
   912  		managers, err = loadManagers(c, accessLevel, "", nil)
   913  		return err
   914  	})
   915  	g.Go(func() error {
   916  		var err error
   917  		errorLog, err = fetchErrorLogs(c)
   918  		return err
   919  	})
   920  	if r.FormValue("job_type") != "" {
   921  		value, err := strconv.Atoi(r.FormValue("job_type"))
   922  		if err != nil {
   923  			return fmt.Errorf("%w: %w", ErrClientBadRequest, err)
   924  		}
   925  		g.Go(func() error {
   926  			var err error
   927  			typeJobs, err = loadJobsOfType(c, JobType(value))
   928  			return err
   929  		})
   930  	} else {
   931  		g.Go(func() error {
   932  			var err error
   933  			recentJobs, err = loadRecentJobs(c)
   934  			return err
   935  		})
   936  		g.Go(func() error {
   937  			var err error
   938  			pendingJobs, err = loadPendingJobs(c)
   939  			return err
   940  		})
   941  		g.Go(func() error {
   942  			var err error
   943  			runningJobs, err = loadRunningJobs(c)
   944  			return err
   945  		})
   946  	}
   947  	alreadyStopped := false
   948  	g.Go(func() error {
   949  		var err error
   950  		alreadyStopped, err = emergentlyStopped(c)
   951  		return err
   952  	})
   953  	err = g.Wait()
   954  	if err != nil {
   955  		return err
   956  	}
   957  	data := &uiAdminPage{
   958  		Header:         hdr,
   959  		Log:            errorLog,
   960  		Managers:       makeManagerList(managers, hdr.Namespace),
   961  		MemcacheStats:  memcacheStats,
   962  		Stopped:        alreadyStopped,
   963  		MoreStopClicks: 2,
   964  		StopLink:       html.AmendURL("/admin", "stop_clicked", "1"),
   965  	}
   966  	if r.FormValue("stop_clicked") != "" {
   967  		data.MoreStopClicks = 1
   968  		data.StopLink = html.AmendURL("/admin", "action", "emergency_stop")
   969  	}
   970  	if r.FormValue("job_type") != "" {
   971  		data.TypeJobs = &uiJobList{Title: "Last jobs:", Jobs: typeJobs}
   972  		data.JobOverviewLink = "/admin"
   973  	} else {
   974  		data.RecentJobs = &uiJobList{Title: "Recent jobs:", Jobs: recentJobs}
   975  		data.RunningJobs = &uiJobList{Title: "Running jobs:", Jobs: runningJobs}
   976  		data.PendingJobs = &uiJobList{Title: "Pending jobs:", Jobs: pendingJobs}
   977  		data.FixBisectionsLink = html.AmendURL("/admin", "job_type", fmt.Sprintf("%d", JobBisectFix))
   978  		data.CauseBisectionsLink = html.AmendURL("/admin", "job_type", fmt.Sprintf("%d", JobBisectCause))
   979  	}
   980  	return serveTemplate(w, "admin.html", data)
   981  }
   982  
   983  // handleBug serves page about a single bug (which is passed in id argument).
   984  // nolint: funlen, gocyclo
   985  func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error {
   986  	bug, err := findBugByID(c, r)
   987  	if err != nil {
   988  		return fmt.Errorf("%w: %w", ErrClientNotFound, err)
   989  	}
   990  	accessLevel := accessLevel(c, r)
   991  	if err := checkAccessLevel(c, r, bug.sanitizeAccess(c, accessLevel)); err != nil {
   992  		return err
   993  	}
   994  	if r.FormValue("debug_subsystems") != "" && accessLevel == AccessAdmin {
   995  		return debugBugSubsystems(c, w, bug)
   996  	}
   997  	hdr, err := commonHeader(c, r, w, bug.Namespace)
   998  	if err != nil {
   999  		return err
  1000  	}
  1001  	state, err := loadReportingState(c)
  1002  	if err != nil {
  1003  		return err
  1004  	}
  1005  	managers, err := CachedManagerList(c, bug.Namespace)
  1006  	if err != nil {
  1007  		return err
  1008  	}
  1009  	sections := []*uiCollapsible{}
  1010  	if bug.DupOf != "" {
  1011  		dup := new(Bug)
  1012  		if err := db.Get(c, db.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil {
  1013  			return err
  1014  		}
  1015  		if accessLevel >= dup.sanitizeAccess(c, accessLevel) {
  1016  			sections = append(sections, &uiCollapsible{
  1017  				Title: "Duplicate of",
  1018  				Show:  true,
  1019  				Type:  sectionBugList,
  1020  				Value: &uiBugGroup{
  1021  					Now:  timeNow(c),
  1022  					Bugs: []*uiBug{createUIBug(c, dup, state, managers)},
  1023  				},
  1024  			})
  1025  		}
  1026  	}
  1027  	uiBug := createUIBug(c, bug, state, managers)
  1028  	crashes, sampleReport, err := loadCrashesForBug(c, bug)
  1029  	if err != nil {
  1030  		return err
  1031  	}
  1032  	crashesTable := &uiCrashTable{
  1033  		Crashes: crashes,
  1034  		Caption: fmt.Sprintf("Crashes (%d)", bug.NumCrashes),
  1035  	}
  1036  	dups, err := loadDupsForBug(c, r, bug, state, managers)
  1037  	if err != nil {
  1038  		return err
  1039  	}
  1040  	if len(dups.Bugs) > 0 {
  1041  		sections = append(sections, &uiCollapsible{
  1042  			Title: fmt.Sprintf("Duplicate bugs (%d)", len(dups.Bugs)),
  1043  			Type:  sectionBugList,
  1044  			Value: dups,
  1045  		})
  1046  	}
  1047  	discussions, err := getBugDiscussionsUI(c, bug)
  1048  	if err != nil {
  1049  		return err
  1050  	}
  1051  	if len(discussions) > 0 {
  1052  		sections = append(sections, &uiCollapsible{
  1053  			Title: fmt.Sprintf("Discussions (%d)", len(discussions)),
  1054  			Show:  true,
  1055  			Type:  sectionDiscussionList,
  1056  			Value: discussions,
  1057  		})
  1058  	}
  1059  	treeTestJobs, err := treeTestJobs(c, bug)
  1060  	if err != nil {
  1061  		return err
  1062  	}
  1063  	if len(treeTestJobs) > 0 {
  1064  		sections = append(sections, &uiCollapsible{
  1065  			Title: fmt.Sprintf("Bug presence (%d)", len(treeTestJobs)),
  1066  			Show:  true,
  1067  			Type:  sectionTestResults,
  1068  			Value: treeTestJobs,
  1069  		})
  1070  	}
  1071  	similar, err := loadSimilarBugsUI(c, r, bug, state)
  1072  	if err != nil {
  1073  		return err
  1074  	}
  1075  	if len(similar.Bugs) > 0 {
  1076  		sections = append(sections, &uiCollapsible{
  1077  			Title: fmt.Sprintf("Similar bugs (%d)", len(similar.Bugs)),
  1078  			Show:  getNsConfig(c, hdr.Namespace).AccessLevel != AccessPublic,
  1079  			Type:  sectionBugList,
  1080  			Value: similar,
  1081  		})
  1082  	}
  1083  	causeBisections, err := queryBugJobs(c, bug, JobBisectCause)
  1084  	if err != nil {
  1085  		return fmt.Errorf("failed to load cause bisections: %w", err)
  1086  	}
  1087  	var bisectCause *uiJob
  1088  	if bug.BisectCause > BisectPending {
  1089  		bisectCause, err = causeBisections.uiBestBisection(c)
  1090  		if err != nil {
  1091  			return err
  1092  		}
  1093  	}
  1094  	fixBisections, err := queryBugJobs(c, bug, JobBisectFix)
  1095  	if err != nil {
  1096  		return fmt.Errorf("failed to load cause bisections: %w", err)
  1097  	}
  1098  	var bisectFix *uiJob
  1099  	if bug.BisectFix > BisectPending {
  1100  		bisectFix, err = fixBisections.uiBestBisection(c)
  1101  		if err != nil {
  1102  			return err
  1103  		}
  1104  	}
  1105  	var fixCandidate *uiJob
  1106  	if bug.FixCandidateJob != "" {
  1107  		fixCandidate, err = fixBisections.uiBestFixCandidate(c)
  1108  		if err != nil {
  1109  			return err
  1110  		}
  1111  	}
  1112  	testPatchJobs, err := loadTestPatchJobs(c, bug)
  1113  	if err != nil {
  1114  		return err
  1115  	}
  1116  	if len(testPatchJobs) > 0 {
  1117  		sections = append(sections, &uiCollapsible{
  1118  			Title: fmt.Sprintf("Last patch testing requests (%d)", len(testPatchJobs)),
  1119  			Type:  sectionJobList,
  1120  			Value: &uiJobList{
  1121  				PerBug: true,
  1122  				Jobs:   testPatchJobs,
  1123  			},
  1124  		})
  1125  	}
  1126  	if accessLevel == AccessAdmin && len(bug.ReproAttempts) > 0 {
  1127  		reproAttempts := getReproAttempts(bug)
  1128  		sections = append(sections, &uiCollapsible{
  1129  			Title: fmt.Sprintf("Failed repro attempts (%d)", len(reproAttempts)),
  1130  			Type:  sectionReproAttempts,
  1131  			Value: reproAttempts,
  1132  		})
  1133  	}
  1134  	data := &uiBugPage{
  1135  		Header:       hdr,
  1136  		Now:          timeNow(c),
  1137  		Bug:          uiBug,
  1138  		BisectCause:  bisectCause,
  1139  		BisectFix:    bisectFix,
  1140  		FixCandidate: fixCandidate,
  1141  		Sections:     sections,
  1142  		SampleReport: sampleReport,
  1143  		Crashes:      crashesTable,
  1144  		LabelGroups:  getLabelGroups(c, bug),
  1145  	}
  1146  	if accessLevel == AccessAdmin && !bug.hasUserSubsystems() {
  1147  		data.DebugSubsystems = html.AmendURL(data.Bug.Link, "debug_subsystems", "1")
  1148  	}
  1149  	// bug.BisectFix is set to BisectNot in three cases :
  1150  	// - no fix bisections have been performed on the bug
  1151  	// - fix bisection was performed but resulted in a crash on HEAD
  1152  	// - there have been infrastructure problems during the job execution
  1153  	if len(fixBisections.all()) > 1 || len(fixBisections.all()) > 0 && bisectFix == nil {
  1154  		uiList, err := fixBisections.uiAll(c)
  1155  		if err != nil {
  1156  			return err
  1157  		}
  1158  		if len(uiList) != 0 {
  1159  			data.Sections = append(data.Sections, makeCollapsibleBugJobs(
  1160  				"Fix bisection attempts", uiList))
  1161  		}
  1162  	}
  1163  	// Similarly, a cause bisection can be repeated if there were infrastructure problems.
  1164  	if len(causeBisections.all()) > 1 || len(causeBisections.all()) > 0 && bisectCause == nil {
  1165  		uiList, err := causeBisections.uiAll(c)
  1166  		if err != nil {
  1167  			return err
  1168  		}
  1169  		if len(uiList) != 0 {
  1170  			data.Sections = append(data.Sections, makeCollapsibleBugJobs(
  1171  				"Cause bisection attempts", uiList))
  1172  		}
  1173  	}
  1174  	if r.FormValue("json") == "1" {
  1175  		w.Header().Set("Content-Type", "application/json")
  1176  		return writeJSONVersionOf(w, data)
  1177  	}
  1178  
  1179  	return serveTemplate(w, "bug.html", data)
  1180  }
  1181  
  1182  func getReproAttempts(bug *Bug) []*uiReproAttempt {
  1183  	var ret []*uiReproAttempt
  1184  	for _, item := range bug.ReproAttempts {
  1185  		ret = append(ret, &uiReproAttempt{
  1186  			Time:    item.Time,
  1187  			Manager: item.Manager,
  1188  			LogLink: textLink(textReproLog, item.Log),
  1189  		})
  1190  	}
  1191  	return ret
  1192  }
  1193  
  1194  type labelGroupInfo struct {
  1195  	Label BugLabelType
  1196  	Name  string
  1197  }
  1198  
  1199  var labelGroupOrder = []labelGroupInfo{
  1200  	{
  1201  		Label: OriginLabel,
  1202  		Name:  "Bug presence",
  1203  	},
  1204  	{
  1205  		Label: SubsystemLabel,
  1206  		Name:  "Subsystems",
  1207  	},
  1208  	{
  1209  		Label: EmptyLabel, // all the rest
  1210  		Name:  "Labels",
  1211  	},
  1212  }
  1213  
  1214  func getLabelGroups(c context.Context, bug *Bug) []*uiBugLabelGroup {
  1215  	var ret []*uiBugLabelGroup
  1216  	seenLabel := map[string]bool{}
  1217  	for _, info := range labelGroupOrder {
  1218  		obj := &uiBugLabelGroup{
  1219  			Name: info.Name,
  1220  		}
  1221  		for _, entry := range bug.Labels {
  1222  			if seenLabel[entry.String()] {
  1223  				continue
  1224  			}
  1225  			if entry.Label == info.Label || info.Label == EmptyLabel {
  1226  				seenLabel[entry.String()] = true
  1227  				obj.Labels = append(obj.Labels, makeBugLabelUI(c, bug, entry))
  1228  			}
  1229  		}
  1230  		if len(obj.Labels) == 0 {
  1231  			continue
  1232  		}
  1233  		ret = append(ret, obj)
  1234  	}
  1235  	return ret
  1236  }
  1237  
  1238  func debugBugSubsystems(c context.Context, w http.ResponseWriter, bug *Bug) error {
  1239  	service := getNsConfig(c, bug.Namespace).Subsystems.Service
  1240  	if service == nil {
  1241  		w.Write([]byte("Subsystem service was not found."))
  1242  		return nil
  1243  	}
  1244  	_, err := inferSubsystems(c, bug, bug.key(c), &debugtracer.GenericTracer{
  1245  		TraceWriter: w,
  1246  	})
  1247  	if err != nil {
  1248  		w.Write([]byte(fmt.Sprintf("%s", err)))
  1249  	}
  1250  	return nil
  1251  }
  1252  
  1253  func makeBugLabelUI(c context.Context, bug *Bug, entry BugLabel) *uiBugLabel {
  1254  	url := getCurrentURL(c)
  1255  	filterValue := entry.String()
  1256  
  1257  	// If we're on a main/terminal/subsystem page, let's stay there.
  1258  	link := url
  1259  	if !strings.HasPrefix(url, "/"+bug.Namespace) {
  1260  		link = fmt.Sprintf("/%s", bug.Namespace)
  1261  	}
  1262  	link = html.TransformURL(link, "label", func(oldLabels []string) []string {
  1263  		return mergeLabelSet(oldLabels, entry.String())
  1264  	})
  1265  	ret := &uiBugLabel{
  1266  		Name: filterValue,
  1267  		Link: link,
  1268  	}
  1269  	// Patch depending on the specific label type.
  1270  	switch entry.Label {
  1271  	case SubsystemLabel:
  1272  		// Use just the subsystem name.
  1273  		ret.Name = entry.Value
  1274  		// Prefer link to the per-subsystem page.
  1275  		if !strings.HasPrefix(url, "/"+bug.Namespace) || strings.Contains(url, "/s/") {
  1276  			ret.Link = fmt.Sprintf("/%s/s/%s", bug.Namespace, entry.Value)
  1277  		}
  1278  	}
  1279  	return ret
  1280  }
  1281  
  1282  func mergeLabelSet(oldLabels []string, newLabel string) []string {
  1283  	// Leave only one label for each type.
  1284  	labelsMap := map[BugLabelType]string{}
  1285  	for _, rawLabel := range append(oldLabels, newLabel) {
  1286  		label, value := splitLabel(rawLabel)
  1287  		labelsMap[label] = value
  1288  	}
  1289  	var ret []string
  1290  	for label, value := range labelsMap {
  1291  		ret = append(ret, BugLabel{
  1292  			Label: label,
  1293  			Value: value,
  1294  		}.String())
  1295  	}
  1296  	return ret
  1297  }
  1298  
  1299  func getBugDiscussionsUI(c context.Context, bug *Bug) ([]*uiBugDiscussion, error) {
  1300  	// TODO: also include dup bug discussions.
  1301  	// TODO: limit the number of DiscussionReminder type entries, e.g. all with
  1302  	// external replies + one latest.
  1303  	var list []*uiBugDiscussion
  1304  	discussions, err := discussionsForBug(c, bug.key(c))
  1305  	if err != nil {
  1306  		return nil, err
  1307  	}
  1308  	for _, d := range discussions {
  1309  		list = append(list, &uiBugDiscussion{
  1310  			Subject:  d.Subject,
  1311  			Link:     d.link(),
  1312  			Total:    d.Summary.AllMessages,
  1313  			External: d.Summary.ExternalMessages,
  1314  			Last:     d.Summary.LastMessage,
  1315  		})
  1316  	}
  1317  	sort.SliceStable(list, func(i, j int) bool {
  1318  		return list[i].Last.After(list[j].Last)
  1319  	})
  1320  	return list, nil
  1321  }
  1322  
  1323  func handleBugSummaries(c context.Context, w http.ResponseWriter, r *http.Request) error {
  1324  	if accessLevel(c, r) != AccessAdmin {
  1325  		return fmt.Errorf("admin only")
  1326  	}
  1327  	hdr, err := commonHeader(c, r, w, "")
  1328  	if err != nil {
  1329  		return err
  1330  	}
  1331  	stage := r.FormValue("stage")
  1332  	if stage == "" {
  1333  		return fmt.Errorf("stage must be specified")
  1334  	}
  1335  	list, err := getBugSummaries(c, hdr.Namespace, stage)
  1336  	if err != nil {
  1337  		return err
  1338  	}
  1339  	w.Header().Set("Content-Type", "application/json")
  1340  	return json.NewEncoder(w).Encode(list)
  1341  }
  1342  
  1343  func writeJSONVersionOf(writer http.ResponseWriter, page interface{}) error {
  1344  	data, err := GetJSONDescrFor(page)
  1345  	if err != nil {
  1346  		return err
  1347  	}
  1348  	_, err = writer.Write(data)
  1349  	return err
  1350  }
  1351  
  1352  func findBugByID(c context.Context, r *http.Request) (*Bug, error) {
  1353  	if id := r.FormValue("id"); id != "" {
  1354  		bug := new(Bug)
  1355  		bugKey := db.NewKey(c, "Bug", id, 0, nil)
  1356  		err := db.Get(c, bugKey, bug)
  1357  		return bug, err
  1358  	}
  1359  	if extID := r.FormValue("extid"); extID != "" {
  1360  		bug, _, err := findBugByReportingID(c, extID)
  1361  		return bug, err
  1362  	}
  1363  	return nil, fmt.Errorf("mandatory parameter id/extid is missing")
  1364  }
  1365  
  1366  func handleSubsystemsList(c context.Context, w http.ResponseWriter, r *http.Request) error {
  1367  	hdr, err := commonHeader(c, r, w, "")
  1368  	if err != nil {
  1369  		return err
  1370  	}
  1371  	cached, err := CacheGet(c, r, hdr.Namespace)
  1372  	if err != nil {
  1373  		return err
  1374  	}
  1375  	service := getNsConfig(c, hdr.Namespace).Subsystems.Service
  1376  	if service == nil {
  1377  		return fmt.Errorf("%w: the namespace does not have subsystems", ErrClientBadRequest)
  1378  	}
  1379  	nonEmpty := r.FormValue("all") != "true"
  1380  	list := []*uiSubsystem{}
  1381  	someHidden := false
  1382  	for _, item := range service.List() {
  1383  		record := createUISubsystem(hdr.Namespace, item, cached)
  1384  		if nonEmpty && (record.Open.Count+record.Fixed.Count) == 0 {
  1385  			someHidden = true
  1386  			continue
  1387  		}
  1388  		list = append(list, record)
  1389  	}
  1390  	unclassified := &uiSubsystem{
  1391  		Name: "",
  1392  		Open: uiSubsystemStats{
  1393  			Count: cached.NoSubsystem.Open,
  1394  			Link:  html.AmendURL("/"+hdr.Namespace, "no_subsystem", "true"),
  1395  		},
  1396  		Fixed: uiSubsystemStats{
  1397  			Count: cached.NoSubsystem.Fixed,
  1398  			Link:  html.AmendURL("/"+hdr.Namespace+"/fixed", "no_subsystem", "true"), // nolint: goconst
  1399  		},
  1400  	}
  1401  	sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name })
  1402  	return serveTemplate(w, "subsystems.html", &uiSubsystemsPage{
  1403  		Header:       hdr,
  1404  		List:         list,
  1405  		Unclassified: unclassified,
  1406  		SomeHidden:   someHidden,
  1407  		ShowAllURL:   html.AmendURL(getCurrentURL(c), "all", "true"),
  1408  	})
  1409  }
  1410  
  1411  func createUISubsystem(ns string, item *subsystem.Subsystem, cached *Cached) *uiSubsystem {
  1412  	stats := cached.Subsystems[item.Name]
  1413  	return &uiSubsystem{
  1414  		Name:        item.Name,
  1415  		Lists:       strings.Join(item.Lists, ", "),
  1416  		Maintainers: strings.Join(item.Maintainers, ", "),
  1417  		Open: uiSubsystemStats{
  1418  			Count: stats.Open,
  1419  			Link:  "/" + ns + "/s/" + item.Name,
  1420  		},
  1421  		Fixed: uiSubsystemStats{
  1422  			Count: stats.Fixed,
  1423  			Link: html.AmendURL("/"+ns+"/fixed", "label", BugLabel{ // nolint: goconst
  1424  				Label: SubsystemLabel,
  1425  				Value: item.Name,
  1426  			}.String()),
  1427  		},
  1428  	}
  1429  }
  1430  
  1431  // handleText serves plain text blobs (crash logs, reports, reproducers, etc).
  1432  func handleTextImpl(c context.Context, w http.ResponseWriter, r *http.Request, tag string) error {
  1433  	var id int64
  1434  	if x := r.FormValue("x"); x != "" {
  1435  		xid, err := strconv.ParseUint(x, 16, 64)
  1436  		if err != nil || xid == 0 {
  1437  			return fmt.Errorf("%w: failed to parse text id: %w", ErrClientBadRequest, err)
  1438  		}
  1439  		id = int64(xid)
  1440  	} else {
  1441  		// Old link support, don't remove.
  1442  		xid, err := strconv.ParseInt(r.FormValue("id"), 10, 64)
  1443  		if err != nil || xid == 0 {
  1444  			return fmt.Errorf("%w: failed to parse text id: %w", ErrClientBadRequest, err)
  1445  		}
  1446  		id = xid
  1447  	}
  1448  	bug, crash, err := checkTextAccess(c, r, tag, id)
  1449  	if err != nil {
  1450  		return err
  1451  	}
  1452  	data, ns, err := getText(c, tag, id)
  1453  	if err != nil {
  1454  		if strings.Contains(err.Error(), "datastore: no such entity") {
  1455  			err = fmt.Errorf("%w: %w", ErrClientNotFound, err)
  1456  		}
  1457  		return err
  1458  	}
  1459  	if err := checkAccessLevel(c, r, getNsConfig(c, ns).AccessLevel); err != nil {
  1460  		return err
  1461  	}
  1462  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  1463  	// Unfortunately filename does not work in chrome on linux due to:
  1464  	// https://bugs.chromium.org/p/chromium/issues/detail?id=608342
  1465  	w.Header().Set("Content-Disposition", "inline; filename="+textFilename(tag))
  1466  	augmentRepro(c, w, tag, bug, crash)
  1467  	w.Write(data)
  1468  	return nil
  1469  }
  1470  
  1471  func augmentRepro(c context.Context, w http.ResponseWriter, tag string, bug *Bug, crash *Crash) {
  1472  	if tag == textReproSyz || tag == textReproC {
  1473  		// Users asked for the bug link in reproducers (in case you only saved the repro link).
  1474  		if bug != nil {
  1475  			prefix := "#"
  1476  			if tag == textReproC {
  1477  				prefix = "//"
  1478  			}
  1479  			fmt.Fprintf(w, "%v %v/bug?id=%v\n", prefix, appURL(c), bug.keyHash(c))
  1480  		}
  1481  	}
  1482  	if tag == textReproSyz {
  1483  		// Add link to documentation and repro opts for syzkaller reproducers.
  1484  		w.Write([]byte(syzReproPrefix))
  1485  		if crash != nil {
  1486  			fmt.Fprintf(w, "#%s\n", crash.ReproOpts)
  1487  		}
  1488  	}
  1489  }
  1490  
  1491  func handleText(c context.Context, w http.ResponseWriter, r *http.Request) error {
  1492  	return handleTextImpl(c, w, r, r.FormValue("tag"))
  1493  }
  1494  
  1495  func handleTextX(tag string) contextHandler {
  1496  	return func(c context.Context, w http.ResponseWriter, r *http.Request) error {
  1497  		return handleTextImpl(c, w, r, tag)
  1498  	}
  1499  }
  1500  
  1501  func textFilename(tag string) string {
  1502  	switch tag {
  1503  	case textKernelConfig:
  1504  		return ".config"
  1505  	case textCrashLog:
  1506  		return "log.txt"
  1507  	case textCrashReport:
  1508  		return "report.txt"
  1509  	case textReproSyz:
  1510  		return "repro.syz"
  1511  	case textReproC:
  1512  		return "repro.c"
  1513  	case textPatch:
  1514  		return "patch.diff"
  1515  	case textLog:
  1516  		return "bisect.txt"
  1517  	case textError:
  1518  		return "error.txt"
  1519  	case textMachineInfo:
  1520  		return "minfo.txt"
  1521  	case textReproLog:
  1522  		return "repro.log"
  1523  	default:
  1524  		panic(fmt.Sprintf("unknown tag %v", tag))
  1525  	}
  1526  }
  1527  
  1528  func fetchFixPendingBugs(c context.Context, ns, manager string) ([]*Bug, error) {
  1529  	filter := func(query *db.Query) *db.Query {
  1530  		query = query.Filter("Namespace=", ns).
  1531  			Filter("Status=", BugStatusOpen).
  1532  			Filter("Commits>", "")
  1533  		if manager != "" {
  1534  			query = query.Filter("HappenedOn=", manager)
  1535  		}
  1536  		return query
  1537  	}
  1538  	rawBugs, _, err := loadAllBugs(c, filter)
  1539  	if err != nil {
  1540  		return nil, err
  1541  	}
  1542  	return rawBugs, nil
  1543  }
  1544  
  1545  func fetchNamespaceBugs(c context.Context, accessLevel AccessLevel, ns string,
  1546  	filter *userBugFilter) ([]*uiBugGroup, error) {
  1547  	if !filter.Any() && getNsConfig(c, ns).CacheUIPages {
  1548  		// If there's no filter, try to fetch data from cache.
  1549  		cached, err := CachedBugGroups(c, ns, accessLevel)
  1550  		if err != nil {
  1551  			log.Errorf(c, "failed to fetch from bug groups cache: %v", err)
  1552  		} else if cached != nil {
  1553  			return cached, nil
  1554  		}
  1555  	}
  1556  	bugs, err := loadVisibleBugs(c, ns, filter)
  1557  	if err != nil {
  1558  		return nil, err
  1559  	}
  1560  	managers, err := CachedManagerList(c, ns)
  1561  	if err != nil {
  1562  		return nil, err
  1563  	}
  1564  	return prepareBugGroups(c, bugs, managers, accessLevel, ns)
  1565  }
  1566  
  1567  func prepareBugGroups(c context.Context, bugs []*Bug, managers []string,
  1568  	accessLevel AccessLevel, ns string) ([]*uiBugGroup, error) {
  1569  	state, err := loadReportingState(c)
  1570  	if err != nil {
  1571  		return nil, err
  1572  	}
  1573  	groups := make(map[int][]*uiBug)
  1574  	bugMap := make(map[string]*uiBug)
  1575  	var dups []*Bug
  1576  	for _, bug := range bugs {
  1577  		if accessLevel < bug.sanitizeAccess(c, accessLevel) {
  1578  			continue
  1579  		}
  1580  		if bug.Status == BugStatusDup {
  1581  			dups = append(dups, bug)
  1582  			continue
  1583  		}
  1584  		uiBug := createUIBug(c, bug, state, managers)
  1585  		if len(uiBug.Commits) != 0 {
  1586  			// Don't show "fix pending" bugs on the main page.
  1587  			continue
  1588  		}
  1589  		bugMap[bug.keyHash(c)] = uiBug
  1590  		id := uiBug.ReportingIndex
  1591  		groups[id] = append(groups[id], uiBug)
  1592  	}
  1593  	for _, dup := range dups {
  1594  		bug := bugMap[dup.DupOf]
  1595  		if bug == nil {
  1596  			continue // this can be an invalid bug which we filtered above
  1597  		}
  1598  		mergeUIBug(c, bug, dup)
  1599  	}
  1600  	cfg := getNsConfig(c, ns)
  1601  	var uiGroups []*uiBugGroup
  1602  	for index, bugs := range groups {
  1603  		sort.Slice(bugs, func(i, j int) bool {
  1604  			if bugs[i].Namespace != bugs[j].Namespace {
  1605  				return bugs[i].Namespace < bugs[j].Namespace
  1606  			}
  1607  			if bugs[i].ClosedTime != bugs[j].ClosedTime {
  1608  				return bugs[i].ClosedTime.After(bugs[j].ClosedTime)
  1609  			}
  1610  			return bugs[i].ReportedTime.After(bugs[j].ReportedTime)
  1611  		})
  1612  		caption, fragment := "", ""
  1613  		switch index {
  1614  		case len(cfg.Reporting) - 1:
  1615  			caption = "open"
  1616  			fragment = "open"
  1617  		default:
  1618  			reporting := &cfg.Reporting[index]
  1619  			caption = reporting.DisplayTitle
  1620  			fragment = reporting.Name
  1621  		}
  1622  		uiGroups = append(uiGroups, &uiBugGroup{
  1623  			Now:       timeNow(c),
  1624  			Caption:   caption,
  1625  			Fragment:  fragment,
  1626  			Namespace: ns,
  1627  			ShowIndex: index,
  1628  			Bugs:      bugs,
  1629  		})
  1630  	}
  1631  	sort.Slice(uiGroups, func(i, j int) bool {
  1632  		return uiGroups[i].ShowIndex > uiGroups[j].ShowIndex
  1633  	})
  1634  	return uiGroups, nil
  1635  }
  1636  
  1637  func loadVisibleBugs(c context.Context, ns string, bugFilter *userBugFilter) ([]*Bug, error) {
  1638  	// Load open and dup bugs in in 2 separate queries.
  1639  	// Ideally we load them in one query with a suitable filter,
  1640  	// but unfortunately status values don't allow one query (<BugStatusFixed || >BugStatusInvalid).
  1641  	// Ideally we also have separate status for "dup of a closed bug" as we don't need to fetch them.
  1642  	// Potentially changing "dup" to "dup of a closed bug" can be done in background.
  1643  	// But 2 queries is still much faster than fetching all bugs and we can do this in parallel.
  1644  	errc := make(chan error)
  1645  	var dups []*Bug
  1646  	go func() {
  1647  		// Don't apply bugFilter to dups -- they need to be joined unconditionally.
  1648  		filter := func(query *db.Query) *db.Query {
  1649  			return query.Filter("Namespace=", ns).
  1650  				Filter("Status=", BugStatusDup)
  1651  		}
  1652  		var err error
  1653  		dups, _, err = loadAllBugs(c, filter)
  1654  		errc <- err
  1655  	}()
  1656  	filter := func(query *db.Query) *db.Query {
  1657  		return applyBugFilter(
  1658  			query.Filter("Namespace=", ns).
  1659  				Filter("Status<", BugStatusFixed),
  1660  			bugFilter,
  1661  		)
  1662  	}
  1663  	bugs, _, err := loadAllBugs(c, filter)
  1664  	if err != nil {
  1665  		return nil, err
  1666  	}
  1667  	if err := <-errc; err != nil {
  1668  		return nil, err
  1669  	}
  1670  	var filteredBugs []*Bug
  1671  	for _, bug := range bugs {
  1672  		if bugFilter.MatchBug(bug) {
  1673  			filteredBugs = append(filteredBugs, bug)
  1674  		}
  1675  	}
  1676  	return append(filteredBugs, dups...), nil
  1677  }
  1678  
  1679  func fetchTerminalBugs(c context.Context, accessLevel AccessLevel,
  1680  	ns string, typ *TerminalBug, extraBugs []*Bug) (*uiBugGroup, *uiBugStats, error) {
  1681  	bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
  1682  		return applyBugFilter(
  1683  			query.Filter("Namespace=", ns).Filter("Status=", typ.Status),
  1684  			typ.Filter,
  1685  		)
  1686  	})
  1687  	if err != nil {
  1688  		return nil, nil, err
  1689  	}
  1690  	bugs = append(bugs, extraBugs...)
  1691  	state, err := loadReportingState(c)
  1692  	if err != nil {
  1693  		return nil, nil, err
  1694  	}
  1695  	managers, err := CachedManagerList(c, ns)
  1696  	if err != nil {
  1697  		return nil, nil, err
  1698  	}
  1699  	sort.Slice(bugs, func(i, j int) bool {
  1700  		iFixed := bugs[i].Status == BugStatusFixed
  1701  		jFixed := bugs[j].Status == BugStatusFixed
  1702  		if iFixed != jFixed {
  1703  			// Not-yet-fully-patched bugs come first.
  1704  			return jFixed
  1705  		}
  1706  		return bugs[i].Closed.After(bugs[j].Closed)
  1707  	})
  1708  	stats := &uiBugStats{}
  1709  	res := &uiBugGroup{
  1710  		Now:         timeNow(c),
  1711  		ShowPatch:   typ.ShowPatch,
  1712  		ShowPatched: typ.ShowPatched,
  1713  		Namespace:   ns,
  1714  	}
  1715  	for _, bug := range bugs {
  1716  		if accessLevel < bug.sanitizeAccess(c, accessLevel) {
  1717  			continue
  1718  		}
  1719  		if !typ.Filter.MatchBug(bug) {
  1720  			continue
  1721  		}
  1722  		uiBug := createUIBug(c, bug, state, managers)
  1723  		res.Bugs = append(res.Bugs, uiBug)
  1724  		stats.Record(bug, &bug.Reporting[uiBug.ReportingIndex])
  1725  	}
  1726  	return res, stats, nil
  1727  }
  1728  
  1729  func applyBugFilter(query *db.Query, filter *userBugFilter) *db.Query {
  1730  	if filter == nil {
  1731  		return query
  1732  	}
  1733  	manager := filter.ManagerName()
  1734  	if len(filter.Labels) > 0 {
  1735  		// Take just the first one.
  1736  		label, value := splitLabel(filter.Labels[0])
  1737  		query = query.Filter("Labels.Label=", string(label))
  1738  		query = query.Filter("Labels.Value=", value)
  1739  	} else if manager != "" {
  1740  		query = query.Filter("HappenedOn=", manager)
  1741  	}
  1742  	return query
  1743  }
  1744  
  1745  func loadDupsForBug(c context.Context, r *http.Request, bug *Bug, state *ReportingState, managers []string) (
  1746  	*uiBugGroup, error) {
  1747  	bugHash := bug.keyHash(c)
  1748  	var dups []*Bug
  1749  	_, err := db.NewQuery("Bug").
  1750  		Filter("Status=", BugStatusDup).
  1751  		Filter("DupOf=", bugHash).
  1752  		GetAll(c, &dups)
  1753  	if err != nil {
  1754  		return nil, err
  1755  	}
  1756  	var results []*uiBug
  1757  	accessLevel := accessLevel(c, r)
  1758  	for _, dup := range dups {
  1759  		if accessLevel < dup.sanitizeAccess(c, accessLevel) {
  1760  			continue
  1761  		}
  1762  		results = append(results, createUIBug(c, dup, state, managers))
  1763  	}
  1764  	group := &uiBugGroup{
  1765  		Now:         timeNow(c),
  1766  		Caption:     "duplicates",
  1767  		ShowPatched: true,
  1768  		ShowStatus:  true,
  1769  		Bugs:        results,
  1770  	}
  1771  	return group, nil
  1772  }
  1773  
  1774  func loadSimilarBugsUI(c context.Context, r *http.Request, bug *Bug, state *ReportingState) (*uiBugGroup, error) {
  1775  	managers := make(map[string][]string)
  1776  	accessLevel := accessLevel(c, r)
  1777  	similarBugs, err := loadSimilarBugs(c, bug)
  1778  	if err != nil {
  1779  		return nil, err
  1780  	}
  1781  	var results []*uiBug
  1782  	for _, similar := range similarBugs {
  1783  		if accessLevel < similar.sanitizeAccess(c, accessLevel) {
  1784  			continue
  1785  		}
  1786  		if managers[similar.Namespace] == nil {
  1787  			mgrs, err := CachedManagerList(c, similar.Namespace)
  1788  			if err != nil {
  1789  				return nil, err
  1790  			}
  1791  			managers[similar.Namespace] = mgrs
  1792  		}
  1793  		results = append(results, createUIBug(c, similar, state, managers[similar.Namespace]))
  1794  	}
  1795  	group := &uiBugGroup{
  1796  		Now:           timeNow(c),
  1797  		ShowNamespace: true,
  1798  		ShowPatched:   true,
  1799  		ShowStatus:    true,
  1800  		Bugs:          results,
  1801  	}
  1802  	return group, nil
  1803  }
  1804  
  1805  func closedBugStatus(bug *Bug, bugReporting *BugReporting) string {
  1806  	status := ""
  1807  	switch bug.Status {
  1808  	case BugStatusInvalid:
  1809  		switch bug.StatusReason {
  1810  		case dashapi.InvalidatedByNoActivity:
  1811  			fallthrough
  1812  		case dashapi.InvalidatedByRevokedRepro:
  1813  			status = "obsoleted due to no activity"
  1814  		default:
  1815  			status = "closed as invalid"
  1816  		}
  1817  		if bugReporting.Auto {
  1818  			status = "auto-" + status
  1819  		}
  1820  	case BugStatusFixed:
  1821  		status = "fixed"
  1822  	case BugStatusDup:
  1823  		status = "closed as dup"
  1824  	default:
  1825  		status = fmt.Sprintf("unknown (%v)", bug.Status)
  1826  	}
  1827  	return fmt.Sprintf("%v on %v", status, html.FormatTime(bug.Closed))
  1828  }
  1829  
  1830  func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug {
  1831  	reportingIdx, status, link := 0, "", ""
  1832  	var reported time.Time
  1833  	var err error
  1834  	if bug.Status == BugStatusOpen && state != nil {
  1835  		_, _, reportingIdx, status, link, err = needReport(c, "", state, bug)
  1836  		reported = bug.Reporting[reportingIdx].Reported
  1837  		if err != nil {
  1838  			status = err.Error()
  1839  		}
  1840  		if status == "" {
  1841  			status = "???"
  1842  		}
  1843  	} else {
  1844  		for i := range bug.Reporting {
  1845  			bugReporting := &bug.Reporting[i]
  1846  			if i == len(bug.Reporting)-1 ||
  1847  				bug.Status == BugStatusInvalid && !bugReporting.Closed.IsZero() &&
  1848  					bug.Reporting[i+1].Closed.IsZero() ||
  1849  				(bug.Status == BugStatusFixed || bug.Status == BugStatusDup) &&
  1850  					bugReporting.Closed.IsZero() {
  1851  				reportingIdx = i
  1852  				reported = bugReporting.Reported
  1853  				link = bugReporting.Link
  1854  				status = closedBugStatus(bug, bugReporting)
  1855  				break
  1856  			}
  1857  		}
  1858  	}
  1859  	creditEmail := ""
  1860  	if bug.Reporting[reportingIdx].ID != "" {
  1861  		// If the bug was never reported to the public, sanitizeReporting() would clear IDs
  1862  		// for non-authorized users. In such case, don't show CreditEmail at all.
  1863  		creditEmail, err = email.AddAddrContext(ownEmail(c), bug.Reporting[reportingIdx].ID)
  1864  		if err != nil {
  1865  			log.Errorf(c, "failed to generate credit email: %v", err)
  1866  		}
  1867  	}
  1868  	uiBug := &uiBug{
  1869  		Namespace:      bug.Namespace,
  1870  		Title:          bug.displayTitle(),
  1871  		BisectCause:    bug.BisectCause,
  1872  		BisectFix:      bug.BisectFix,
  1873  		NumCrashes:     bug.NumCrashes,
  1874  		FirstTime:      bug.FirstTime,
  1875  		LastTime:       bug.LastTime,
  1876  		ReportedTime:   reported,
  1877  		ClosedTime:     bug.Closed,
  1878  		ReproLevel:     bug.ReproLevel,
  1879  		ReportingIndex: reportingIdx,
  1880  		Status:         status,
  1881  		Link:           bugExtLink(c, bug),
  1882  		ExternalLink:   link,
  1883  		CreditEmail:    creditEmail,
  1884  		NumManagers:    len(managers),
  1885  		LastActivity:   bug.LastActivity,
  1886  		Discussions:    bug.discussionSummary(),
  1887  		ID:             bug.keyHash(c),
  1888  	}
  1889  	for _, entry := range bug.Labels {
  1890  		uiBug.Labels = append(uiBug.Labels, makeBugLabelUI(c, bug, entry))
  1891  	}
  1892  	updateBugBadness(c, uiBug)
  1893  	if len(bug.Commits) != 0 {
  1894  		for i, com := range bug.Commits {
  1895  			cfg := getNsConfig(c, bug.Namespace)
  1896  			info := bug.getCommitInfo(i)
  1897  			uiBug.Commits = append(uiBug.Commits, &uiCommit{
  1898  				Hash:   info.Hash,
  1899  				Title:  com,
  1900  				Link:   vcs.CommitLink(cfg.Repos[0].URL, info.Hash),
  1901  				Repo:   cfg.Repos[0].URL,
  1902  				Branch: cfg.Repos[0].Branch,
  1903  			})
  1904  		}
  1905  		for _, mgr := range managers {
  1906  			found := false
  1907  			for _, mgr1 := range bug.PatchedOn {
  1908  				if mgr == mgr1 {
  1909  					found = true
  1910  					break
  1911  				}
  1912  			}
  1913  			if found {
  1914  				uiBug.PatchedOn = append(uiBug.PatchedOn, mgr)
  1915  			} else {
  1916  				uiBug.MissingOn = append(uiBug.MissingOn, mgr)
  1917  			}
  1918  		}
  1919  		sort.Strings(uiBug.PatchedOn)
  1920  		sort.Strings(uiBug.MissingOn)
  1921  	}
  1922  	return uiBug
  1923  }
  1924  
  1925  func mergeUIBug(c context.Context, bug *uiBug, dup *Bug) {
  1926  	bug.NumCrashes += dup.NumCrashes
  1927  	bug.BisectCause = mergeBisectStatus(bug.BisectCause, dup.BisectCause)
  1928  	bug.BisectFix = mergeBisectStatus(bug.BisectFix, dup.BisectFix)
  1929  	if bug.LastTime.Before(dup.LastTime) {
  1930  		bug.LastTime = dup.LastTime
  1931  	}
  1932  	if bug.ReproLevel < dup.ReproLevel {
  1933  		bug.ReproLevel = dup.ReproLevel
  1934  	}
  1935  	updateBugBadness(c, bug)
  1936  }
  1937  
  1938  func mergeBisectStatus(a, b BisectStatus) BisectStatus {
  1939  	// The statuses are stored in the datastore, so we can't reorder them.
  1940  	// But if one of bisections is Yes, then we want to show Yes.
  1941  	bisectPriority := [bisectStatusLast]int{0, 1, 2, 6, 5, 4, 3}
  1942  	if bisectPriority[a] >= bisectPriority[b] {
  1943  		return a
  1944  	}
  1945  	return b
  1946  }
  1947  
  1948  func updateBugBadness(c context.Context, bug *uiBug) {
  1949  	bug.NumCrashesBad = bug.NumCrashes >= 10000 && timeNow(c).Sub(bug.LastTime) < 24*time.Hour
  1950  }
  1951  
  1952  func loadCrashesForBug(c context.Context, bug *Bug) ([]*uiCrash, template.HTML, error) {
  1953  	bugKey := bug.key(c)
  1954  	// We can have more than maxCrashes crashes, if we have lots of reproducers.
  1955  	crashes, _, err := queryCrashesForBug(c, bugKey, 2*maxCrashes()+200)
  1956  	if err != nil || len(crashes) == 0 {
  1957  		return nil, "", err
  1958  	}
  1959  	builds := make(map[string]*Build)
  1960  	var results []*uiCrash
  1961  	for _, crash := range crashes {
  1962  		build := builds[crash.BuildID]
  1963  		if build == nil {
  1964  			build, err = loadBuild(c, bug.Namespace, crash.BuildID)
  1965  			if err != nil {
  1966  				return nil, "", err
  1967  			}
  1968  			builds[crash.BuildID] = build
  1969  		}
  1970  		results = append(results, makeUICrash(c, crash, build))
  1971  	}
  1972  	sampleReport, _, err := getText(c, textCrashReport, crashes[0].Report)
  1973  	if err != nil {
  1974  		return nil, "", err
  1975  	}
  1976  	sampleBuild := builds[crashes[0].BuildID]
  1977  	linkifiedReport := linkifyReport(sampleReport, sampleBuild.KernelRepo, sampleBuild.KernelCommit)
  1978  	return results, linkifiedReport, nil
  1979  }
  1980  
  1981  func linkifyReport(report []byte, repo, commit string) template.HTML {
  1982  	escaped := template.HTMLEscapeString(string(report))
  1983  	return template.HTML(sourceFileRe.ReplaceAllStringFunc(escaped, func(match string) string {
  1984  		sub := sourceFileRe.FindStringSubmatch(match)
  1985  		line, _ := strconv.Atoi(sub[3])
  1986  		url := vcs.FileLink(repo, commit, sub[2], line)
  1987  		return fmt.Sprintf("%v<a href='%v'>%v:%v</a>%v", sub[1], url, sub[2], sub[3], sub[4])
  1988  	}))
  1989  }
  1990  
  1991  var sourceFileRe = regexp.MustCompile("( |\t|\n)([a-zA-Z0-9/_.-]+\\.(?:h|c|cc|cpp|s|S|go|rs)):([0-9]+)( |!|\\)|\t|\n)")
  1992  
  1993  func makeUIAssets(build *Build, crash *Crash, forReport bool) []*uiAsset {
  1994  	var uiAssets []*uiAsset
  1995  	for _, asset := range createAssetList(build, crash, forReport) {
  1996  		uiAssets = append(uiAssets, &uiAsset{
  1997  			Title:       asset.Title,
  1998  			DownloadURL: asset.DownloadURL,
  1999  		})
  2000  	}
  2001  	return uiAssets
  2002  }
  2003  
  2004  func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash {
  2005  	ui := &uiCrash{
  2006  		Title:           crash.Title,
  2007  		Manager:         crash.Manager,
  2008  		Time:            crash.Time,
  2009  		Maintainers:     strings.Join(crash.Maintainers, ", "),
  2010  		LogLink:         textLink(textCrashLog, crash.Log),
  2011  		LogHasStrace:    dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
  2012  		ReportLink:      textLink(textCrashReport, crash.Report),
  2013  		ReproSyzLink:    textLink(textReproSyz, crash.ReproSyz),
  2014  		ReproCLink:      textLink(textReproC, crash.ReproC),
  2015  		ReproIsRevoked:  crash.ReproIsRevoked,
  2016  		MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo),
  2017  		Assets:          makeUIAssets(build, crash, true),
  2018  	}
  2019  	if build != nil {
  2020  		ui.uiBuild = makeUIBuild(c, build, true)
  2021  	}
  2022  	return ui
  2023  }
  2024  
  2025  func makeUIBuild(c context.Context, build *Build, forReport bool) *uiBuild {
  2026  	return &uiBuild{
  2027  		Time:                build.Time,
  2028  		SyzkallerCommit:     build.SyzkallerCommit,
  2029  		SyzkallerCommitLink: vcs.LogLink(vcs.SyzkallerRepo, build.SyzkallerCommit),
  2030  		SyzkallerCommitDate: build.SyzkallerCommitDate,
  2031  		KernelRepo:          build.KernelRepo,
  2032  		KernelBranch:        build.KernelBranch,
  2033  		KernelAlias:         kernelRepoInfo(c, build).Alias,
  2034  		KernelCommit:        build.KernelCommit,
  2035  		KernelCommitLink:    vcs.LogLink(build.KernelRepo, build.KernelCommit),
  2036  		KernelCommitTitle:   build.KernelCommitTitle,
  2037  		KernelCommitDate:    build.KernelCommitDate,
  2038  		KernelConfigLink:    textLink(textKernelConfig, build.KernelConfig),
  2039  		Assets:              makeUIAssets(build, nil, forReport),
  2040  	}
  2041  }
  2042  
  2043  func loadRepos(c context.Context, ns string) ([]*uiRepo, error) {
  2044  	managers, _, err := loadNsManagerList(c, ns, nil)
  2045  	if err != nil {
  2046  		return nil, err
  2047  	}
  2048  	var buildKeys []*db.Key
  2049  	for _, mgr := range managers {
  2050  		if mgr.CurrentBuild != "" {
  2051  			buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild))
  2052  		}
  2053  	}
  2054  	builds := make([]*Build, len(buildKeys))
  2055  	err = db.GetMulti(c, buildKeys, builds)
  2056  	if err != nil {
  2057  		return nil, err
  2058  	}
  2059  	ret := []*uiRepo{}
  2060  	dedupRepos := map[string]bool{}
  2061  	for _, build := range builds {
  2062  		if build == nil {
  2063  			continue
  2064  		}
  2065  		repo := &uiRepo{
  2066  			URL:    build.KernelRepo,
  2067  			Branch: build.KernelBranch,
  2068  		}
  2069  		hash := repo.String()
  2070  		if dedupRepos[hash] {
  2071  			continue
  2072  		}
  2073  		dedupRepos[hash] = true
  2074  		ret = append(ret, repo)
  2075  	}
  2076  	sort.Slice(ret, func(i, j int) bool {
  2077  		if ret[i].URL != ret[j].URL {
  2078  			return ret[i].URL < ret[j].URL
  2079  		}
  2080  		return ret[i].Branch < ret[j].Branch
  2081  	})
  2082  	return ret, nil
  2083  }
  2084  
  2085  func loadManagers(c context.Context, accessLevel AccessLevel, ns string, filter *userBugFilter) ([]*uiManager, error) {
  2086  	now := timeNow(c)
  2087  	date := timeDate(now)
  2088  	managers, managerKeys, err := loadNsManagerList(c, ns, filter)
  2089  	if err != nil {
  2090  		return nil, err
  2091  	}
  2092  	var buildKeys []*db.Key
  2093  	var statsKeys []*db.Key
  2094  	for i, mgr := range managers {
  2095  		if mgr.CurrentBuild != "" {
  2096  			buildKeys = append(buildKeys, buildKey(c, mgr.Namespace, mgr.CurrentBuild))
  2097  		}
  2098  		if timeDate(mgr.LastAlive) == date {
  2099  			statsKeys = append(statsKeys,
  2100  				db.NewKey(c, "ManagerStats", "", int64(date), managerKeys[i]))
  2101  		}
  2102  	}
  2103  	builds := make([]*Build, len(buildKeys))
  2104  	stats := make([]*ManagerStats, len(statsKeys))
  2105  	coverAssets := map[string]Asset{}
  2106  	g, _ := errgroup.WithContext(context.Background())
  2107  	g.Go(func() error {
  2108  		return db.GetMulti(c, buildKeys, builds)
  2109  	})
  2110  	g.Go(func() error {
  2111  		return db.GetMulti(c, statsKeys, stats)
  2112  	})
  2113  	g.Go(func() error {
  2114  		// Get the last coverage report asset for the last week.
  2115  		const maxDuration = time.Hour * 24 * 7
  2116  		var err error
  2117  		coverAssets, err = queryLatestManagerAssets(c, ns, dashapi.HTMLCoverageReport, maxDuration)
  2118  		return err
  2119  	})
  2120  	err = g.Wait()
  2121  	if err != nil {
  2122  		return nil, fmt.Errorf("failed to query manager-related info: %w", err)
  2123  	}
  2124  	uiBuilds := make(map[string]*uiBuild)
  2125  	for _, build := range builds {
  2126  		uiBuilds[build.Namespace+"|"+build.ID] = makeUIBuild(c, build, true)
  2127  	}
  2128  	var fullStats []*ManagerStats
  2129  	for _, mgr := range managers {
  2130  		if timeDate(mgr.LastAlive) != date {
  2131  			fullStats = append(fullStats, &ManagerStats{})
  2132  			continue
  2133  		}
  2134  		fullStats = append(fullStats, stats[0])
  2135  		stats = stats[1:]
  2136  	}
  2137  	var results []*uiManager
  2138  	for i, mgr := range managers {
  2139  		stats := fullStats[i]
  2140  		link := mgr.Link
  2141  		if accessLevel < AccessUser {
  2142  			link = ""
  2143  		}
  2144  		uptime := mgr.CurrentUpTime
  2145  		if now.Sub(mgr.LastAlive) > 6*time.Hour {
  2146  			uptime = 0
  2147  		}
  2148  		// TODO: also display how fresh the coverage report is (to display it on
  2149  		// the main page -- this will reduce confusion).
  2150  		coverURL := ""
  2151  		if asset, ok := coverAssets[mgr.Name]; ok {
  2152  			coverURL = asset.DownloadURL
  2153  		} else if getConfig(c).CoverPath != "" {
  2154  			coverURL = getConfig(c).CoverPath + mgr.Name + ".html"
  2155  		}
  2156  		ui := &uiManager{
  2157  			Now:                   timeNow(c),
  2158  			Namespace:             mgr.Namespace,
  2159  			Name:                  mgr.Name,
  2160  			Link:                  link,
  2161  			PageLink:              mgr.Namespace + "/manager/" + mgr.Name,
  2162  			CoverLink:             coverURL,
  2163  			CurrentBuild:          uiBuilds[mgr.Namespace+"|"+mgr.CurrentBuild],
  2164  			FailedBuildBugLink:    bugLink(mgr.FailedBuildBug),
  2165  			FailedSyzBuildBugLink: bugLink(mgr.FailedSyzBuildBug),
  2166  			LastActive:            mgr.LastAlive,
  2167  			CurrentUpTime:         uptime,
  2168  			MaxCorpus:             stats.MaxCorpus,
  2169  			MaxCover:              stats.MaxCover,
  2170  			TotalFuzzingTime:      stats.TotalFuzzingTime,
  2171  			TotalCrashes:          stats.TotalCrashes,
  2172  			TotalExecs:            stats.TotalExecs,
  2173  			TotalExecsBad:         stats.TotalExecs == 0,
  2174  		}
  2175  		results = append(results, ui)
  2176  	}
  2177  	sort.Slice(results, func(i, j int) bool {
  2178  		if results[i].Namespace != results[j].Namespace {
  2179  			return results[i].Namespace < results[j].Namespace
  2180  		}
  2181  		return results[i].Name < results[j].Name
  2182  	})
  2183  	return results, nil
  2184  }
  2185  
  2186  func loadNsManagerList(c context.Context, ns string, filter *userBugFilter) ([]*Manager, []*db.Key, error) {
  2187  	managers, keys, err := loadAllManagers(c, ns)
  2188  	if err != nil {
  2189  		return nil, nil, err
  2190  	}
  2191  	var filtered []*Manager
  2192  	var filteredKeys []*db.Key
  2193  	for i, mgr := range managers {
  2194  		cfg := getNsConfig(c, mgr.Namespace)
  2195  		if ns == "" && cfg.Decommissioned {
  2196  			continue
  2197  		}
  2198  		if !filter.MatchManagerName(mgr.Name) {
  2199  			continue
  2200  		}
  2201  		filtered = append(filtered, mgr)
  2202  		filteredKeys = append(filteredKeys, keys[i])
  2203  	}
  2204  	return filtered, filteredKeys, nil
  2205  }
  2206  
  2207  func loadRecentJobs(c context.Context) ([]*uiJob, error) {
  2208  	var jobs []*Job
  2209  	keys, err := db.NewQuery("Job").
  2210  		Order("-Created").
  2211  		Limit(80).
  2212  		GetAll(c, &jobs)
  2213  	if err != nil {
  2214  		return nil, err
  2215  	}
  2216  	return getUIJobs(c, keys, jobs), nil
  2217  }
  2218  
  2219  func loadPendingJobs(c context.Context) ([]*uiJob, error) {
  2220  	var jobs []*Job
  2221  	keys, err := db.NewQuery("Job").
  2222  		Filter("Started=", time.Time{}).
  2223  		Limit(50).
  2224  		GetAll(c, &jobs)
  2225  	if err != nil {
  2226  		return nil, err
  2227  	}
  2228  	return getUIJobs(c, keys, jobs), nil
  2229  }
  2230  
  2231  func loadRunningJobs(c context.Context) ([]*uiJob, error) {
  2232  	var jobs []*Job
  2233  	keys, err := db.NewQuery("Job").
  2234  		Filter("IsRunning=", true).
  2235  		Limit(50).
  2236  		GetAll(c, &jobs)
  2237  	if err != nil {
  2238  		return nil, err
  2239  	}
  2240  	return getUIJobs(c, keys, jobs), nil
  2241  }
  2242  
  2243  func loadJobsOfType(c context.Context, t JobType) ([]*uiJob, error) {
  2244  	var jobs []*Job
  2245  	keys, err := db.NewQuery("Job").
  2246  		Filter("Type=", t).
  2247  		Order("-Finished").
  2248  		Limit(50).
  2249  		GetAll(c, &jobs)
  2250  	if err != nil {
  2251  		return nil, err
  2252  	}
  2253  	return getUIJobs(c, keys, jobs), nil
  2254  }
  2255  
  2256  func getUIJobs(c context.Context, keys []*db.Key, jobs []*Job) []*uiJob {
  2257  	var results []*uiJob
  2258  	for i, job := range jobs {
  2259  		results = append(results, makeUIJob(c, job, keys[i], nil, nil, nil))
  2260  	}
  2261  	return results
  2262  }
  2263  
  2264  func loadTestPatchJobs(c context.Context, bug *Bug) ([]*uiJob, error) {
  2265  	bugKey := bug.key(c)
  2266  
  2267  	var jobs []*Job
  2268  	keys, err := db.NewQuery("Job").
  2269  		Ancestor(bugKey).
  2270  		Filter("Type=", JobTestPatch).
  2271  		Filter("Finished>=", time.Time{}).
  2272  		Order("-Finished").
  2273  		GetAll(c, &jobs)
  2274  	if err != nil {
  2275  		return nil, err
  2276  	}
  2277  	const maxAutomaticJobs = 10
  2278  	autoJobsLeft := maxAutomaticJobs
  2279  	var results []*uiJob
  2280  	for i, job := range jobs {
  2281  		if job.User == "" {
  2282  			if autoJobsLeft == 0 {
  2283  				continue
  2284  			}
  2285  			autoJobsLeft--
  2286  		}
  2287  		if job.TreeOrigin && !job.Finished.IsZero() {
  2288  			continue
  2289  		}
  2290  		var build *Build
  2291  		if job.BuildID != "" {
  2292  			if build, err = loadBuild(c, bug.Namespace, job.BuildID); err != nil {
  2293  				return nil, err
  2294  			}
  2295  		}
  2296  		results = append(results, makeUIJob(c, job, keys[i], nil, nil, build))
  2297  	}
  2298  	return results, nil
  2299  }
  2300  
  2301  func makeUIJob(c context.Context, job *Job, jobKey *db.Key, bug *Bug, crash *Crash, build *Build) *uiJob {
  2302  	ui := &uiJob{
  2303  		JobInfo:           makeJobInfo(c, job, jobKey, bug, build, crash),
  2304  		InvalidateJobLink: invalidateJobLink(c, job, jobKey, false),
  2305  		RestartJobLink:    invalidateJobLink(c, job, jobKey, true),
  2306  		FixCandidate:      job.IsCrossTree(),
  2307  	}
  2308  	if crash != nil {
  2309  		ui.Crash = makeUICrash(c, crash, build)
  2310  	}
  2311  	return ui
  2312  }
  2313  
  2314  func invalidateJobLink(c context.Context, job *Job, jobKey *db.Key, restart bool) string {
  2315  	if !user.IsAdmin(c) {
  2316  		return ""
  2317  	}
  2318  	if job.InvalidatedBy != "" || job.Finished.IsZero() {
  2319  		return ""
  2320  	}
  2321  	if job.Type != JobBisectCause && job.Type != JobBisectFix {
  2322  		return ""
  2323  	}
  2324  	params := url.Values{}
  2325  	params.Add("action", "invalidate_bisection")
  2326  	params.Add("key", jobKey.Encode())
  2327  	if restart {
  2328  		params.Add("restart", "1")
  2329  	}
  2330  	return "/admin?" + params.Encode()
  2331  }
  2332  
  2333  func formatLogLine(line string) string {
  2334  	const maxLineLen = 1000
  2335  
  2336  	line = strings.Replace(line, "\n", " ", -1)
  2337  	line = strings.Replace(line, "\r", "", -1)
  2338  	if len(line) > maxLineLen {
  2339  		line = line[:maxLineLen]
  2340  		line += "..."
  2341  	}
  2342  	return line + "\n"
  2343  }
  2344  
  2345  func fetchErrorLogs(c context.Context) ([]byte, error) {
  2346  	if !appengine.IsAppEngine() {
  2347  		return nil, nil
  2348  	}
  2349  
  2350  	const (
  2351  		maxLines = 100
  2352  	)
  2353  	projID := os.Getenv("GOOGLE_CLOUD_PROJECT")
  2354  
  2355  	adminClient, err := logadmin.NewClient(c, projID)
  2356  	if err != nil {
  2357  		return nil, fmt.Errorf("failed to create the logging client: %w", err)
  2358  	}
  2359  	defer adminClient.Close()
  2360  
  2361  	lastWeek := time.Now().Add(-1 * 7 * 24 * time.Hour).Format(time.RFC3339)
  2362  	iter := adminClient.Entries(c,
  2363  		logadmin.Filter(
  2364  			// We filter our instances.delete errors as false positives. Delete event happens every second.
  2365  			fmt.Sprintf(`(NOT protoPayload.methodName:v1.compute.instances.delete) AND timestamp > "%s" AND severity>="ERROR"`,
  2366  				lastWeek)),
  2367  		logadmin.NewestFirst(),
  2368  	)
  2369  
  2370  	var entries []*logging.Entry
  2371  	for len(entries) < maxLines {
  2372  		entry, err := iter.Next()
  2373  		if err == iterator.Done {
  2374  			break
  2375  		}
  2376  		if err != nil {
  2377  			return nil, err
  2378  		}
  2379  		entries = append(entries, entry)
  2380  	}
  2381  
  2382  	var lines []string
  2383  	for _, entry := range entries {
  2384  		requestLog, isRequestLog := entry.Payload.(*proto.RequestLog)
  2385  		if isRequestLog {
  2386  			for _, logLine := range requestLog.Line {
  2387  				if logLine.GetSeverity() < ltype.LogSeverity_ERROR {
  2388  					continue
  2389  				}
  2390  				line := fmt.Sprintf("%v: %v %v %v \"%v\"",
  2391  					entry.Timestamp.Format(time.Stamp),
  2392  					requestLog.GetStatus(),
  2393  					requestLog.GetMethod(),
  2394  					requestLog.GetResource(),
  2395  					logLine.GetLogMessage())
  2396  				lines = append(lines, formatLogLine(line))
  2397  			}
  2398  		} else {
  2399  			line := fmt.Sprintf("%v: %v",
  2400  				entry.Timestamp.Format(time.Stamp),
  2401  				entry.Payload)
  2402  			lines = append(lines, formatLogLine(line))
  2403  		}
  2404  	}
  2405  
  2406  	buf := new(bytes.Buffer)
  2407  	for i := len(lines) - 1; i >= 0; i-- {
  2408  		buf.WriteString(lines[i])
  2409  	}
  2410  	return buf.Bytes(), nil
  2411  }
  2412  
  2413  func (j *bugJob) ui(c context.Context) (*uiJob, error) {
  2414  	err := j.load(c)
  2415  	if err != nil {
  2416  		return nil, err
  2417  	}
  2418  	return makeUIJob(c, j.job, j.key, j.bug, j.crash, j.build), nil
  2419  }
  2420  
  2421  func (b *bugJobs) uiAll(c context.Context) ([]*uiJob, error) {
  2422  	var ret []*uiJob
  2423  	for _, j := range b.all() {
  2424  		obj, err := j.ui(c)
  2425  		if err != nil {
  2426  			return nil, err
  2427  		}
  2428  		ret = append(ret, obj)
  2429  	}
  2430  	return ret, nil
  2431  }
  2432  
  2433  func (b *bugJobs) uiBestBisection(c context.Context) (*uiJob, error) {
  2434  	j := b.bestBisection()
  2435  	if j == nil {
  2436  		return nil, nil
  2437  	}
  2438  	return j.ui(c)
  2439  }
  2440  
  2441  func (b *bugJobs) uiBestFixCandidate(c context.Context) (*uiJob, error) {
  2442  	j := b.bestFixCandidate()
  2443  	if j == nil {
  2444  		return nil, nil
  2445  	}
  2446  	return j.ui(c)
  2447  }
  2448  
  2449  // bugExtLink should be preferred to bugLink since it provides a URL that's more consistent with
  2450  // links from email addresses.
  2451  func bugExtLink(c context.Context, bug *Bug) string {
  2452  	_, bugReporting, _, _, _ := currentReporting(c, bug)
  2453  	if bugReporting == nil || bugReporting.ID == "" {
  2454  		return bugLink(bug.keyHash(c))
  2455  	}
  2456  	return "/bug?extid=" + bugReporting.ID
  2457  }
  2458  
  2459  // bugLink should only be used when it's too inconvenient to actually load the bug from the DB.
  2460  func bugLink(id string) string {
  2461  	if id == "" {
  2462  		return ""
  2463  	}
  2464  	return "/bug?id=" + id
  2465  }