Publish HTML quickly. https://html.house
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

375 行
9.7 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. return nil
  109. }
  110. func renovateHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  111. vars := mux.Vars(r)
  112. houseID := vars["house"]
  113. html := r.FormValue("html")
  114. if strings.TrimSpace(html) == "" {
  115. return impart.HTTPError{http.StatusBadRequest, "Supply something to publish."}
  116. }
  117. public := r.FormValue("public") == "true"
  118. authHouseID, err := app.session.readToken(r)
  119. if err != nil {
  120. return err
  121. }
  122. if authHouseID != houseID {
  123. return impart.HTTPError{http.StatusUnauthorized, "Bad token for this ⌂ house ⌂."}
  124. }
  125. _, err = app.db.Exec("UPDATE houses SET html = ? WHERE id = ?", html, houseID)
  126. if err != nil {
  127. return err
  128. }
  129. if err = app.session.writeToken(w, houseID); err != nil {
  130. return err
  131. }
  132. resUser := newSessionInfo(houseID)
  133. if public {
  134. go addPublicAccess(app, houseID, html)
  135. } else {
  136. go removePublicAccess(app, houseID)
  137. }
  138. return impart.WriteSuccess(w, resUser, http.StatusOK)
  139. }
  140. func getHouseStats(app *app, houseID string) (*time.Time, int64, error) {
  141. var created time.Time
  142. var views int64
  143. err := app.db.QueryRow("SELECT created, view_count FROM houses WHERE id = ?", houseID).Scan(&created, &views)
  144. switch {
  145. case err == sql.ErrNoRows:
  146. return nil, 0, impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  147. case err != nil:
  148. fmt.Printf("Couldn't fetch: %v\n", err)
  149. return nil, 0, err
  150. }
  151. return &created, views, nil
  152. }
  153. func getHouseHTML(app *app, houseID string) (string, error) {
  154. var html string
  155. err := app.db.QueryRow("SELECT html FROM houses WHERE id = ?", houseID).Scan(&html)
  156. switch {
  157. case err == sql.ErrNoRows:
  158. return "", impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  159. case err != nil:
  160. fmt.Printf("Couldn't fetch: %v\n", err)
  161. return "", err
  162. }
  163. return html, nil
  164. }
  165. // regular expressions for extracting data
  166. var (
  167. htmlReg = regexp.MustCompile("<html( ?.*)>")
  168. titleReg = regexp.MustCompile("<title>(.+)</title>")
  169. headerReg = regexp.MustCompile("<h1>(.+)</h1>")
  170. )
  171. func getHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  172. vars := mux.Vars(r)
  173. houseID := vars["house"]
  174. // Fetch HTML
  175. html, err := getHouseHTML(app, houseID)
  176. if err != nil {
  177. if err, ok := err.(impart.HTTPError); ok {
  178. if err.Status == http.StatusNotFound {
  179. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  180. if err != nil {
  181. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  182. }
  183. fmt.Fprintf(w, "%s", page)
  184. return nil
  185. }
  186. }
  187. return err
  188. }
  189. // Add nofollow meta tag
  190. if strings.Index(html, "<head>") == -1 {
  191. html = htmlReg.ReplaceAllString(html, "<html$1><head></head>")
  192. }
  193. html = strings.Replace(html, "<head>", "<head><meta name=\"robots\" content=\"nofollow\" />", 1)
  194. // Add links back to HTMLhouse
  195. homeLink := "<a href='/'>&lt;&#8962;/&gt;</a>"
  196. 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)
  197. if strings.Index(html, "</body>") == -1 {
  198. html = strings.Replace(html, "</html>", "</body></html>", 1)
  199. }
  200. html = strings.Replace(html, "</body>", fmt.Sprintf("%s</body>", watermark), 1)
  201. // Print HTML, with sanity check in case someone did something crazy
  202. if strings.Index(html, homeLink) == -1 {
  203. fmt.Fprintf(w, "%s%s", html, watermark)
  204. } else {
  205. fmt.Fprintf(w, "%s", html)
  206. }
  207. if r.Method != "HEAD" && !bots.IsBot(r.UserAgent()) {
  208. app.db.Exec("UPDATE houses SET view_count = view_count + 1 WHERE id = ?", houseID)
  209. }
  210. return nil
  211. }
  212. func viewHouseStats(app *app, w http.ResponseWriter, r *http.Request) error {
  213. vars := mux.Vars(r)
  214. houseID := vars["house"]
  215. created, views, err := getHouseStats(app, houseID)
  216. if err != nil {
  217. if err, ok := err.(impart.HTTPError); ok {
  218. if err.Status == http.StatusNotFound {
  219. // TODO: put this logic in one place (shared with getHouse func)
  220. page, err := ioutil.ReadFile(app.cfg.StaticDir + "/404.html")
  221. if err != nil {
  222. page = []byte("<!DOCTYPE html><html><body>HTMLlot.</body></html>")
  223. }
  224. fmt.Fprintf(w, "%s", page)
  225. return nil
  226. }
  227. }
  228. return err
  229. }
  230. viewsLbl := "view"
  231. if views != 1 {
  232. viewsLbl = "views"
  233. }
  234. app.templates["stats"].ExecuteTemplate(w, "stats", &HouseStats{
  235. ID: houseID,
  236. Stats: []Stat{
  237. Stat{
  238. Data: fmt.Sprintf("%d", views),
  239. Label: viewsLbl,
  240. },
  241. Stat{
  242. Data: created.Format(time.RFC1123),
  243. Label: "created",
  244. },
  245. },
  246. })
  247. return nil
  248. }
  249. func viewHouses(app *app, w http.ResponseWriter, r *http.Request) error {
  250. houses, err := getPublicHouses(app)
  251. if err != nil {
  252. fmt.Fprintf(w, ":(")
  253. return err
  254. }
  255. app.templates["browse"].ExecuteTemplate(w, "browse", struct{ Houses *[]PublicHouse }{houses})
  256. return nil
  257. }
  258. func getPublicHouses(app *app) (*[]PublicHouse, error) {
  259. houses := []PublicHouse{}
  260. rows, err := app.db.Query("SELECT house_id, title, thumb_url FROM publichouses WHERE approved = 1 ORDER BY updated DESC LIMIT 10")
  261. switch {
  262. case err == sql.ErrNoRows:
  263. return nil, impart.HTTPError{http.StatusNotFound, "Return to sender. Address unknown."}
  264. case err != nil:
  265. fmt.Printf("Couldn't fetch: %v\n", err)
  266. return nil, err
  267. }
  268. defer rows.Close()
  269. house := &PublicHouse{}
  270. for rows.Next() {
  271. err = rows.Scan(&house.ID, &house.Title, &house.ThumbURL)
  272. houses = append(houses, *house)
  273. }
  274. return &houses, nil
  275. }
  276. func isHousePublic(app *app, houseID string) bool {
  277. var dummy int64
  278. err := app.db.QueryRow("SELECT 1 FROM publichouses WHERE house_id = ?", houseID).Scan(&dummy)
  279. switch {
  280. case err == sql.ErrNoRows:
  281. return false
  282. case err != nil:
  283. fmt.Printf("Couldn't fetch: %v\n", err)
  284. return false
  285. }
  286. return true
  287. }
  288. func banHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  289. houseID := r.FormValue("house")
  290. pass := r.FormValue("pass")
  291. if app.cfg.AdminPass != pass {
  292. w.WriteHeader(http.StatusNotFound)
  293. return nil
  294. }
  295. _, err := app.db.Exec("UPDATE publichouses SET approved = 0 WHERE house_id = ?", houseID)
  296. if err != nil {
  297. fmt.Fprintf(w, "Couldn't ban house: %v", err)
  298. return err
  299. }
  300. fmt.Fprintf(w, "BOOM! %s banned.", houseID)
  301. return nil
  302. }
  303. func unbanHouse(app *app, w http.ResponseWriter, r *http.Request) error {
  304. houseID := r.FormValue("house")
  305. pass := r.FormValue("pass")
  306. if app.cfg.AdminPass != pass {
  307. w.WriteHeader(http.StatusNotFound)
  308. return nil
  309. }
  310. _, err := app.db.Exec("UPDATE publichouses SET approved = 1 WHERE house_id = ?", houseID)
  311. if err != nil {
  312. fmt.Fprintf(w, "Couldn't ban house: %v", err)
  313. return err
  314. }
  315. fmt.Fprintf(w, "boom. Ban on %s reversed.", houseID)
  316. return nil
  317. }