Last active
January 24, 2018 11:20
-
-
Save miguelfermin/1b8bcf4412479e44249382416299ed73 to your computer and use it in GitHub Desktop.
The "Writing Web Applications" exercise described here https://golang.org/doc/articles/wiki/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Writing Web Applications: | |
// | |
// - Creating a data structure with load and save methods | |
// - Using the net/http package to build web applications | |
// - Using the html/template package to process HTML templates | |
// - Using the regexp package to validate user input | |
// - Using closures | |
// | |
// - Link: https://golang.org/doc/articles/wiki/ | |
package main | |
import ( | |
_ "errors" | |
_ "fmt" | |
"html/template" | |
"io/ioutil" | |
"net/http" | |
"regexp" | |
) | |
// MARK: - HTML Templates | |
// The function template.Must is a convenience wrapper that panics when passed a non-nil error | |
// value, and otherwise returns the *Template unaltered. A panic is appropriate here; if the | |
// templates can't be loaded the only sensible thing to do is exit the program. | |
var templates = template.Must(template.ParseFiles("edit.html", "view.html")) | |
// As you may have observed, this program has a serious security flaw: a user can supply an arbitrary | |
// path to be read/written on the server. To mitigate this, we can write a function to validate the | |
// title with a regular expression. | |
// First, add "regexp" to the import list. Then we can create a global variable to store our validation | |
// expression: | |
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$") | |
// MARK: -Data Structures | |
// A wiki consists of a series of interconnected pages, each of which has a title and a body | |
type Page struct { | |
Title string | |
// The type []byte means "a byte slice. The Body element is a []byte rather than string | |
// because that is the type expected by the io libraries we will use, as you'll see below. | |
Body []byte | |
} | |
// This is a method that takes as its receiver p, a pointer to Page. | |
// It takes no parameters, and returns a value of type error. | |
func (p *Page) save() error { | |
// This method will save the Page's Body to a text file. For simplicity, | |
// we will use the Title as the file name. | |
// The octal integer literal 0600, passed as the third parameter to WriteFile, indicates | |
// that the file should be created with read-write permissions for the current user only. | |
filename := p.Title + ".txt" | |
return ioutil.WriteFile(filename, p.Body, 0600) | |
} | |
// MARK: - Functions | |
/// Constructs the file name from the title parameter, reads the file's contents into a new variable | |
/// body, and returns a pointer to a Page literal constructed with the proper title and body values. | |
func loadPage(title string) (*Page, error) { | |
filename := title + ".txt" | |
body, err := ioutil.ReadFile(filename) | |
// Callers of this function can check the second parameter; | |
// if it is nil then it has successfully loaded a Page. | |
if err != nil { | |
return nil, err | |
} | |
return &Page{Title: title, Body: body}, nil | |
} | |
// MARK: - Handlers | |
// Extracts the page title from r.URL.Path, the path component of the request URL. | |
// The Path is re-sliced with [len("/view/"):] to drop the leading "/view/" component of | |
// the request path. | |
// | |
// The function then loads the page data, formats the page with a string of simple HTML, and | |
// writes it to w, the http.ResponseWriter. | |
// | |
// func viewHandler(w http.ResponseWriter, r *http.Request) { | |
func viewHandler(w http.ResponseWriter, r *http.Request, title string) { | |
page, err := loadPage(title) | |
if err != nil { | |
http.Redirect(w, r, "/edit/"+title, http.StatusNotFound) | |
return | |
} | |
renderTemplate(w, "view", page) | |
} | |
// The function editHandler loads the page (or, if it doesn't exist, create an empty Page struct), | |
// and displays an HTML form. | |
// | |
// func editHandler(w http.ResponseWriter, r *http.Request) { | |
func editHandler(w http.ResponseWriter, r *http.Request, title string) { | |
page, err := loadPage(title) | |
if err != nil { | |
page = &Page{Title: title} | |
} | |
renderTemplate(w, "edit", page) | |
} | |
// handles the submission of forms located on the edit pages | |
// The page title (provided in the URL) and the form's only field, Body, are stored in a new Page | |
// | |
// func saveHandler(w http.ResponseWriter, r *http.Request) { | |
func saveHandler(w http.ResponseWriter, r *http.Request, title string) { | |
body := r.FormValue("body") | |
// The value returned by FormValue is of type string. | |
// We must convert that value to []byte before it will fit into the Page struct. | |
// We use []byte(body) to perform the conversion. | |
page := &Page{Title: title, Body: []byte(body)} | |
err := page.save() | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
return | |
} | |
http.Redirect(w, r, "/view/"+title, http.StatusNotFound) | |
} | |
// MARK: - Helpers | |
func renderTemplate(w http.ResponseWriter, tmpl string, page *Page) { | |
err := templates.ExecuteTemplate(w, tmpl+".html", page) | |
if err != nil { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | |
} | |
} | |
// wrapper function that takes a function of the above defined "XXhandlers" type, and returns a | |
// function of type http.HandlerFunc (suitable to be passed to the function http.HandleFunc) | |
// | |
// The returned function is called a closure because it encloses values defined outside of it. | |
// In this case, the variable fn (the single argument to makeHandler) is enclosed by the closure. | |
// The variable fn will be one of our save, edit, or view handlers. | |
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc { | |
return func(w http.ResponseWriter, r *http.Request) { | |
m := validPath.FindStringSubmatch(r.URL.Path) | |
if m == nil { | |
http.NotFound(w, r) | |
return | |
} | |
fn(w, r, m[2]) | |
} | |
} | |
// MARK: - Program | |
func main() { | |
http.HandleFunc("/view/", makeHandler(viewHandler)) | |
http.HandleFunc("/edit/", makeHandler(editHandler)) | |
http.HandleFunc("/save/", makeHandler(saveHandler)) | |
http.ListenAndServe(":8080", nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment