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.
 
 
 
 

406 lines
11 KiB

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