From 513246162767bca05d2ceb8f2cf9b28e6f67f6fa Mon Sep 17 00:00:00 2001 From: "commit-bot@chromium.org" Date: Tue, 13 May 2014 18:03:45 +0000 Subject: BugChomper utility - rewrite in Go BUG=skia: R=jcgregorio@google.com Author: borenet@google.com Review URL: https://codereview.chromium.org/274693002 git-svn-id: http://skia.googlecode.com/svn/trunk@14715 2bbb7eff-a529-9590-31e7-b0007b416f81 --- tools/bug_chomper/res/favicon.ico | Bin 0 -> 318 bytes tools/bug_chomper/res/style.css | 72 ++++ .../bug_chomper/res/third_party/jquery.tablednd.js | 314 +++++++++++++++++ tools/bug_chomper/run_server.sh | 11 + .../bug_chomper/src/issue_tracker/issue_tracker.go | 303 +++++++++++++++++ tools/bug_chomper/src/server/server.go | 376 +++++++++++++++++++++ tools/bug_chomper/templates/bug_chomper.html | 118 +++++++ tools/bug_chomper/templates/error.html | 12 + tools/bug_chomper/templates/submitted.html | 13 + 9 files changed, 1219 insertions(+) create mode 100644 tools/bug_chomper/res/favicon.ico create mode 100644 tools/bug_chomper/res/style.css create mode 100644 tools/bug_chomper/res/third_party/jquery.tablednd.js create mode 100755 tools/bug_chomper/run_server.sh create mode 100644 tools/bug_chomper/src/issue_tracker/issue_tracker.go create mode 100644 tools/bug_chomper/src/server/server.go create mode 100644 tools/bug_chomper/templates/bug_chomper.html create mode 100644 tools/bug_chomper/templates/error.html create mode 100644 tools/bug_chomper/templates/submitted.html diff --git a/tools/bug_chomper/res/favicon.ico b/tools/bug_chomper/res/favicon.ico new file mode 100644 index 0000000000..e7440c7512 Binary files /dev/null and b/tools/bug_chomper/res/favicon.ico differ diff --git a/tools/bug_chomper/res/style.css b/tools/bug_chomper/res/style.css new file mode 100644 index 0000000000..726e5aecd5 --- /dev/null +++ b/tools/bug_chomper/res/style.css @@ -0,0 +1,72 @@ +table#buglist { + border-collapse: collapse; + border-style: solid; + border-color: rgba(0, 0, 0, 1.0); + border-width: 3px; + width: 80%; + margin-left: 10%; + margin-right: 10%; +} + +tr { + border-color: rgba(0, 0, 0, 1.0); + border-style: dashed; + border-width: 1px 3px; +} + +tr.priority_Critical { + background-color: rgba(255, 0, 0, 0.3); +} + +tr.priority_High { + background-color: rgba(255, 165, 0, 0.3); +} + +tr.priority_Medium { + background-color: rgba(255, 255, 0, 0.3); +} + +tr.priority_Low { + background-color: rgba(0, 255, 0, 0.3); +} + +tr.priority_Never { + background-color: rgba(190, 190, 190, 0.3); +} + +tbody { + background-color: rgba(190, 190, 190, 0.1); +} + +tr.priority_row { + background-color: rgba(190, 190, 190, 0.1); + border-style: solid; +} + +tr.tr_head { + background-color: rgba(190, 190, 190, 0.5); +} + +#table_header { + text-align: center; +} + +td { + padding: 5px; +} + +td.priority_td { + text-align: center; +} + +a { + color: black; +} + +a:visited { + color: black; +} + +a:hover { + text-decoration: none; +} \ No newline at end of file diff --git a/tools/bug_chomper/res/third_party/jquery.tablednd.js b/tools/bug_chomper/res/third_party/jquery.tablednd.js new file mode 100644 index 0000000000..869f94effc --- /dev/null +++ b/tools/bug_chomper/res/third_party/jquery.tablednd.js @@ -0,0 +1,314 @@ +/** + * TableDnD plug-in for JQuery, allows you to drag and drop table rows + * You can set up various options to control how the system will work + * Copyright © Denis Howlett + * Licensed like jQuery, see http://docs.jquery.com/License. + * + * Configuration options: + * + * onDragStyle + * This is the style that is assigned to the row during drag. There are limitations to the styles that can be + * associated with a row (such as you can't assign a border—well you can, but it won't be + * displayed). (So instead consider using onDragClass.) The CSS style to apply is specified as + * a map (as used in the jQuery css(...) function). + * onDropStyle + * This is the style that is assigned to the row when it is dropped. As for onDragStyle, there are limitations + * to what you can do. Also this replaces the original style, so again consider using onDragClass which + * is simply added and then removed on drop. + * onDragClass + * This class is added for the duration of the drag and then removed when the row is dropped. It is more + * flexible than using onDragStyle since it can be inherited by the row cells and other content. The default + * is class is tDnD_whileDrag. So to use the default, simply customise this CSS class in your + * stylesheet. + * onDrop + * Pass a function that will be called when the row is dropped. The function takes 2 parameters: the table + * and the row that was dropped. You can work out the new order of the rows by using + * table.rows. + * onDragStart + * Pass a function that will be called when the user starts dragging. The function takes 2 parameters: the + * table and the row which the user has started to drag. + * onAllowDrop + * Pass a function that will be called as a row is over another row. If the function returns true, allow + * dropping on that row, otherwise not. The function takes 2 parameters: the dragged row and the row under + * the cursor. It returns a boolean: true allows the drop, false doesn't allow it. + * scrollAmount + * This is the number of pixels to scroll if the user moves the mouse cursor to the top or bottom of the + * window. The page should automatically scroll up or down as appropriate (tested in IE6, IE7, Safari, FF2, + * FF3 beta) + * + * Other ways to control behaviour: + * + * Add class="nodrop" to any rows for which you don't want to allow dropping, and class="nodrag" to any rows + * that you don't want to be draggable. + * + * Inside the onDrop method you can also call $.tableDnD.serialize() this returns a string of the form + * []=&[]= so that you can send this back to the server. The table must have + * an ID as must all the rows. + * + * Known problems: + * - Auto-scoll has some problems with IE7 (it scrolls even when it shouldn't), work-around: set scrollAmount to 0 + * + * Version 0.2: 2008-02-20 First public version + * Version 0.3: 2008-02-07 Added onDragStart option + * Made the scroll amount configurable (default is 5 as before) + * Version 0.4: 2008-03-15 Changed the noDrag/noDrop attributes to nodrag/nodrop classes + * Added onAllowDrop to control dropping + * Fixed a bug which meant that you couldn't set the scroll amount in both directions + * Added serialise method + */ +jQuery.tableDnD = { + /** Keep hold of the current table being dragged */ + currentTable : null, + /** Keep hold of the current drag object if any */ + dragObject: null, + /** The current mouse offset */ + mouseOffset: null, + /** Remember the old value of Y so that we don't do too much processing */ + oldY: 0, + + /** Actually build the structure */ + build: function(options) { + // Make sure options exists + options = options || {}; + // Set up the defaults if any + + this.each(function() { + // Remember the options + this.tableDnDConfig = { + onDragStyle: options.onDragStyle, + onDropStyle: options.onDropStyle, + // Add in the default class for whileDragging + onDragClass: options.onDragClass ? options.onDragClass : "tDnD_whileDrag", + onDrop: options.onDrop, + onDragStart: options.onDragStart, + scrollAmount: options.scrollAmount ? options.scrollAmount : 5 + }; + // Now make the rows draggable + jQuery.tableDnD.makeDraggable(this); + }); + + // Now we need to capture the mouse up and mouse move event + // We can use bind so that we don't interfere with other event handlers + jQuery(document) + .bind('mousemove', jQuery.tableDnD.mousemove) + .bind('mouseup', jQuery.tableDnD.mouseup); + + // Don't break the chain + return this; + }, + + /** This function makes all the rows on the table draggable apart from those marked as "NoDrag" */ + makeDraggable: function(table) { + // Now initialise the rows + var rows = table.rows; //getElementsByTagName("tr") + var config = table.tableDnDConfig; + for (var i=0; i jQuery.tableDnD.oldY; + // update the old value + jQuery.tableDnD.oldY = y; + // update the style to show we're dragging + if (config.onDragClass) { + dragObj.addClass(config.onDragClass); + } else { + dragObj.css(config.onDragStyle); + } + // If we're over a row then move the dragged row to there so that the user sees the + // effect dynamically + var currentRow = jQuery.tableDnD.findDropTargetRow(dragObj, y); + if (currentRow) { + // TODO worry about what happens when there are multiple TBODIES + if (movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow.nextSibling); + } else if (! movingDown && jQuery.tableDnD.dragObject != currentRow) { + jQuery.tableDnD.dragObject.parentNode.insertBefore(jQuery.tableDnD.dragObject, currentRow); + } + } + } + + return false; + }, + + /** We're only worried about the y position really, because we can only move rows up and down */ + findDropTargetRow: function(draggedRow, y) { + var rows = jQuery.tableDnD.currentTable.rows; + for (var i=0; i rowY - rowHeight) && (y < (rowY + rowHeight))) { + // that's the row we're over + // If it's the same as the current row, ignore it + if (row == draggedRow) {return null;} + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + if (config.onAllowDrop) { + if (config.onAllowDrop(draggedRow, row)) { + return row; + } else { + return null; + } + } else { + // If a row has nodrop class, then don't allow dropping (inspired by John Tarr and Famic) + var nodrop = $(row).hasClass("nodrop"); + if (! nodrop) { + return row; + } else { + return null; + } + } + return row; + } + } + return null; + }, + + mouseup: function(e) { + if (jQuery.tableDnD.currentTable && jQuery.tableDnD.dragObject) { + var droppedRow = jQuery.tableDnD.dragObject; + var config = jQuery.tableDnD.currentTable.tableDnDConfig; + // If we have a dragObject, then we need to release it, + // The row will already have been moved to the right place so we just reset stuff + if (config.onDragClass) { + jQuery(droppedRow).removeClass(config.onDragClass); + } else { + jQuery(droppedRow).css(config.onDropStyle); + } + jQuery.tableDnD.dragObject = null; + if (config.onDrop) { + // Call the onDrop method if there is one + config.onDrop(jQuery.tableDnD.currentTable, droppedRow); + } + jQuery.tableDnD.currentTable = null; // let go of the table too + } + }, + + serialize: function() { + if (jQuery.tableDnD.currentTable) { + var result = ""; + var tableId = jQuery.tableDnD.currentTable.id; + var rows = jQuery.tableDnD.currentTable.rows; + for (var i=0; i 0) result += "&"; + result += tableId + '[]=' + rows[i].id; + } + return result; + } else { + return "Error: No Table id set, you need to set an id on your table and every row"; + } + } +} + +jQuery.fn.extend( + { + tableDnD : jQuery.tableDnD.build + } +); \ No newline at end of file diff --git a/tools/bug_chomper/run_server.sh b/tools/bug_chomper/run_server.sh new file mode 100755 index 0000000000..5bedb4f9ae --- /dev/null +++ b/tools/bug_chomper/run_server.sh @@ -0,0 +1,11 @@ +if [[ -z `which go` ]]; then + echo "Please install Go before running the server." + exit 1 +fi + +go get github.com/gorilla/securecookie + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd $DIR +GOPATH="$GOPATH:$DIR" go run $DIR/src/server/server.go $@ + diff --git a/tools/bug_chomper/src/issue_tracker/issue_tracker.go b/tools/bug_chomper/src/issue_tracker/issue_tracker.go new file mode 100644 index 0000000000..e4854f5ee6 --- /dev/null +++ b/tools/bug_chomper/src/issue_tracker/issue_tracker.go @@ -0,0 +1,303 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* + Utilities for interacting with the GoogleCode issue tracker. + + Example usage: + issueTracker := issue_tracker.MakeIssueTraker(myOAuthConfigFile) + authURL := issueTracker.MakeAuthRequestURL() + // Visit the authURL to obtain an authorization code. + issueTracker.UpgradeCode(code) + // Now issueTracker can be used to retrieve and edit issues. +*/ +package issue_tracker + +import ( + "bytes" + "code.google.com/p/goauth2/oauth" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" +) + +// BugPriorities are the possible values for "Priority-*" labels for issues. +var BugPriorities = []string{"Critical", "High", "Medium", "Low", "Never"} + +var apiScope = []string{ + "https://www.googleapis.com/auth/projecthosting", + "https://www.googleapis.com/auth/userinfo.email", +} + +const issueApiURL = "https://www.googleapis.com/projecthosting/v2/projects/" +const issueURL = "https://code.google.com/p/skia/issues/detail?id=" +const personApiURL = "https://www.googleapis.com/userinfo/v2/me" + +// Enum for determining whether a label has been added, removed, or is +// unchanged. +const ( + labelAdded = iota + labelRemoved + labelUnchanged +) + +// loadOAuthConfig reads the OAuth given config file path and returns an +// appropriate oauth.Config. +func loadOAuthConfig(oauthConfigFile string) (*oauth.Config, error) { + errFmt := "failed to read OAuth config file: %s" + fileContents, err := ioutil.ReadFile(oauthConfigFile) + if err != nil { + return nil, fmt.Errorf(errFmt, err) + } + var decodedJson map[string]struct { + AuthURL string `json:"auth_uri"` + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + TokenURL string `json:"token_uri"` + } + if err := json.Unmarshal(fileContents, &decodedJson); err != nil { + return nil, fmt.Errorf(errFmt, err) + } + config, ok := decodedJson["web"] + if !ok { + return nil, fmt.Errorf(errFmt, err) + } + return &oauth.Config{ + ClientId: config.ClientId, + ClientSecret: config.ClientSecret, + Scope: strings.Join(apiScope, " "), + AuthURL: config.AuthURL, + TokenURL: config.TokenURL, + }, nil +} + +// Issue contains information about an issue. +type Issue struct { + Id int `json:"id"` + Project string `json:"projectId"` + Title string `json:"title"` + Labels []string `json:"labels"` +} + +// URL returns the URL of a given issue. +func (i Issue) URL() string { + return issueURL + strconv.Itoa(i.Id) +} + +// IssueList represents a list of issues from the IssueTracker. +type IssueList struct { + TotalResults int `json:"totalResults"` + Items []*Issue `json:"items"` +} + +// IssueTracker is the primary point of contact with the issue tracker, +// providing methods for authenticating to and interacting with it. +type IssueTracker struct { + OAuthConfig *oauth.Config + OAuthTransport *oauth.Transport +} + +// MakeIssueTracker creates and returns an IssueTracker with authentication +// configuration from the given authConfigFile. +func MakeIssueTracker(authConfigFile string, redirectURL string) (*IssueTracker, error) { + oauthConfig, err := loadOAuthConfig(authConfigFile) + if err != nil { + return nil, fmt.Errorf( + "failed to create IssueTracker: %s", err) + } + oauthConfig.RedirectURL = redirectURL + return &IssueTracker{ + OAuthConfig: oauthConfig, + OAuthTransport: &oauth.Transport{Config: oauthConfig}, + }, nil +} + +// MakeAuthRequestURL returns an authentication request URL which can be used +// to obtain an authorization code via user sign-in. +func (it IssueTracker) MakeAuthRequestURL() string { + // NOTE: Need to add XSRF protection if we ever want to run this on a public + // server. + return it.OAuthConfig.AuthCodeURL(it.OAuthConfig.RedirectURL) +} + +// IsAuthenticated determines whether the IssueTracker has sufficient +// permissions to retrieve and edit Issues. +func (it IssueTracker) IsAuthenticated() bool { + return it.OAuthTransport.Token != nil +} + +// UpgradeCode exchanges the single-use authorization code, obtained by +// following the URL obtained from IssueTracker.MakeAuthRequestURL, for a +// multi-use, session token. This is required before IssueTracker can retrieve +// and edit issues. +func (it *IssueTracker) UpgradeCode(code string) error { + token, err := it.OAuthTransport.Exchange(code) + if err == nil { + it.OAuthTransport.Token = token + return nil + } else { + return fmt.Errorf( + "failed to exchange single-user auth code: %s", err) + } +} + +// GetLoggedInUser retrieves the email address of the authenticated user. +func (it IssueTracker) GetLoggedInUser() (string, error) { + errFmt := "error retrieving user email: %s" + if !it.IsAuthenticated() { + return "", fmt.Errorf(errFmt, "User is not authenticated!") + } + resp, err := it.OAuthTransport.Client().Get(personApiURL) + if err != nil { + return "", fmt.Errorf(errFmt, err) + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf(errFmt, fmt.Sprintf( + "user data API returned code %d: %v", resp.StatusCode, string(body))) + } + userInfo := struct { + Email string `json:"email"` + }{} + if err := json.Unmarshal(body, &userInfo); err != nil { + return "", fmt.Errorf(errFmt, err) + } + return userInfo.Email, nil +} + +// GetBug retrieves the Issue with the given ID from the IssueTracker. +func (it IssueTracker) GetBug(project string, id int) (*Issue, error) { + errFmt := fmt.Sprintf("error retrieving issue %d: %s", id, "%s") + if !it.IsAuthenticated() { + return nil, fmt.Errorf(errFmt, "user is not authenticated!") + } + requestURL := issueApiURL + project + "/issues/" + strconv.Itoa(id) + resp, err := it.OAuthTransport.Client().Get(requestURL) + if err != nil { + return nil, fmt.Errorf(errFmt, err) + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf(errFmt, fmt.Sprintf( + "issue tracker returned code %d:%v", resp.StatusCode, string(body))) + } + var issue Issue + if err := json.Unmarshal(body, &issue); err != nil { + return nil, fmt.Errorf(errFmt, err) + } + return &issue, nil +} + +// GetBugs retrieves all Issues with the given owner from the IssueTracker, +// returning an IssueList. +func (it IssueTracker) GetBugs(project string, owner string) (*IssueList, error) { + errFmt := "error retrieving issues: %s" + if !it.IsAuthenticated() { + return nil, fmt.Errorf(errFmt, "user is not authenticated!") + } + params := map[string]string{ + "owner": url.QueryEscape(owner), + "can": "open", + "maxResults": "9999", + } + requestURL := issueApiURL + project + "/issues?" + first := true + for k, v := range params { + if first { + first = false + } else { + requestURL += "&" + } + requestURL += k + "=" + v + } + resp, err := it.OAuthTransport.Client().Get(requestURL) + if err != nil { + return nil, fmt.Errorf(errFmt, err) + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf(errFmt, fmt.Sprintf( + "issue tracker returned code %d:%v", resp.StatusCode, string(body))) + } + + var bugList IssueList + if err := json.Unmarshal(body, &bugList); err != nil { + return nil, fmt.Errorf(errFmt, err) + } + return &bugList, nil +} + +// SubmitIssueChanges creates a comment on the given Issue which modifies it +// according to the contents of the passed-in Issue struct. +func (it IssueTracker) SubmitIssueChanges(issue *Issue, comment string) error { + errFmt := "Error updating issue " + strconv.Itoa(issue.Id) + ": %s" + if !it.IsAuthenticated() { + return fmt.Errorf(errFmt, "user is not authenticated!") + } + oldIssue, err := it.GetBug(issue.Project, issue.Id) + if err != nil { + return fmt.Errorf(errFmt, err) + } + postData := struct { + Content string `json:"content"` + Updates struct { + Title *string `json:"summary"` + Labels []string `json:"labels"` + } `json:"updates"` + }{ + Content: comment, + } + if issue.Title != oldIssue.Title { + postData.Updates.Title = &issue.Title + } + // TODO(borenet): Add other issue attributes, eg. Owner. + labels := make(map[string]int) + for _, label := range issue.Labels { + labels[label] = labelAdded + } + for _, label := range oldIssue.Labels { + if _, ok := labels[label]; ok { + labels[label] = labelUnchanged + } else { + labels[label] = labelRemoved + } + } + labelChanges := make([]string, 0) + for labelName, present := range labels { + if present == labelRemoved { + labelChanges = append(labelChanges, "-"+labelName) + } else if present == labelAdded { + labelChanges = append(labelChanges, labelName) + } + } + if len(labelChanges) > 0 { + postData.Updates.Labels = labelChanges + } + + postBytes, err := json.Marshal(&postData) + if err != nil { + return fmt.Errorf(errFmt, err) + } + requestURL := issueApiURL + issue.Project + "/issues/" + + strconv.Itoa(issue.Id) + "/comments" + resp, err := it.OAuthTransport.Client().Post( + requestURL, "application/json", bytes.NewReader(postBytes)) + if err != nil { + return fmt.Errorf(errFmt, err) + } + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf(errFmt, fmt.Sprintf( + "Issue tracker returned code %d:%v", resp.StatusCode, string(body))) + } + return nil +} diff --git a/tools/bug_chomper/src/server/server.go b/tools/bug_chomper/src/server/server.go new file mode 100644 index 0000000000..9fb21ed594 --- /dev/null +++ b/tools/bug_chomper/src/server/server.go @@ -0,0 +1,376 @@ +// Copyright (c) 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/* + Serves a webpage for easy management of Skia bugs. + + WARNING: This server is NOT secure and should not be made publicly + accessible. +*/ + +package main + +import ( + "encoding/json" + "flag" + "fmt" + "html/template" + "issue_tracker" + "log" + "net/http" + "net/url" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +import "github.com/gorilla/securecookie" + +const certFile = "certs/cert.pem" +const keyFile = "certs/key.pem" +const issueComment = "Edited by BugChomper" +const oauthCallbackPath = "/oauth2callback" +const oauthConfigFile = "oauth_client_secret.json" +const defaultPort = 8000 +const localHost = "127.0.0.1" +const maxSessionLen = time.Duration(3600 * time.Second) +const priorityPrefix = "Priority-" +const project = "skia" +const cookieName = "BugChomperCookie" + +var scheme = "http" + +var curdir, _ = filepath.Abs(".") +var templatePath, _ = filepath.Abs("templates") +var templates = template.Must(template.ParseFiles( + path.Join(templatePath, "bug_chomper.html"), + path.Join(templatePath, "submitted.html"), + path.Join(templatePath, "error.html"))) + +var hashKey = securecookie.GenerateRandomKey(32) +var blockKey = securecookie.GenerateRandomKey(32) +var secureCookie = securecookie.New(hashKey, blockKey) + +// SessionState contains data for a given session. +type SessionState struct { + IssueTracker *issue_tracker.IssueTracker + OrigRequestURL string + SessionStart time.Time +} + +// getAbsoluteURL returns the absolute URL of the given Request. +func getAbsoluteURL(r *http.Request) string { + return scheme + "://" + r.Host + r.URL.Path +} + +// getOAuth2CallbackURL returns a callback URL to be used by the OAuth2 login +// page. +func getOAuth2CallbackURL(r *http.Request) string { + return scheme + "://" + r.Host + oauthCallbackPath +} + +func saveSession(session *SessionState, w http.ResponseWriter, r *http.Request) error { + encodedSession, err := secureCookie.Encode(cookieName, session) + if err != nil { + return fmt.Errorf("unable to encode session state: %s", err) + } + cookie := &http.Cookie{ + Name: cookieName, + Value: encodedSession, + Domain: strings.Split(r.Host, ":")[0], + Path: "/", + HttpOnly: true, + } + http.SetCookie(w, cookie) + return nil +} + +// makeSession creates a new session for the Request. +func makeSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) { + log.Println("Creating new session.") + // Create the session state. + issueTracker, err := issue_tracker.MakeIssueTracker( + oauthConfigFile, getOAuth2CallbackURL(r)) + if err != nil { + return nil, fmt.Errorf("unable to create IssueTracker for session: %s", err) + } + session := SessionState{ + IssueTracker: issueTracker, + OrigRequestURL: getAbsoluteURL(r), + SessionStart: time.Now(), + } + + // Encode and store the session state. + if err := saveSession(&session, w, r); err != nil { + return nil, err + } + + return &session, nil +} + +// getSession retrieves the active SessionState or creates and returns a new +// SessionState. +func getSession(w http.ResponseWriter, r *http.Request) (*SessionState, error) { + cookie, err := r.Cookie(cookieName) + if err != nil { + log.Println("No cookie found! Starting new session.") + return makeSession(w, r) + } + var session SessionState + if err := secureCookie.Decode(cookieName, cookie.Value, &session); err != nil { + log.Printf("Invalid or corrupted session. Starting another: %s", err.Error()) + return makeSession(w, r) + } + + currentTime := time.Now() + if currentTime.Sub(session.SessionStart) > maxSessionLen { + log.Printf("Session starting at %s is expired. Starting another.", + session.SessionStart.Format(time.RFC822)) + return makeSession(w, r) + } + saveSession(&session, w, r) + return &session, nil +} + +// reportError serves the error page with the given message. +func reportError(w http.ResponseWriter, msg string, code int) { + errData := struct { + Code int + CodeString string + Message string + }{ + Code: code, + CodeString: http.StatusText(code), + Message: msg, + } + w.WriteHeader(code) + err := templates.ExecuteTemplate(w, "error.html", errData) + if err != nil { + log.Println("Failed to display error.html!!") + } +} + +// makeBugChomperPage builds and serves the BugChomper page. +func makeBugChomperPage(w http.ResponseWriter, r *http.Request) { + session, err := getSession(w, r) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + issueTracker := session.IssueTracker + user, err := issueTracker.GetLoggedInUser() + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + log.Println("Loading bugs for " + user) + bugList, err := issueTracker.GetBugs(project, user) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + bugsById := make(map[string]*issue_tracker.Issue) + bugsByPriority := make(map[string][]*issue_tracker.Issue) + for _, bug := range bugList.Items { + bugsById[strconv.Itoa(bug.Id)] = bug + var bugPriority string + for _, label := range bug.Labels { + if strings.HasPrefix(label, priorityPrefix) { + bugPriority = label[len(priorityPrefix):] + } + } + if _, ok := bugsByPriority[bugPriority]; !ok { + bugsByPriority[bugPriority] = make( + []*issue_tracker.Issue, 0) + } + bugsByPriority[bugPriority] = append( + bugsByPriority[bugPriority], bug) + } + bugsJson, err := json.Marshal(bugsById) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + data := struct { + Title string + User string + BugsJson template.JS + BugsByPriority *map[string][]*issue_tracker.Issue + Priorities []string + PriorityPrefix string + }{ + Title: "BugChomper", + User: user, + BugsJson: template.JS(string(bugsJson)), + BugsByPriority: &bugsByPriority, + Priorities: issue_tracker.BugPriorities, + PriorityPrefix: priorityPrefix, + } + + if err := templates.ExecuteTemplate(w, "bug_chomper.html", data); err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// authIfNeeded determines whether the current user is logged in. If not, it +// redirects to a login page. Returns true if the user is redirected and false +// otherwise. +func authIfNeeded(w http.ResponseWriter, r *http.Request) bool { + session, err := getSession(w, r) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return false + } + issueTracker := session.IssueTracker + if !issueTracker.IsAuthenticated() { + loginURL := issueTracker.MakeAuthRequestURL() + log.Println("Redirecting for login:", loginURL) + http.Redirect(w, r, loginURL, http.StatusTemporaryRedirect) + return true + } + return false +} + +// submitData attempts to submit data from a POST request to the IssueTracker. +func submitData(w http.ResponseWriter, r *http.Request) { + session, err := getSession(w, r) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + issueTracker := session.IssueTracker + edits := r.FormValue("all_edits") + var editsMap map[string]*issue_tracker.Issue + if err := json.Unmarshal([]byte(edits), &editsMap); err != nil { + errMsg := "Could not parse edits from form response: " + err.Error() + reportError(w, errMsg, http.StatusInternalServerError) + return + } + data := struct { + Title string + Message string + BackLink string + }{} + if len(editsMap) == 0 { + data.Title = "No Changes Submitted" + data.Message = "You didn't change anything!" + data.BackLink = "" + if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + return + } + errorList := make([]error, 0) + for issueId, newIssue := range editsMap { + log.Println("Editing issue " + issueId) + if err := issueTracker.SubmitIssueChanges(newIssue, issueComment); err != nil { + errorList = append(errorList, err) + } + } + if len(errorList) > 0 { + errorStrings := "" + for _, err := range errorList { + errorStrings += err.Error() + "\n" + } + errMsg := "Not all changes could be submitted: \n" + errorStrings + reportError(w, errMsg, http.StatusInternalServerError) + return + } + data.Title = "Submitted Changes" + data.Message = "Your changes were submitted to the issue tracker." + data.BackLink = "" + if err := templates.ExecuteTemplate(w, "submitted.html", data); err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + return + } + return +} + +// handleBugChomper handles HTTP requests for the bug_chomper page. +func handleBugChomper(w http.ResponseWriter, r *http.Request) { + if authIfNeeded(w, r) { + return + } + switch r.Method { + case "GET": + makeBugChomperPage(w, r) + case "POST": + submitData(w, r) + } +} + +// handleOAuth2Callback handles callbacks from the OAuth2 sign-in. +func handleOAuth2Callback(w http.ResponseWriter, r *http.Request) { + session, err := getSession(w, r) + if err != nil { + reportError(w, err.Error(), http.StatusInternalServerError) + } + issueTracker := session.IssueTracker + invalidLogin := "Invalid login credentials" + params, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + reportError(w, invalidLogin+": "+err.Error(), http.StatusForbidden) + return + } + code, ok := params["code"] + if !ok { + reportError(w, invalidLogin+": redirect did not include auth code.", + http.StatusForbidden) + return + } + log.Println("Upgrading auth token:", code[0]) + if err := issueTracker.UpgradeCode(code[0]); err != nil { + errMsg := "failed to upgrade token: " + err.Error() + reportError(w, errMsg, http.StatusForbidden) + return + } + if err := saveSession(session, w, r); err != nil { + reportError(w, "failed to save session: "+err.Error(), + http.StatusInternalServerError) + return + } + http.Redirect(w, r, session.OrigRequestURL, http.StatusTemporaryRedirect) + return +} + +// handleRoot is the handler function for all HTTP requests at the root level. +func handleRoot(w http.ResponseWriter, r *http.Request) { + log.Println("Fetching " + r.URL.Path) + if r.URL.Path == "/" || r.URL.Path == "/index.html" { + handleBugChomper(w, r) + return + } + http.NotFound(w, r) +} + +// Run the BugChomper server. +func main() { + var public bool + flag.BoolVar( + &public, "public", false, "Make this server publicly accessible.") + flag.Parse() + + http.HandleFunc("/", handleRoot) + http.HandleFunc(oauthCallbackPath, handleOAuth2Callback) + http.Handle("/res/", http.FileServer(http.Dir(curdir))) + port := ":" + strconv.Itoa(defaultPort) + log.Println("Server is running at " + scheme + "://" + localHost + port) + var err error + if public { + log.Println("WARNING: This server is not secure and should not be made " + + "publicly accessible.") + scheme = "https" + err = http.ListenAndServeTLS(port, certFile, keyFile, nil) + } else { + scheme = "http" + err = http.ListenAndServe(localHost+port, nil) + } + if err != nil { + log.Println(err.Error()) + } +} diff --git a/tools/bug_chomper/templates/bug_chomper.html b/tools/bug_chomper/templates/bug_chomper.html new file mode 100644 index 0000000000..df08570b86 --- /dev/null +++ b/tools/bug_chomper/templates/bug_chomper.html @@ -0,0 +1,118 @@ + + +{{.Title}} + + + + + + + +

