go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/portal/settings.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package portal 16 17 import ( 18 "context" 19 "fmt" 20 "net/http" 21 22 "github.com/julienschmidt/httprouter" 23 24 "go.chromium.org/luci/server/auth/xsrf" 25 "go.chromium.org/luci/server/router" 26 "go.chromium.org/luci/server/templates" 27 ) 28 29 type fieldWithValue struct { 30 Field 31 Value string 32 } 33 34 type validationError struct { 35 FieldTitle string 36 Value string 37 Error string 38 } 39 40 type pageCallback func(id string, p Page) error 41 42 func withPage(c context.Context, rw http.ResponseWriter, p httprouter.Params, cb pageCallback) { 43 id := p.ByName("PageKey") 44 page := GetPages()[id] 45 if page == nil { 46 rw.WriteHeader(http.StatusNotFound) 47 templates.MustRender(c, rw, "pages/error.html", templates.Args{ 48 "Error": "No such portal page", 49 }) 50 return 51 } 52 if err := cb(id, page); err != nil { 53 replyError(c, rw, err) 54 } 55 } 56 57 func portalPageGET(ctx *router.Context) { 58 c, rw, p := ctx.Request.Context(), ctx.Writer, ctx.Params 59 60 withPage(c, rw, p, func(id string, page Page) error { 61 title, err := page.Title(c) 62 if err != nil { 63 return err 64 } 65 overview, err := page.Overview(c) 66 if err != nil { 67 return err 68 } 69 fields, err := page.Fields(c) 70 if err != nil { 71 return err 72 } 73 actions, err := page.Actions(c) 74 if err != nil { 75 return err 76 } 77 values, err := page.ReadSettings(c) 78 if err != nil { 79 return err 80 } 81 82 withValues := make([]fieldWithValue, len(fields)) 83 hasEditable := false 84 for i, f := range fields { 85 withValues[i] = fieldWithValue{ 86 Field: f, 87 Value: values[f.ID], 88 } 89 hasEditable = hasEditable || f.IsEditable() 90 } 91 92 templates.MustRender(c, rw, "pages/page.html", templates.Args{ 93 "ID": id, 94 "Title": title, 95 "Overview": overview, 96 "Fields": withValues, 97 "Actions": actions, 98 "XsrfTokenField": xsrf.TokenField(c), 99 "ShowSaveButton": hasEditable, 100 }) 101 return nil 102 }) 103 } 104 105 func portalPagePOST(ctx *router.Context) { 106 c, rw, r, p := ctx.Request.Context(), ctx.Writer, ctx.Request, ctx.Params 107 108 withPage(c, rw, p, func(id string, page Page) error { 109 title, err := page.Title(c) 110 if err != nil { 111 return err 112 } 113 fields, err := page.Fields(c) 114 if err != nil { 115 return err 116 } 117 118 // Extract values from the page and validate them. 119 values := make(map[string]string, len(fields)) 120 validationErrors := []validationError{} 121 for _, f := range fields { 122 if !f.IsEditable() { 123 continue 124 } 125 val := r.PostFormValue(f.ID) 126 values[f.ID] = val 127 if f.Validator != nil { 128 if err := f.Validator(val); err != nil { 129 validationErrors = append(validationErrors, validationError{ 130 FieldTitle: f.Title, 131 Value: val, 132 Error: err.Error(), 133 }) 134 } 135 } 136 } 137 if len(validationErrors) != 0 { 138 rw.WriteHeader(http.StatusBadRequest) 139 templates.MustRender(c, rw, "pages/validation_error.html", templates.Args{ 140 "ID": id, 141 "Title": title, 142 "Errors": validationErrors, 143 }) 144 return nil 145 } 146 147 // Store. 148 err = page.WriteSettings(c, values) 149 if err != nil { 150 return err 151 } 152 templates.MustRender(c, rw, "pages/done.html", templates.Args{ 153 "ID": id, 154 "Title": title, 155 }) 156 return nil 157 }) 158 } 159 160 func portalActionGETPOST(ctx *router.Context) { 161 c, rw, p := ctx.Request.Context(), ctx.Writer, ctx.Params 162 actionID := p.ByName("ActionID") 163 164 withPage(c, rw, p, func(id string, page Page) error { 165 title, err := page.Title(c) 166 if err != nil { 167 return err 168 } 169 actions, err := page.Actions(c) 170 if err != nil { 171 return err 172 } 173 174 var action *Action 175 for i := range actions { 176 if actions[i].ID == actionID { 177 action = &actions[i] 178 break 179 } 180 } 181 if action == nil { 182 rw.WriteHeader(http.StatusNotFound) 183 templates.MustRender(c, rw, "pages/error.html", templates.Args{ 184 "Error": "No such action defined", 185 }) 186 return nil 187 } 188 189 // Make sure side effect free actions are always executed through GET, and 190 // ones with side effects are through POST. This is important, since only 191 // POST route is protected with XSRF check. 192 expectedMethod := "POST" 193 if action.NoSideEffects { 194 expectedMethod = "GET" 195 } 196 if ctx.Request.Method != expectedMethod { 197 rw.WriteHeader(http.StatusBadRequest) 198 templates.MustRender(c, rw, "pages/error.html", templates.Args{ 199 "Error": fmt.Sprintf("Expecting HTTP method %s, but got %s.", expectedMethod, ctx.Request.Method), 200 }) 201 return nil 202 } 203 204 resultTitle, result, err := action.Callback(c) 205 if err != nil { 206 rw.WriteHeader(http.StatusInternalServerError) 207 templates.MustRender(c, rw, "pages/error.html", templates.Args{ 208 "Error": err.Error(), 209 }) 210 return nil 211 } 212 213 templates.MustRender(c, rw, "pages/action_done.html", templates.Args{ 214 "ID": id, 215 "Title": title, 216 "ResultTitle": resultTitle, 217 "Result": result, 218 }) 219 return nil 220 }) 221 }