1// Copyright 2013 The Go Authors. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd.
6
7// Package lintapp implements the go-lint.appspot.com server.
8package lintapp
9
10import (
11	"bytes"
12	"context"
13	"encoding/gob"
14	"fmt"
15	"html/template"
16	"net/http"
17	"os"
18	"path/filepath"
19	"strconv"
20	"strings"
21	"time"
22
23	"google.golang.org/appengine"
24	"google.golang.org/appengine/datastore"
25	"google.golang.org/appengine/log"
26	"google.golang.org/appengine/urlfetch"
27
28	"github.com/golang/gddo/gosrc"
29	"github.com/golang/gddo/httputil"
30
31	"github.com/golang/lint"
32)
33
34func init() {
35	http.Handle("/", handlerFunc(serveRoot))
36	http.Handle("/-/bot", handlerFunc(serveBot))
37	http.Handle("/-/refresh", handlerFunc(serveRefresh))
38	if s := os.Getenv("CONTACT_EMAIL"); s != "" {
39		contactEmail = s
40	}
41}
42
43var (
44	contactEmail    = "golang-dev@googlegroups.com"
45	homeTemplate    = parseTemplate("common.html", "index.html")
46	packageTemplate = parseTemplate("common.html", "package.html")
47	errorTemplate   = parseTemplate("common.html", "error.html")
48	templateFuncs   = template.FuncMap{
49		"timeago":      timeagoFn,
50		"contactEmail": contactEmailFn,
51	}
52)
53
54func parseTemplate(fnames ...string) *template.Template {
55	paths := make([]string, len(fnames))
56	for i := range fnames {
57		paths[i] = filepath.Join("assets/templates", fnames[i])
58	}
59	t, err := template.New("").Funcs(templateFuncs).ParseFiles(paths...)
60	if err != nil {
61		panic(err)
62	}
63	t = t.Lookup("ROOT")
64	if t == nil {
65		panic(fmt.Sprintf("ROOT template not found in %v", fnames))
66	}
67	return t
68}
69
70func contactEmailFn() string {
71	return contactEmail
72}
73
74func timeagoFn(t time.Time) string {
75	d := time.Since(t)
76	switch {
77	case d < time.Second:
78		return "just now"
79	case d < 2*time.Second:
80		return "one second ago"
81	case d < time.Minute:
82		return fmt.Sprintf("%d seconds ago", d/time.Second)
83	case d < 2*time.Minute:
84		return "one minute ago"
85	case d < time.Hour:
86		return fmt.Sprintf("%d minutes ago", d/time.Minute)
87	case d < 2*time.Hour:
88		return "one hour ago"
89	case d < 48*time.Hour:
90		return fmt.Sprintf("%d hours ago", d/time.Hour)
91	default:
92		return fmt.Sprintf("%d days ago", d/(time.Hour*24))
93	}
94}
95
96func writeResponse(w http.ResponseWriter, status int, t *template.Template, v interface{}) error {
97	var buf bytes.Buffer
98	if err := t.Execute(&buf, v); err != nil {
99		return err
100	}
101	w.Header().Set("Content-Type", "text/html; charset=utf-8")
102	w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
103	w.WriteHeader(status)
104	_, err := w.Write(buf.Bytes())
105	return err
106}
107
108func writeErrorResponse(w http.ResponseWriter, status int) error {
109	return writeResponse(w, status, errorTemplate, http.StatusText(status))
110}
111
112func httpClient(r *http.Request) *http.Client {
113	c := appengine.NewContext(r)
114	return &http.Client{
115		Transport: &httputil.AuthTransport{
116			GithubToken:        os.Getenv("GITHUB_TOKEN"),
117			GithubClientID:     os.Getenv("GITHUB_CLIENT_ID"),
118			GithubClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
119			Base:               &urlfetch.Transport{Context: c},
120			UserAgent:          fmt.Sprintf("%s (+http://%s/-/bot)", appengine.AppID(c), r.Host),
121		},
122	}
123}
124
125const version = 1
126
127type storePackage struct {
128	Data    []byte
129	Version int
130}
131
132type lintPackage struct {
133	Files   []*lintFile
134	Path    string
135	Updated time.Time
136	LineFmt string
137	URL     string
138}
139
140type lintFile struct {
141	Name     string
142	Problems []*lintProblem
143	URL      string
144}
145
146type lintProblem struct {
147	Line       int
148	Text       string
149	LineText   string
150	Confidence float64
151	Link       string
152}
153
154func putPackage(c context.Context, importPath string, pkg *lintPackage) error {
155	var buf bytes.Buffer
156	if err := gob.NewEncoder(&buf).Encode(pkg); err != nil {
157		return err
158	}
159	_, err := datastore.Put(c,
160		datastore.NewKey(c, "Package", importPath, 0, nil),
161		&storePackage{Data: buf.Bytes(), Version: version})
162	return err
163}
164
165func getPackage(c context.Context, importPath string) (*lintPackage, error) {
166	var spkg storePackage
167	if err := datastore.Get(c, datastore.NewKey(c, "Package", importPath, 0, nil), &spkg); err != nil {
168		if err == datastore.ErrNoSuchEntity {
169			err = nil
170		}
171		return nil, err
172	}
173	if spkg.Version != version {
174		return nil, nil
175	}
176	var pkg lintPackage
177	if err := gob.NewDecoder(bytes.NewReader(spkg.Data)).Decode(&pkg); err != nil {
178		return nil, err
179	}
180	return &pkg, nil
181}
182
183func runLint(r *http.Request, importPath string) (*lintPackage, error) {
184	dir, err := gosrc.Get(appengine.NewContext(r), httpClient(r), importPath, "")
185	if err != nil {
186		return nil, err
187	}
188
189	pkg := lintPackage{
190		Path:    importPath,
191		Updated: time.Now(),
192		LineFmt: dir.LineFmt,
193		URL:     dir.BrowseURL,
194	}
195	linter := lint.Linter{}
196	for _, f := range dir.Files {
197		if !strings.HasSuffix(f.Name, ".go") {
198			continue
199		}
200		problems, err := linter.Lint(f.Name, f.Data)
201		if err == nil && len(problems) == 0 {
202			continue
203		}
204		file := lintFile{Name: f.Name, URL: f.BrowseURL}
205		if err != nil {
206			file.Problems = []*lintProblem{{Text: err.Error()}}
207		} else {
208			for _, p := range problems {
209				file.Problems = append(file.Problems, &lintProblem{
210					Line:       p.Position.Line,
211					Text:       p.Text,
212					LineText:   p.LineText,
213					Confidence: p.Confidence,
214					Link:       p.Link,
215				})
216			}
217		}
218		if len(file.Problems) > 0 {
219			pkg.Files = append(pkg.Files, &file)
220		}
221	}
222
223	if err := putPackage(appengine.NewContext(r), importPath, &pkg); err != nil {
224		return nil, err
225	}
226
227	return &pkg, nil
228}
229
230func filterByConfidence(r *http.Request, pkg *lintPackage) {
231	minConfidence, err := strconv.ParseFloat(r.FormValue("minConfidence"), 64)
232	if err != nil {
233		minConfidence = 0.8
234	}
235	for _, f := range pkg.Files {
236		j := 0
237		for i := range f.Problems {
238			if f.Problems[i].Confidence >= minConfidence {
239				f.Problems[j] = f.Problems[i]
240				j++
241			}
242		}
243		f.Problems = f.Problems[:j]
244	}
245}
246
247type handlerFunc func(http.ResponseWriter, *http.Request) error
248
249func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
250	c := appengine.NewContext(r)
251	err := f(w, r)
252	if err == nil {
253		return
254	} else if gosrc.IsNotFound(err) {
255		writeErrorResponse(w, 404)
256	} else if e, ok := err.(*gosrc.RemoteError); ok {
257		log.Infof(c, "Remote error %s: %v", e.Host, e)
258		writeResponse(w, 500, errorTemplate, fmt.Sprintf("Error accessing %s.", e.Host))
259	} else if err != nil {
260		log.Errorf(c, "Internal error %v", err)
261		writeErrorResponse(w, 500)
262	}
263}
264
265func serveRoot(w http.ResponseWriter, r *http.Request) error {
266	switch {
267	case r.Method != "GET" && r.Method != "HEAD":
268		return writeErrorResponse(w, 405)
269	case r.URL.Path == "/":
270		return writeResponse(w, 200, homeTemplate, nil)
271	default:
272		importPath := r.URL.Path[1:]
273		if !gosrc.IsValidPath(importPath) {
274			return gosrc.NotFoundError{Message: "bad path"}
275		}
276		c := appengine.NewContext(r)
277		pkg, err := getPackage(c, importPath)
278		if pkg == nil && err == nil {
279			pkg, err = runLint(r, importPath)
280		}
281		if err != nil {
282			return err
283		}
284		filterByConfidence(r, pkg)
285		return writeResponse(w, 200, packageTemplate, pkg)
286	}
287}
288
289func serveRefresh(w http.ResponseWriter, r *http.Request) error {
290	if r.Method != "POST" {
291		return writeErrorResponse(w, 405)
292	}
293	importPath := r.FormValue("importPath")
294	pkg, err := runLint(r, importPath)
295	if err != nil {
296		return err
297	}
298	http.Redirect(w, r, "/"+pkg.Path, 301)
299	return nil
300}
301
302func serveBot(w http.ResponseWriter, r *http.Request) error {
303	c := appengine.NewContext(r)
304	_, err := fmt.Fprintf(w, "Contact %s for help with the %s bot.", contactEmail, appengine.AppID(c))
305	return err
306}
307