BugChomper

+ +
+ + +
+ + + + + + + + + + + + + {{with $all_data := .}} + {{range $index, $priority := index $all_data.Priorities}} + + + + {{range $index, $bug := index $all_data.BugsByPriority $priority}} + + + + + + {{end}} + {{end}} + {{end}} + +

Open bugs for {{.User}}

IDPriorityTitle
Priority {{$priority}}
+ {{$bug.Id}} + {{$priority}}{{$bug.Title}}
+ + + + diff --git a/tools/bug_chomper/templates/error.html b/tools/bug_chomper/templates/error.html new file mode 100644 index 0000000000..1e8fcda491 --- /dev/null +++ b/tools/bug_chomper/templates/error.html @@ -0,0 +1,12 @@ + + +Error {{.Code}}: {{.CodeString}} + + + + +

Error {{.Code}}: {{.CodeString}}

+{{.Message}} +
+ + diff --git a/tools/bug_chomper/templates/submitted.html b/tools/bug_chomper/templates/submitted.html new file mode 100644 index 0000000000..2b09c238a8 --- /dev/null +++ b/tools/bug_chomper/templates/submitted.html @@ -0,0 +1,13 @@ + + +{{.Title}} + + + + +

{{.Title}}

+{{.Message}} +
+Go back + + -- cgit v1.2.3