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.
 
 
 
 

391 lines
10 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. func getPublicHousesData(app *app, w http.ResponseWriter, r *http.Request) error {
  168. houses, err := getPublicHouses(app)
  169. if err != nil {
  170. return err
  171. }
  172. for i := range *houses {
  173. (*houses)[i].process(app)
  174. }
  175. return impart.WriteSuccess(w, houses, http.StatusOK)
  176. }
  177. // regular expressions for extracting data
  178. var (
  179. htmlReg = regexp.MustCompile("<html( ?.*)>")
  180. titleReg = regexp.MustCompile("<title>(.+)</title>")
  181. headerReg = regexp.MustCompile("<h1>(.+)</h1>")
  182. )
  183. func getHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  184. vars := mux.Vars(r)
  185. houseID := vars["house"]
  186. // Fetch HTML
  187. html, err := getHouseHTML(app, houseID)
  188. if err != nil {
  189. if err, ok := err.(impart.HTTPError); ok {
  190. if err.Status == http.StatusNotFound {
  191. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  192. if err != nil {
  193. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  194. }
  195. fmt.Fprintf(w, "%s", page)
  196. return nil
  197. }
  198. }
  199. return err
  200. }
  201. // Add nofollow meta tag
  202. if strings.Index(html, "<head>") == -1 {
  203. html = htmlReg.ReplaceAllString(html, "<html$1><head></head>")
  204. }
  205. html = strings.Replace(html, "<head>", "<head><meta name=\"robots\" content=\"nofollow\" />", 1)
  206. // Add links back to HTMLhouse
  207. homeLink := "<a href='/'>&lt;&#8962;/&gt;</a>"
  208. 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)
  209. if strings.Index(html, "</body>") == -1 {
  210. html = strings.Replace(html, "</html>", "</body></html>", 1)
  211. }
  212. html = strings.Replace(html, "</body>", fmt.Sprintf("%s</body>", watermark), 1)
  213. // Print HTML, with sanity check in case someone did something crazy
  214. if strings.Index(html, homeLink) == -1 {
  215. fmt.Fprintf(w, "%s%s", html, watermark)
  216. } else {
  217. fmt.Fprintf(w, "%s", html)
  218. }
  219. if r.Method != "HEAD" && !bots.IsBot(r.UserAgent()) {
  220. app.db.Exec("UPDATE houses SET view_count = view_count + 1 WHERE id = ?", houseID)
  221. }
  222. return nil
  223. }
  224. func viewHouseStats(app *app, w http.ResponseWriter, r *http.Request) error {
  225. vars := mux.Vars(r)
  226. houseID := vars["house"]
  227. created, views, err := getHouseStats(app, houseID)
  228. if err != nil {
  229. if err, ok := err.(impart.HTTPError); ok {
  230. if err.Status == http.StatusNotFound {
  231. // TODO: put this logic in one place (shared with getHouse func)
  232. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  233. if err != nil {
  234. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  235. }
  236. fmt.Fprintf(w, "%s", page)
  237. return nil
  238. }
  239. }
  240. return err
  241. }
  242. viewsLbl := "view"
  243. if views != 1 {
  244. viewsLbl = "views"
  245. }
  246. app.templates["stats"].ExecuteTemplate(w, "stats", &HouseStats{
  247. ID: houseID,
  248. Stats: []Stat{
  249. Stat{
  250. Data: fmt.Sprintf("%d", views),
  251. Label: viewsLbl,
  252. },
  253. Stat{
  254. Data: created.Format(time.RFC1123),
  255. Label: "created",
  256. },
  257. },
  258. })
  259. return nil
  260. }
  261. func viewHouses(app *app, w http.ResponseWriter, r *http.Request) error {
  262. houses, err := getPublicHouses(app)
  263. if err != nil {
  264. fmt.Printf("Couln't load houses: %v", err)
  265. return err
  266. }
  267. app.templates["browse"].ExecuteTemplate(w, "browse", struct{ Houses *[]PublicHouse }{houses})
  268. return nil
  269. }
  270. func getPublicHouses(app *app) (*[]PublicHouse, error) {
  271. houses := []PublicHouse{}
  272. 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))
  273. switch {
  274. case err == sql.ErrNoRows:
  275. return nil, impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  276. case err != nil:
  277. fmt.Printf("Couldn't fetch: %v\n", err)
  278. return nil, err
  279. }
  280. defer rows.Close()
  281. house := &PublicHouse{}
  282. for rows.Next() {
  283. err = rows.Scan(&house.ID, &house.Title, &house.ThumbURL)
  284. houses = append(houses, *house)
  285. }
  286. return &houses, nil
  287. }
  288. func isHousePublic(app *app, houseID string) bool {
  289. var dummy int64
  290. err := app.db.QueryRow("SELECT 1 FROM publichouses WHERE house_id = ?", houseID).Scan(&dummy)
  291. switch {
  292. case err == sql.ErrNoRows:
  293. return false
  294. case err != nil:
  295. fmt.Printf("Couldn't fetch: %v\n", err)
  296. return false
  297. }
  298. return true
  299. }
  300. func banHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  301. houseID := r.FormValue("house")
  302. pass := r.FormValue("pass")
  303. if app.cfg.AdminPass != pass {
  304. w.WriteHeader(http.StatusNotFound)
  305. return nil
  306. }
  307. _, err := app.db.Exec("UPDATE publichouses SET approved = 0 WHERE house_id = ?", houseID)
  308. if err != nil {
  309. fmt.Fprintf(w, "Couldn't ban house: %v", err)
  310. return err
  311. }
  312. fmt.Fprintf(w, "BOOM! %s banned.", houseID)
  313. return nil
  314. }
  315. func unbanHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  316. houseID := r.FormValue("house")
  317. pass := r.FormValue("pass")
  318. if app.cfg.AdminPass != pass {
  319. w.WriteHeader(http.StatusNotFound)
  320. return nil
  321. }
  322. _, err := app.db.Exec("UPDATE publichouses SET approved = 1 WHERE house_id = ?", houseID)
  323. if err != nil {
  324. fmt.Fprintf(w, "Couldn't ban house: %v", err)
  325. return err
  326. }
  327. fmt.Fprintf(w, "boom. Ban on %s reversed.", houseID)
  328. return nil
  329. }