Publish HTML quickly. https://html.house
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

360 lines
9.3 KiB

  1. package htmlhouse
  2. import (
  3. "bytes"
  4. "database/sql"
  5. "fmt"
  6. "io/ioutil"
  7. "net/http"
  8. "net/url"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/gorilla/mux"
  14. "github.com/writeas/impart"
  15. "github.com/writeas/nerds/store"
  16. "github.com/writeas/web-core/bots"
  17. )
  18. func createHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  19. html := r.FormValue("html")
  20. if strings.TrimSpace(html) == "" {
  21. return impart.HTTPError{http.StatusBadRequest, "Supply something to publish."}
  22. }
  23. public := r.FormValue("public") == "true"
  24. houseID := store.GenerateFriendlyRandomString(8)
  25. _, err := app.db.Exec("INSERT INTO houses (id, html) VALUES (?, ?)", houseID, html)
  26. if err != nil {
  27. return err
  28. }
  29. if err = app.session.writeToken(w, houseID); err != nil {
  30. return err
  31. }
  32. resUser := newSessionInfo(houseID)
  33. if public {
  34. go addPublicAccess(app, houseID, html)
  35. }
  36. return impart.WriteSuccess(w, resUser, http.StatusCreated)
  37. }
  38. func validTitle(title string) bool {
  39. return title != "" && strings.TrimSpace(title) != "HTMLhouse"
  40. }
  41. func removePublicAccess(app *app, houseID string) error {
  42. var approved sql.NullInt64
  43. err := app.db.QueryRow("SELECT approved FROM publichouses WHERE house_id = ?", houseID).Scan(&approved)
  44. switch {
  45. case err == sql.ErrNoRows:
  46. return nil
  47. case err != nil:
  48. fmt.Printf("Couldn't fetch for public removal: %v\n", err)
  49. return nil
  50. }
  51. if approved.Valid && approved.Int64 == 0 {
  52. // Page has been banned, so do nothing
  53. } else {
  54. _, err = app.db.Exec("DELETE FROM publichouses WHERE house_id = ?", houseID)
  55. if err != nil {
  56. return err
  57. }
  58. }
  59. return nil
  60. }
  61. func addPublicAccess(app *app, houseID, html string) error {
  62. // Parse title of page
  63. title := titleReg.FindStringSubmatch(html)[1]
  64. if !validTitle(title) {
  65. // <title/> was invalid, so look for an <h1/>
  66. header := headerReg.FindStringSubmatch(html)[1]
  67. if validTitle(header) {
  68. // <h1/> was valid, so use that instead of <title/>
  69. title = header
  70. }
  71. }
  72. title = strings.TrimSpace(title)
  73. // Get thumbnail
  74. data := url.Values{}
  75. data.Set("url", fmt.Sprintf("%s/%s.html", app.cfg.HostName, houseID))
  76. u, err := url.ParseRequestURI("https://peeper.html.house")
  77. u.Path = "/"
  78. urlStr := fmt.Sprintf("%v", u)
  79. client := &http.Client{}
  80. r, err := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode()))
  81. if err != nil {
  82. fmt.Printf("Error creating request: %v", err)
  83. }
  84. r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
  85. r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
  86. var thumbURL string
  87. resp, err := client.Do(r)
  88. if err != nil {
  89. fmt.Printf("Error requesting thumbnail: %v", err)
  90. return impart.HTTPError{http.StatusInternalServerError, "Couldn't generate thumbnail"}
  91. } else {
  92. defer resp.Body.Close()
  93. body, _ := ioutil.ReadAll(resp.Body)
  94. if resp.StatusCode == http.StatusOK {
  95. thumbURL = string(body)
  96. }
  97. }
  98. // Add to public houses table
  99. approved := sql.NullInt64{Valid: false}
  100. if app.cfg.AutoApprove {
  101. approved.Int64 = 1
  102. approved.Valid = true
  103. }
  104. _, err = app.db.Exec("INSERT INTO publichouses (house_id, title, thumb_url, added, updated, approved) VALUES (?, ?, ?, NOW(), NOW(), ?) ON DUPLICATE KEY UPDATE title = ?, updated = NOW()", houseID, title, thumbURL, approved, title)
  105. if err != nil {
  106. return err
  107. }
  108. // Tweet about it
  109. tweet(app, houseID, title)
  110. return nil
  111. }
  112. func renovateHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  113. vars := mux.Vars(r)
  114. houseID := vars["house"]
  115. html := r.FormValue("html")
  116. if strings.TrimSpace(html) == "" {
  117. return impart.HTTPError{http.StatusBadRequest, "Supply something to publish."}
  118. }
  119. public := r.FormValue("public") == "true"
  120. authHouseID, err := app.session.readToken(r)
  121. if err != nil {
  122. return err
  123. }
  124. if authHouseID != houseID {
  125. return impart.HTTPError{http.StatusUnauthorized, "Bad token for this ⌂ house ⌂."}
  126. }
  127. _, err = app.db.Exec("UPDATE houses SET html = ? WHERE id = ?", html, houseID)
  128. if err != nil {
  129. return err
  130. }
  131. if err = app.session.writeToken(w, houseID); err != nil {
  132. return err
  133. }
  134. resUser := newSessionInfo(houseID)
  135. if public {
  136. go addPublicAccess(app, houseID, html)
  137. } else {
  138. go removePublicAccess(app, houseID)
  139. }
  140. return impart.WriteSuccess(w, resUser, http.StatusOK)
  141. }
  142. func getHouseStats(app *app, houseID string) (*time.Time, int64, error) {
  143. var created time.Time
  144. var views int64
  145. err := app.db.QueryRow("SELECT created, view_count FROM houses WHERE id = ?", houseID).Scan(&created, &views)
  146. switch {
  147. case err == sql.ErrNoRows:
  148. return nil, 0, impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  149. case err != nil:
  150. fmt.Printf("Couldn't fetch: %v\n", err)
  151. return nil, 0, err
  152. }
  153. return &created, views, nil
  154. }
  155. func getHouseHTML(app *app, houseID string) (string, error) {
  156. var html string
  157. err := app.db.QueryRow("SELECT html FROM houses WHERE id = ?", houseID).Scan(&html)
  158. switch {
  159. case err == sql.ErrNoRows:
  160. return "", impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  161. case err != nil:
  162. fmt.Printf("Couldn't fetch: %v\n", err)
  163. return "", err
  164. }
  165. return html, nil
  166. }
  167. // regular expressions for extracting data
  168. var (
  169. htmlReg = regexp.MustCompile("<html( ?.*)>")
  170. titleReg = regexp.MustCompile("<title>(.+)</title>")
  171. headerReg = regexp.MustCompile("<h1>(.+)</h1>")
  172. )
  173. func getHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  174. vars := mux.Vars(r)
  175. houseID := vars["house"]
  176. // Fetch HTML
  177. html, err := getHouseHTML(app, houseID)
  178. if err != nil {
  179. if err, ok := err.(impart.HTTPError); ok {
  180. if err.Status == http.StatusNotFound {
  181. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  182. if err != nil {
  183. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  184. }
  185. fmt.Fprintf(w, "%s", page)
  186. return nil
  187. }
  188. }
  189. return err
  190. }
  191. // Add nofollow meta tag
  192. if strings.Index(html, "<head>") == -1 {
  193. html = htmlReg.ReplaceAllString(html, "<html$1><head></head>")
  194. }
  195. html = strings.Replace(html, "<head>", "<head><meta name=\"robots\" content=\"nofollow\" />", 1)
  196. // Add links back to HTMLhouse
  197. homeLink := "<a href='/'>&lt;&#8962;/&gt;</a>"
  198. watermark := fmt.Sprintf("<div style='position: absolute;top:16px;right:16px;'>%s &middot; <a href='/stats/%s.html'>stats</a> &middot; <a href='/edit/%s.html'>edit</a></div>", homeLink, houseID, houseID)
  199. if strings.Index(html, "</body>") == -1 {
  200. html = strings.Replace(html, "</html>", "</body></html>", 1)
  201. }
  202. html = strings.Replace(html, "</body>", fmt.Sprintf("%s</body>", watermark), 1)
  203. // Print HTML, with sanity check in case someone did something crazy
  204. if strings.Index(html, homeLink) == -1 {
  205. fmt.Fprintf(w, "%s%s", html, watermark)
  206. } else {
  207. fmt.Fprintf(w, "%s", html)
  208. }
  209. if r.Method != "HEAD" && !bots.IsBot(r.UserAgent()) {
  210. app.db.Exec("UPDATE houses SET view_count = view_count + 1 WHERE id = ?", houseID)
  211. }
  212. return nil
  213. }
  214. func viewHouseStats(app *app, w http.ResponseWriter, r *http.Request) error {
  215. vars := mux.Vars(r)
  216. houseID := vars["house"]
  217. created, views, err := getHouseStats(app, houseID)
  218. if err != nil {
  219. if err, ok := err.(impart.HTTPError); ok {
  220. if err.Status == http.StatusNotFound {
  221. // TODO: put this logic in one place (shared with getHouse func)
  222. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  223. if err != nil {
  224. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  225. }
  226. fmt.Fprintf(w, "%s", page)
  227. return nil
  228. }
  229. }
  230. return err
  231. }
  232. viewsLbl := "view"
  233. if views != 1 {
  234. viewsLbl = "views"
  235. }
  236. app.templates["stats"].ExecuteTemplate(w, "stats", &HouseStats{
  237. ID: houseID,
  238. Stats: []Stat{
  239. Stat{
  240. Data: fmt.Sprintf("%d", views),
  241. Label: viewsLbl,
  242. },
  243. Stat{
  244. Data: created.Format(time.RFC1123),
  245. Label: "created",
  246. },
  247. },
  248. })
  249. return nil
  250. }
  251. func viewHouses(app *app, w http.ResponseWriter, r *http.Request) error {
  252. houses, err := getPublicHouses(app)
  253. if err != nil {
  254. fmt.Fprintf(w, ":(")
  255. return err
  256. }
  257. app.templates["browse"].ExecuteTemplate(w, "browse", struct{ Houses *[]PublicHouse }{houses})
  258. return nil
  259. }
  260. func getPublicHouses(app *app) (*[]PublicHouse, error) {
  261. houses := []PublicHouse{}
  262. rows, err := app.db.Query(fmt.Sprintf("SELECT house_id, title, thumb_url FROM publichouses WHERE approved = 1 ORDER BY updated DESC LIMIT %d", app.cfg.BrowseItems))
  263. switch {
  264. case err == sql.ErrNoRows:
  265. return nil, impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  266. case err != nil:
  267. fmt.Printf("Couldn't fetch: %v\n", err)
  268. return nil, err
  269. }
  270. defer rows.Close()
  271. house := &PublicHouse{}
  272. for rows.Next() {
  273. err = rows.Scan(&house.ID, &house.Title, &house.ThumbURL)
  274. houses = append(houses, *house)
  275. }
  276. return &houses, nil
  277. }
  278. func isHousePublic(app *app, houseID string) bool {
  279. var dummy int64
  280. err := app.db.QueryRow("SELECT 1 FROM publichouses WHERE house_id = ?", houseID).Scan(&dummy)
  281. switch {
  282. case err == sql.ErrNoRows:
  283. return false
  284. case err != nil:
  285. fmt.Printf("Couldn't fetch: %v\n", err)
  286. return false
  287. }
  288. return true
  289. }
  290. func banHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  291. houseID := r.FormValue("house")
  292. pass := r.FormValue("pass")
  293. if app.cfg.AdminPass != pass {
  294. w.WriteHeader(http.StatusNotFound)
  295. return nil
  296. }
  297. _, err := app.db.Exec("UPDATE publichouses SET approved = 0 WHERE house_id = ?", houseID)
  298. if err != nil {
  299. fmt.Fprintf(w, "Couldn't ban house: %v", err)
  300. return err
  301. }
  302. fmt.Fprintf(w, "BOOM! %s banned.", houseID)
  303. return nil
  304. }