| // Copyright 2016 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package portal |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| |
| "github.com/julienschmidt/httprouter" |
| |
| "go.chromium.org/luci/server/auth" |
| "go.chromium.org/luci/server/auth/xsrf" |
| "go.chromium.org/luci/server/router" |
| "go.chromium.org/luci/server/templates" |
| ) |
| |
| type fieldWithValue struct { |
| Field |
| Value string |
| } |
| |
| type validationError struct { |
| FieldTitle string |
| Value string |
| Error string |
| } |
| |
| type pageCallback func(id string, p Page) error |
| |
| func withPage(c context.Context, rw http.ResponseWriter, p httprouter.Params, cb pageCallback) { |
| id := p.ByName("PageKey") |
| page := GetPages()[id] |
| if page == nil { |
| rw.WriteHeader(http.StatusNotFound) |
| templates.MustRender(c, rw, "pages/error.html", templates.Args{ |
| "Error": "No such portal page", |
| }) |
| return |
| } |
| if err := cb(id, page); err != nil { |
| replyError(c, rw, err) |
| } |
| } |
| |
| func portalPageGET(ctx *router.Context) { |
| c, rw, p := ctx.Context, ctx.Writer, ctx.Params |
| |
| withPage(c, rw, p, func(id string, page Page) error { |
| title, err := page.Title(c) |
| if err != nil { |
| return err |
| } |
| overview, err := page.Overview(c) |
| if err != nil { |
| return err |
| } |
| fields, err := page.Fields(c) |
| if err != nil { |
| return err |
| } |
| actions, err := page.Actions(c) |
| if err != nil { |
| return err |
| } |
| values, err := page.ReadSettings(c) |
| if err != nil { |
| return err |
| } |
| |
| withValues := make([]fieldWithValue, len(fields)) |
| hasEditable := false |
| for i, f := range fields { |
| withValues[i] = fieldWithValue{ |
| Field: f, |
| Value: values[f.ID], |
| } |
| hasEditable = hasEditable || f.IsEditable() |
| } |
| |
| templates.MustRender(c, rw, "pages/page.html", templates.Args{ |
| "ID": id, |
| "Title": title, |
| "Overview": overview, |
| "Fields": withValues, |
| "Actions": actions, |
| "XsrfTokenField": xsrf.TokenField(c), |
| "ShowSaveButton": hasEditable, |
| }) |
| return nil |
| }) |
| } |
| |
| func portalPagePOST(ctx *router.Context) { |
| c, rw, r, p := ctx.Context, ctx.Writer, ctx.Request, ctx.Params |
| |
| withPage(c, rw, p, func(id string, page Page) error { |
| title, err := page.Title(c) |
| if err != nil { |
| return err |
| } |
| fields, err := page.Fields(c) |
| if err != nil { |
| return err |
| } |
| |
| // Extract values from the page and validate them. |
| values := make(map[string]string, len(fields)) |
| validationErrors := []validationError{} |
| for _, f := range fields { |
| if !f.IsEditable() { |
| continue |
| } |
| val := r.PostFormValue(f.ID) |
| values[f.ID] = val |
| if f.Validator != nil { |
| if err := f.Validator(val); err != nil { |
| validationErrors = append(validationErrors, validationError{ |
| FieldTitle: f.Title, |
| Value: val, |
| Error: err.Error(), |
| }) |
| } |
| } |
| } |
| if len(validationErrors) != 0 { |
| rw.WriteHeader(http.StatusBadRequest) |
| templates.MustRender(c, rw, "pages/validation_error.html", templates.Args{ |
| "ID": id, |
| "Title": title, |
| "Errors": validationErrors, |
| }) |
| return nil |
| } |
| |
| // Store. |
| err = page.WriteSettings(c, values, auth.CurrentUser(c).Email, "modified via web UI") |
| if err != nil { |
| return err |
| } |
| templates.MustRender(c, rw, "pages/done.html", templates.Args{ |
| "ID": id, |
| "Title": title, |
| }) |
| return nil |
| }) |
| } |
| |
| func portalActionGETPOST(ctx *router.Context) { |
| c, rw, p := ctx.Context, ctx.Writer, ctx.Params |
| actionID := p.ByName("ActionID") |
| |
| withPage(c, rw, p, func(id string, page Page) error { |
| title, err := page.Title(c) |
| if err != nil { |
| return err |
| } |
| actions, err := page.Actions(c) |
| if err != nil { |
| return err |
| } |
| |
| var action *Action |
| for i := range actions { |
| if actions[i].ID == actionID { |
| action = &actions[i] |
| break |
| } |
| } |
| if action == nil { |
| rw.WriteHeader(http.StatusNotFound) |
| templates.MustRender(c, rw, "pages/error.html", templates.Args{ |
| "Error": "No such action defined", |
| }) |
| return nil |
| } |
| |
| // Make sure side effect free actions are always executed through GET, and |
| // ones with side effects are through POST. This is important, since only |
| // POST route is protected with XSRF check. |
| expectedMethod := "POST" |
| if action.NoSideEffects { |
| expectedMethod = "GET" |
| } |
| if ctx.Request.Method != expectedMethod { |
| rw.WriteHeader(http.StatusBadRequest) |
| templates.MustRender(c, rw, "pages/error.html", templates.Args{ |
| "Error": fmt.Sprintf("Expecting HTTP method %s, but got %s.", expectedMethod, ctx.Request.Method), |
| }) |
| return nil |
| } |
| |
| resultTitle, result, err := action.Callback(c) |
| if err != nil { |
| rw.WriteHeader(http.StatusInternalServerError) |
| templates.MustRender(c, rw, "pages/error.html", templates.Args{ |
| "Error": err.Error(), |
| }) |
| return nil |
| } |
| |
| templates.MustRender(c, rw, "pages/action_done.html", templates.Args{ |
| "ID": id, |
| "Title": title, |
| "ResultTitle": resultTitle, |
| "Result": result, |
| }) |
| return nil |
| }) |
| } |