Skip to content

Instantly share code, notes, and snippets.

@miguelfermin
Last active January 24, 2018 11:20
Show Gist options
  • Save miguelfermin/1b8bcf4412479e44249382416299ed73 to your computer and use it in GitHub Desktop.
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/
// 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