You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

308 lines
7.5 KiB

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