A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
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.
 
 
 
 
 

1058 lines
28 KiB

  1. /*
  2. * Copyright © 2018 A Bunch Tell LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package writefreely
  11. import (
  12. "database/sql"
  13. "encoding/json"
  14. "fmt"
  15. "html/template"
  16. "math"
  17. "net/http"
  18. "net/url"
  19. "regexp"
  20. "strconv"
  21. "strings"
  22. "unicode"
  23. "github.com/gorilla/mux"
  24. "github.com/writeas/impart"
  25. "github.com/writeas/web-core/activitystreams"
  26. "github.com/writeas/web-core/auth"
  27. "github.com/writeas/web-core/bots"
  28. "github.com/writeas/web-core/log"
  29. waposts "github.com/writeas/web-core/posts"
  30. "github.com/writeas/writefreely/author"
  31. "github.com/writeas/writefreely/page"
  32. )
  33. type (
  34. // TODO: add Direction to db
  35. // TODO: add Language to db
  36. Collection struct {
  37. ID int64 `datastore:"id" json:"-"`
  38. Alias string `datastore:"alias" schema:"alias" json:"alias"`
  39. Title string `datastore:"title" schema:"title" json:"title"`
  40. Description string `datastore:"description" schema:"description" json:"description"`
  41. Direction string `schema:"dir" json:"dir,omitempty"`
  42. Language string `schema:"lang" json:"lang,omitempty"`
  43. StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"`
  44. Script string `datastore:"script" schema:"script" json:"script,omitempty"`
  45. Public bool `datastore:"public" json:"public"`
  46. Visibility collVisibility `datastore:"private" json:"-"`
  47. Format string `datastore:"format" json:"format,omitempty"`
  48. Views int64 `json:"views"`
  49. OwnerID int64 `datastore:"owner_id" json:"-"`
  50. PublicOwner bool `datastore:"public_owner" json:"-"`
  51. URL string `json:"url,omitempty"`
  52. db *datastore
  53. }
  54. CollectionObj struct {
  55. Collection
  56. TotalPosts int `json:"total_posts"`
  57. Owner *User `json:"owner,omitempty"`
  58. Posts *[]PublicPost `json:"posts,omitempty"`
  59. }
  60. DisplayCollection struct {
  61. *CollectionObj
  62. Prefix string
  63. IsTopLevel bool
  64. CurrentPage int
  65. TotalPages int
  66. Format *CollectionFormat
  67. }
  68. SubmittedCollection struct {
  69. // Data used for updating a given collection
  70. ID int64
  71. OwnerID uint64
  72. // Form helpers
  73. PreferURL string `schema:"prefer_url" json:"prefer_url"`
  74. Privacy int `schema:"privacy" json:"privacy"`
  75. Pass string `schema:"password" json:"password"`
  76. MathJax bool `schema:"mathjax" json:"mathjax"`
  77. Handle string `schema:"handle" json:"handle"`
  78. // Actual collection values updated in the DB
  79. Alias *string `schema:"alias" json:"alias"`
  80. Title *string `schema:"title" json:"title"`
  81. Description *string `schema:"description" json:"description"`
  82. StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"`
  83. Script *sql.NullString `schema:"script" json:"script"`
  84. Visibility *int `schema:"visibility" json:"public"`
  85. Format *sql.NullString `schema:"format" json:"format"`
  86. }
  87. CollectionFormat struct {
  88. Format string
  89. }
  90. collectionReq struct {
  91. // Information about the collection request itself
  92. prefix, alias, domain string
  93. isCustomDomain bool
  94. // User-related fields
  95. isCollOwner bool
  96. }
  97. )
  98. func (sc *SubmittedCollection) FediverseHandle() string {
  99. if sc.Handle == "" {
  100. return apCustomHandleDefault
  101. }
  102. return getSlug(sc.Handle, "")
  103. }
  104. // collVisibility represents the visibility level for the collection.
  105. type collVisibility int
  106. // Visibility levels. Values are bitmasks, stored in the database as
  107. // decimal numbers. If adding types, append them to this list. If removing,
  108. // replace the desired visibility with a new value.
  109. const CollUnlisted collVisibility = 0
  110. const (
  111. CollPublic collVisibility = 1 << iota
  112. CollPrivate
  113. CollProtected
  114. )
  115. func (cf *CollectionFormat) Ascending() bool {
  116. return cf.Format == "novel"
  117. }
  118. func (cf *CollectionFormat) ShowDates() bool {
  119. return cf.Format == "blog"
  120. }
  121. func (cf *CollectionFormat) PostsPerPage() int {
  122. if cf.Format == "novel" {
  123. return postsPerPage
  124. }
  125. return postsPerPage
  126. }
  127. // Valid returns whether or not a format value is valid.
  128. func (cf *CollectionFormat) Valid() bool {
  129. return cf.Format == "blog" ||
  130. cf.Format == "novel" ||
  131. cf.Format == "notebook"
  132. }
  133. // NewFormat creates a new CollectionFormat object from the Collection.
  134. func (c *Collection) NewFormat() *CollectionFormat {
  135. cf := &CollectionFormat{Format: c.Format}
  136. // Fill in default format
  137. if cf.Format == "" {
  138. cf.Format = "blog"
  139. }
  140. return cf
  141. }
  142. func (c *Collection) IsUnlisted() bool {
  143. return c.Visibility == 0
  144. }
  145. func (c *Collection) IsPrivate() bool {
  146. return c.Visibility&CollPrivate != 0
  147. }
  148. func (c *Collection) IsProtected() bool {
  149. return c.Visibility&CollProtected != 0
  150. }
  151. func (c *Collection) IsPublic() bool {
  152. return c.Visibility&CollPublic != 0
  153. }
  154. func (c *Collection) FriendlyVisibility() string {
  155. if c.IsPrivate() {
  156. return "Private"
  157. }
  158. if c.IsPublic() {
  159. return "Public"
  160. }
  161. if c.IsProtected() {
  162. return "Password-protected"
  163. }
  164. return "Unlisted"
  165. }
  166. func (c *Collection) ShowFooterBranding() bool {
  167. // TODO: implement this setting
  168. return true
  169. }
  170. // CanonicalURL returns a fully-qualified URL to the collection.
  171. func (c *Collection) CanonicalURL() string {
  172. return c.RedirectingCanonicalURL(false)
  173. }
  174. func (c *Collection) DisplayCanonicalURL() string {
  175. us := c.CanonicalURL()
  176. u, err := url.Parse(us)
  177. if err != nil {
  178. return us
  179. }
  180. p := u.Path
  181. if p == "/" {
  182. p = ""
  183. }
  184. return u.Hostname() + p
  185. }
  186. func (c *Collection) RedirectingCanonicalURL(isRedir bool) string {
  187. if isSingleUser {
  188. return hostName + "/"
  189. }
  190. return fmt.Sprintf("%s/%s/", hostName, c.Alias)
  191. }
  192. // PrevPageURL provides a full URL for the previous page of collection posts,
  193. // returning a /page/N result for pages >1
  194. func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string {
  195. u := ""
  196. if n == 2 {
  197. // Previous page is 1; no need for /page/ prefix
  198. if prefix == "" {
  199. u = "/"
  200. }
  201. // Else leave off trailing slash
  202. } else {
  203. u = fmt.Sprintf("/page/%d", n-1)
  204. }
  205. if tl {
  206. return u
  207. }
  208. return "/" + prefix + c.Alias + u
  209. }
  210. // NextPageURL provides a full URL for the next page of collection posts
  211. func (c *Collection) NextPageURL(prefix string, n int, tl bool) string {
  212. if tl {
  213. return fmt.Sprintf("/page/%d", n+1)
  214. }
  215. return fmt.Sprintf("/%s%s/page/%d", prefix, c.Alias, n+1)
  216. }
  217. func (c *Collection) DisplayTitle() string {
  218. if c.Title != "" {
  219. return c.Title
  220. }
  221. return c.Alias
  222. }
  223. func (c *Collection) StyleSheetDisplay() template.CSS {
  224. return template.CSS(c.StyleSheet)
  225. }
  226. // ForPublic modifies the Collection for public consumption, such as via
  227. // the API.
  228. func (c *Collection) ForPublic() {
  229. c.URL = c.CanonicalURL()
  230. }
  231. var isLowerLetter = regexp.MustCompile("[a-z]").MatchString
  232. func (c *Collection) PersonObject(ids ...int64) *activitystreams.Person {
  233. accountRoot := c.FederatedAccount()
  234. p := activitystreams.NewPerson(accountRoot)
  235. p.URL = c.CanonicalURL()
  236. uname := c.Alias
  237. p.PreferredUsername = uname
  238. p.Name = c.DisplayTitle()
  239. p.Summary = c.Description
  240. if p.Name != "" {
  241. if av := c.AvatarURL(); av != "" {
  242. p.Icon = activitystreams.Image{
  243. Type: "Image",
  244. MediaType: "image/png",
  245. URL: av,
  246. }
  247. }
  248. }
  249. collID := c.ID
  250. if len(ids) > 0 {
  251. collID = ids[0]
  252. }
  253. pub, priv := c.db.GetAPActorKeys(collID)
  254. if pub != nil {
  255. p.AddPubKey(pub)
  256. p.SetPrivKey(priv)
  257. }
  258. return p
  259. }
  260. func (c *Collection) AvatarURL() string {
  261. fl := string(unicode.ToLower([]rune(c.DisplayTitle())[0]))
  262. if !isLowerLetter(fl) {
  263. return ""
  264. }
  265. return hostName + "/img/avatars/" + fl + ".png"
  266. }
  267. func (c *Collection) FederatedAPIBase() string {
  268. return hostName + "/"
  269. }
  270. func (c *Collection) FederatedAccount() string {
  271. accountUser := c.Alias
  272. return c.FederatedAPIBase() + "api/collections/" + accountUser
  273. }
  274. func (c *Collection) RenderMathJax() bool {
  275. return c.db.CollectionHasAttribute(c.ID, "render_mathjax")
  276. }
  277. func newCollection(app *app, w http.ResponseWriter, r *http.Request) error {
  278. reqJSON := IsJSON(r.Header.Get("Content-Type"))
  279. alias := r.FormValue("alias")
  280. title := r.FormValue("title")
  281. var missingParams, accessToken string
  282. var u *User
  283. c := struct {
  284. Alias string `json:"alias" schema:"alias"`
  285. Title string `json:"title" schema:"title"`
  286. Web bool `json:"web" schema:"web"`
  287. }{}
  288. if reqJSON {
  289. // Decode JSON request
  290. decoder := json.NewDecoder(r.Body)
  291. err := decoder.Decode(&c)
  292. if err != nil {
  293. log.Error("Couldn't parse post update JSON request: %v\n", err)
  294. return ErrBadJSON
  295. }
  296. } else {
  297. // TODO: move form parsing to formDecoder
  298. c.Alias = alias
  299. c.Title = title
  300. }
  301. if c.Alias == "" {
  302. if c.Title != "" {
  303. // If only a title was given, just use it to generate the alias.
  304. c.Alias = getSlug(c.Title, "")
  305. } else {
  306. missingParams += "`alias` "
  307. }
  308. }
  309. if c.Title == "" {
  310. missingParams += "`title` "
  311. }
  312. if missingParams != "" {
  313. return impart.HTTPError{http.StatusBadRequest, fmt.Sprintf("Parameter(s) %srequired.", missingParams)}
  314. }
  315. if reqJSON && !c.Web {
  316. accessToken = r.Header.Get("Authorization")
  317. if accessToken == "" {
  318. return ErrNoAccessToken
  319. }
  320. } else {
  321. u = getUserSession(app, r)
  322. if u == nil {
  323. return ErrNotLoggedIn
  324. }
  325. }
  326. if !author.IsValidUsername(app.cfg, c.Alias) {
  327. return impart.HTTPError{http.StatusPreconditionFailed, "Collection alias isn't valid."}
  328. }
  329. var coll *Collection
  330. var err error
  331. if accessToken != "" {
  332. coll, err = app.db.CreateCollectionFromToken(c.Alias, c.Title, accessToken)
  333. if err != nil {
  334. // TODO: handle this
  335. return err
  336. }
  337. } else {
  338. coll, err = app.db.CreateCollection(c.Alias, c.Title, u.ID)
  339. if err != nil {
  340. // TODO: handle this
  341. return err
  342. }
  343. }
  344. res := &CollectionObj{Collection: *coll}
  345. if reqJSON {
  346. return impart.WriteSuccess(w, res, http.StatusCreated)
  347. }
  348. redirectTo := "/me/c/"
  349. // TODO: redirect to pad when necessary
  350. return impart.HTTPError{http.StatusFound, redirectTo}
  351. }
  352. func apiCheckCollectionPermissions(app *app, r *http.Request, c *Collection) (int64, error) {
  353. accessToken := r.Header.Get("Authorization")
  354. var userID int64 = -1
  355. if accessToken != "" {
  356. userID = app.db.GetUserID(accessToken)
  357. }
  358. isCollOwner := userID == c.OwnerID
  359. if c.IsPrivate() && !isCollOwner {
  360. // Collection is private, but user isn't authenticated
  361. return -1, ErrCollectionNotFound
  362. }
  363. if c.IsProtected() {
  364. // TODO: check access token
  365. return -1, ErrCollectionUnauthorizedRead
  366. }
  367. return userID, nil
  368. }
  369. // fetchCollection handles the API endpoint for retrieving collection data.
  370. func fetchCollection(app *app, w http.ResponseWriter, r *http.Request) error {
  371. accept := r.Header.Get("Accept")
  372. if strings.Contains(accept, "application/activity+json") {
  373. return handleFetchCollectionActivities(app, w, r)
  374. }
  375. vars := mux.Vars(r)
  376. alias := vars["alias"]
  377. // TODO: move this logic into a common getCollection function
  378. // Get base Collection data
  379. c, err := app.db.GetCollection(alias)
  380. if err != nil {
  381. return err
  382. }
  383. // Redirect users who aren't requesting JSON
  384. reqJSON := IsJSON(r.Header.Get("Content-Type"))
  385. if !reqJSON {
  386. return impart.HTTPError{http.StatusFound, c.CanonicalURL()}
  387. }
  388. // Check permissions
  389. userID, err := apiCheckCollectionPermissions(app, r, c)
  390. if err != nil {
  391. return err
  392. }
  393. isCollOwner := userID == c.OwnerID
  394. // Fetch extra data about the Collection
  395. res := &CollectionObj{Collection: *c}
  396. if c.PublicOwner {
  397. u, err := app.db.GetUserByID(res.OwnerID)
  398. if err != nil {
  399. // Log the error and just continue
  400. log.Error("Error getting user for collection: %v", err)
  401. } else {
  402. res.Owner = u
  403. }
  404. }
  405. app.db.GetPostsCount(res, isCollOwner)
  406. // Strip non-public information
  407. res.Collection.ForPublic()
  408. return impart.WriteSuccess(w, res, http.StatusOK)
  409. }
  410. // fetchCollectionPosts handles an API endpoint for retrieving a collection's
  411. // posts.
  412. func fetchCollectionPosts(app *app, w http.ResponseWriter, r *http.Request) error {
  413. vars := mux.Vars(r)
  414. alias := vars["alias"]
  415. c, err := app.db.GetCollection(alias)
  416. if err != nil {
  417. return err
  418. }
  419. // Check permissions
  420. userID, err := apiCheckCollectionPermissions(app, r, c)
  421. if err != nil {
  422. return err
  423. }
  424. isCollOwner := userID == c.OwnerID
  425. // Get page
  426. page := 1
  427. if p := r.FormValue("page"); p != "" {
  428. pInt, _ := strconv.Atoi(p)
  429. if pInt > 0 {
  430. page = pInt
  431. }
  432. }
  433. posts, err := app.db.GetPosts(c, page, isCollOwner, false)
  434. if err != nil {
  435. return err
  436. }
  437. coll := &CollectionObj{Collection: *c, Posts: posts}
  438. app.db.GetPostsCount(coll, isCollOwner)
  439. // Strip non-public information
  440. coll.Collection.ForPublic()
  441. // Transform post bodies if needed
  442. if r.FormValue("body") == "html" {
  443. for _, p := range *coll.Posts {
  444. p.Content = waposts.ApplyMarkdown([]byte(p.Content))
  445. }
  446. }
  447. return impart.WriteSuccess(w, coll, http.StatusOK)
  448. }
  449. type CollectionPage struct {
  450. page.StaticPage
  451. *DisplayCollection
  452. IsCustomDomain bool
  453. IsWelcome bool
  454. IsOwner bool
  455. CanPin bool
  456. Username string
  457. Collections *[]Collection
  458. PinnedPosts *[]PublicPost
  459. }
  460. func (c *CollectionObj) ScriptDisplay() template.JS {
  461. return template.JS(c.Script)
  462. }
  463. var jsSourceCommentReg = regexp.MustCompile("(?m)^// src:(.+)$")
  464. func (c *CollectionObj) ExternalScripts() []template.URL {
  465. scripts := []template.URL{}
  466. if c.Script == "" {
  467. return scripts
  468. }
  469. matches := jsSourceCommentReg.FindAllStringSubmatch(c.Script, -1)
  470. for _, m := range matches {
  471. scripts = append(scripts, template.URL(strings.TrimSpace(m[1])))
  472. }
  473. return scripts
  474. }
  475. func (c *CollectionObj) CanShowScript() bool {
  476. return false
  477. }
  478. func processCollectionRequest(cr *collectionReq, vars map[string]string, w http.ResponseWriter, r *http.Request) error {
  479. cr.prefix = vars["prefix"]
  480. cr.alias = vars["collection"]
  481. // Normalize the URL, redirecting user to consistent post URL
  482. if cr.alias != strings.ToLower(cr.alias) {
  483. return impart.HTTPError{http.StatusMovedPermanently, fmt.Sprintf("/%s/", strings.ToLower(cr.alias))}
  484. }
  485. return nil
  486. }
  487. // processCollectionPermissions checks the permissions for the given
  488. // collectionReq, returning a Collection if access is granted; otherwise this
  489. // renders any necessary collection pages, for example, if requesting a custom
  490. // domain that doesn't yet have a collection associated, or if a collection
  491. // requires a password. In either case, this will return nil, nil -- thus both
  492. // values should ALWAYS be checked to determine whether or not to continue.
  493. func processCollectionPermissions(app *app, cr *collectionReq, u *User, w http.ResponseWriter, r *http.Request) (*Collection, error) {
  494. // Display collection if this is a collection
  495. var c *Collection
  496. var err error
  497. if app.cfg.App.SingleUser {
  498. c, err = app.db.GetCollectionByID(1)
  499. } else {
  500. c, err = app.db.GetCollection(cr.alias)
  501. }
  502. // TODO: verify we don't reveal the existence of a private collection with redirection
  503. if err != nil {
  504. if err, ok := err.(impart.HTTPError); ok {
  505. if err.Status == http.StatusNotFound {
  506. if cr.isCustomDomain {
  507. // User is on the site from a custom domain
  508. //tErr := pages["404-domain.tmpl"].ExecuteTemplate(w, "base", pageForHost(page.StaticPage{}, r))
  509. //if tErr != nil {
  510. //log.Error("Unable to render 404-domain page: %v", err)
  511. //}
  512. return nil, nil
  513. }
  514. if len(cr.alias) >= minIDLen && len(cr.alias) <= maxIDLen {
  515. // Alias is within post ID range, so just be sure this isn't a post
  516. if app.db.PostIDExists(cr.alias) {
  517. // TODO: use StatusFound for vanity post URLs when we implement them
  518. return nil, impart.HTTPError{http.StatusMovedPermanently, "/" + cr.alias}
  519. }
  520. }
  521. // Redirect if necessary
  522. newAlias := app.db.GetCollectionRedirect(cr.alias)
  523. if newAlias != "" {
  524. return nil, impart.HTTPError{http.StatusFound, "/" + newAlias + "/"}
  525. }
  526. }
  527. }
  528. return nil, err
  529. }
  530. // Update CollectionRequest to reflect owner status
  531. cr.isCollOwner = u != nil && u.ID == c.OwnerID
  532. // Check permissions
  533. if !cr.isCollOwner {
  534. if c.IsPrivate() {
  535. return nil, ErrCollectionNotFound
  536. } else if c.IsProtected() {
  537. uname := ""
  538. if u != nil {
  539. uname = u.Username
  540. }
  541. // See if we've authorized this collection
  542. authd := isAuthorizedForCollection(app, c.Alias, r)
  543. if !authd {
  544. p := struct {
  545. page.StaticPage
  546. *CollectionObj
  547. Username string
  548. Next string
  549. Flashes []template.HTML
  550. }{
  551. StaticPage: pageForReq(app, r),
  552. CollectionObj: &CollectionObj{Collection: *c},
  553. Username: uname,
  554. Next: r.FormValue("g"),
  555. Flashes: []template.HTML{},
  556. }
  557. // Get owner information
  558. p.CollectionObj.Owner, err = app.db.GetUserByID(c.OwnerID)
  559. if err != nil {
  560. // Log the error and just continue
  561. log.Error("Error getting user for collection: %v", err)
  562. }
  563. flashes, _ := getSessionFlashes(app, w, r, nil)
  564. for _, flash := range flashes {
  565. p.Flashes = append(p.Flashes, template.HTML(flash))
  566. }
  567. err = templates["password-collection"].ExecuteTemplate(w, "password-collection", p)
  568. if err != nil {
  569. log.Error("Unable to render password-collection: %v", err)
  570. return nil, err
  571. }
  572. return nil, nil
  573. }
  574. }
  575. }
  576. return c, nil
  577. }
  578. func checkUserForCollection(app *app, cr *collectionReq, r *http.Request, isPostReq bool) (*User, error) {
  579. u := getUserSession(app, r)
  580. return u, nil
  581. }
  582. func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCollection {
  583. coll := &DisplayCollection{
  584. CollectionObj: &CollectionObj{Collection: *c},
  585. CurrentPage: page,
  586. Prefix: cr.prefix,
  587. IsTopLevel: isSingleUser,
  588. Format: c.NewFormat(),
  589. }
  590. c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner)
  591. return coll
  592. }
  593. func getCollectionPage(vars map[string]string) int {
  594. page := 1
  595. var p int
  596. p, _ = strconv.Atoi(vars["page"])
  597. if p > 0 {
  598. page = p
  599. }
  600. return page
  601. }
  602. // handleViewCollection displays the requested Collection
  603. func handleViewCollection(app *app, w http.ResponseWriter, r *http.Request) error {
  604. vars := mux.Vars(r)
  605. cr := &collectionReq{}
  606. err := processCollectionRequest(cr, vars, w, r)
  607. if err != nil {
  608. return err
  609. }
  610. u, err := checkUserForCollection(app, cr, r, false)
  611. if err != nil {
  612. return err
  613. }
  614. page := getCollectionPage(vars)
  615. c, err := processCollectionPermissions(app, cr, u, w, r)
  616. if c == nil || err != nil {
  617. return err
  618. }
  619. // Serve ActivityStreams data now, if requested
  620. if strings.Contains(r.Header.Get("Accept"), "application/activity+json") {
  621. ac := c.PersonObject()
  622. ac.Context = []interface{}{activitystreams.Namespace}
  623. return impart.RenderActivityJSON(w, ac, http.StatusOK)
  624. }
  625. // Fetch extra data about the Collection
  626. // TODO: refactor out this logic, shared in collection.go:fetchCollection()
  627. coll := newDisplayCollection(c, cr, page)
  628. coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(coll.Format.PostsPerPage())))
  629. if coll.TotalPages > 0 && page > coll.TotalPages {
  630. redirURL := fmt.Sprintf("/page/%d", coll.TotalPages)
  631. if !app.cfg.App.SingleUser {
  632. redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL)
  633. }
  634. return impart.HTTPError{http.StatusFound, redirURL}
  635. }
  636. coll.Posts, _ = app.db.GetPosts(c, page, cr.isCollOwner, false)
  637. // Serve collection
  638. displayPage := CollectionPage{
  639. DisplayCollection: coll,
  640. StaticPage: pageForReq(app, r),
  641. IsCustomDomain: cr.isCustomDomain,
  642. IsWelcome: r.FormValue("greeting") != "",
  643. }
  644. var owner *User
  645. if u != nil {
  646. displayPage.Username = u.Username
  647. displayPage.IsOwner = u.ID == coll.OwnerID
  648. if displayPage.IsOwner {
  649. // Add in needed information for users viewing their own collection
  650. owner = u
  651. displayPage.CanPin = true
  652. pubColls, err := app.db.GetPublishableCollections(owner)
  653. if err != nil {
  654. log.Error("unable to fetch collections: %v", err)
  655. }
  656. displayPage.Collections = pubColls
  657. }
  658. }
  659. if owner == nil {
  660. // Current user doesn't own collection; retrieve owner information
  661. owner, err = app.db.GetUserByID(coll.OwnerID)
  662. if err != nil {
  663. // Log the error and just continue
  664. log.Error("Error getting user for collection: %v", err)
  665. }
  666. }
  667. displayPage.Owner = owner
  668. coll.Owner = displayPage.Owner
  669. // Add more data
  670. // TODO: fix this mess of collections inside collections
  671. displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
  672. err = templates["collection"].ExecuteTemplate(w, "collection", displayPage)
  673. if err != nil {
  674. log.Error("Unable to render collection index: %v", err)
  675. }
  676. // Update collection view count
  677. go func() {
  678. // Don't update if owner is viewing the collection.
  679. if u != nil && u.ID == coll.OwnerID {
  680. return
  681. }
  682. // Only update for human views
  683. if r.Method == "HEAD" || bots.IsBot(r.UserAgent()) {
  684. return
  685. }
  686. _, err := app.db.Exec("UPDATE collections SET view_count = view_count + 1 WHERE id = ?", coll.ID)
  687. if err != nil {
  688. log.Error("Unable to update collections count: %v", err)
  689. }
  690. }()
  691. return err
  692. }
  693. func handleViewCollectionTag(app *app, w http.ResponseWriter, r *http.Request) error {
  694. vars := mux.Vars(r)
  695. tag := vars["tag"]
  696. cr := &collectionReq{}
  697. err := processCollectionRequest(cr, vars, w, r)
  698. if err != nil {
  699. return err
  700. }
  701. u, err := checkUserForCollection(app, cr, r, false)
  702. if err != nil {
  703. return err
  704. }
  705. page := getCollectionPage(vars)
  706. c, err := processCollectionPermissions(app, cr, u, w, r)
  707. if c == nil || err != nil {
  708. return err
  709. }
  710. coll := newDisplayCollection(c, cr, page)
  711. coll.Posts, _ = app.db.GetPostsTagged(c, tag, page, cr.isCollOwner)
  712. if coll.Posts != nil && len(*coll.Posts) == 0 {
  713. return ErrCollectionPageNotFound
  714. }
  715. // Serve collection
  716. displayPage := struct {
  717. CollectionPage
  718. Tag string
  719. }{
  720. CollectionPage: CollectionPage{
  721. DisplayCollection: coll,
  722. StaticPage: pageForReq(app, r),
  723. IsCustomDomain: cr.isCustomDomain,
  724. },
  725. Tag: tag,
  726. }
  727. var owner *User
  728. if u != nil {
  729. displayPage.Username = u.Username
  730. displayPage.IsOwner = u.ID == coll.OwnerID
  731. if displayPage.IsOwner {
  732. // Add in needed information for users viewing their own collection
  733. owner = u
  734. displayPage.CanPin = true
  735. pubColls, err := app.db.GetPublishableCollections(owner)
  736. if err != nil {
  737. log.Error("unable to fetch collections: %v", err)
  738. }
  739. displayPage.Collections = pubColls
  740. }
  741. }
  742. if owner == nil {
  743. // Current user doesn't own collection; retrieve owner information
  744. owner, err = app.db.GetUserByID(coll.OwnerID)
  745. if err != nil {
  746. // Log the error and just continue
  747. log.Error("Error getting user for collection: %v", err)
  748. }
  749. }
  750. displayPage.Owner = owner
  751. coll.Owner = displayPage.Owner
  752. // Add more data
  753. // TODO: fix this mess of collections inside collections
  754. displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj)
  755. err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage)
  756. if err != nil {
  757. log.Error("Unable to render collection tag page: %v", err)
  758. }
  759. return nil
  760. }
  761. func handleCollectionPostRedirect(app *app, w http.ResponseWriter, r *http.Request) error {
  762. vars := mux.Vars(r)
  763. slug := vars["slug"]
  764. cr := &collectionReq{}
  765. err := processCollectionRequest(cr, vars, w, r)
  766. if err != nil {
  767. return err
  768. }
  769. // Normalize the URL, redirecting user to consistent post URL
  770. loc := fmt.Sprintf("/%s", slug)
  771. if !app.cfg.App.SingleUser {
  772. loc = fmt.Sprintf("/%s/%s", cr.alias, slug)
  773. }
  774. return impart.HTTPError{http.StatusFound, loc}
  775. }
  776. func existingCollection(app *app, w http.ResponseWriter, r *http.Request) error {
  777. reqJSON := IsJSON(r.Header.Get("Content-Type"))
  778. vars := mux.Vars(r)
  779. collAlias := vars["alias"]
  780. isWeb := r.FormValue("web") == "1"
  781. var u *User
  782. if reqJSON && !isWeb {
  783. // Ensure an access token was given
  784. accessToken := r.Header.Get("Authorization")
  785. u = &User{}
  786. u.ID = app.db.GetUserID(accessToken)
  787. if u.ID == -1 {
  788. return ErrBadAccessToken
  789. }
  790. } else {
  791. u = getUserSession(app, r)
  792. if u == nil {
  793. return ErrNotLoggedIn
  794. }
  795. }
  796. if r.Method == "DELETE" {
  797. err := app.db.DeleteCollection(collAlias, u.ID)
  798. if err != nil {
  799. // TODO: if not HTTPError, report error to admin
  800. log.Error("Unable to delete collection: %s", err)
  801. return err
  802. }
  803. addSessionFlash(app, w, r, "Deleted your blog, "+collAlias+".", nil)
  804. return impart.HTTPError{Status: http.StatusNoContent}
  805. }
  806. c := SubmittedCollection{OwnerID: uint64(u.ID)}
  807. var err error
  808. if reqJSON {
  809. // Decode JSON request
  810. decoder := json.NewDecoder(r.Body)
  811. err = decoder.Decode(&c)
  812. if err != nil {
  813. log.Error("Couldn't parse collection update JSON request: %v\n", err)
  814. return ErrBadJSON
  815. }
  816. } else {
  817. err = r.ParseForm()
  818. if err != nil {
  819. log.Error("Couldn't parse collection update form request: %v\n", err)
  820. return ErrBadFormData
  821. }
  822. err = app.formDecoder.Decode(&c, r.PostForm)
  823. if err != nil {
  824. log.Error("Couldn't decode collection update form request: %v\n", err)
  825. return ErrBadFormData
  826. }
  827. }
  828. err = app.db.UpdateCollection(&c, collAlias)
  829. if err != nil {
  830. if err, ok := err.(impart.HTTPError); ok {
  831. if reqJSON {
  832. return err
  833. }
  834. addSessionFlash(app, w, r, err.Message, nil)
  835. return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias}
  836. } else {
  837. log.Error("Couldn't update collection: %v\n", err)
  838. return err
  839. }
  840. }
  841. if reqJSON {
  842. return impart.WriteSuccess(w, struct {
  843. }{}, http.StatusOK)
  844. }
  845. addSessionFlash(app, w, r, "Blog updated!", nil)
  846. return impart.HTTPError{http.StatusFound, "/me/c/" + collAlias}
  847. }
  848. // collectionAliasFromReq takes a request and returns the collection alias
  849. // if it can be ascertained, as well as whether or not the collection uses a
  850. // custom domain.
  851. func collectionAliasFromReq(r *http.Request) string {
  852. vars := mux.Vars(r)
  853. alias := vars["subdomain"]
  854. isSubdomain := alias != ""
  855. if !isSubdomain {
  856. // Fall back to write.as/{collection} since this isn't a custom domain
  857. alias = vars["collection"]
  858. }
  859. return alias
  860. }
  861. func handleWebCollectionUnlock(app *app, w http.ResponseWriter, r *http.Request) error {
  862. var readReq struct {
  863. Alias string `schema:"alias" json:"alias"`
  864. Pass string `schema:"password" json:"password"`
  865. Next string `schema:"to" json:"to"`
  866. }
  867. // Get params
  868. if impart.ReqJSON(r) {
  869. decoder := json.NewDecoder(r.Body)
  870. err := decoder.Decode(&readReq)
  871. if err != nil {
  872. log.Error("Couldn't parse readReq JSON request: %v\n", err)
  873. return ErrBadJSON
  874. }
  875. } else {
  876. err := r.ParseForm()
  877. if err != nil {
  878. log.Error("Couldn't parse readReq form request: %v\n", err)
  879. return ErrBadFormData
  880. }
  881. err = app.formDecoder.Decode(&readReq, r.PostForm)
  882. if err != nil {
  883. log.Error("Couldn't decode readReq form request: %v\n", err)
  884. return ErrBadFormData
  885. }
  886. }
  887. if readReq.Alias == "" {
  888. return impart.HTTPError{http.StatusBadRequest, "Need a collection `alias` to read."}
  889. }
  890. if readReq.Pass == "" {
  891. return impart.HTTPError{http.StatusBadRequest, "Please supply a password."}
  892. }
  893. var collHashedPass []byte
  894. err := app.db.QueryRow("SELECT password FROM collectionpasswords INNER JOIN collections ON id = collection_id WHERE alias = ?", readReq.Alias).Scan(&collHashedPass)
  895. if err != nil {
  896. if err == sql.ErrNoRows {
  897. log.Error("No collectionpassword found when trying to read collection %s", readReq.Alias)
  898. return impart.HTTPError{http.StatusInternalServerError, "Something went very wrong. The humans have been alerted."}
  899. }
  900. return err
  901. }
  902. if !auth.Authenticated(collHashedPass, []byte(readReq.Pass)) {
  903. return impart.HTTPError{http.StatusUnauthorized, "Incorrect password."}
  904. }
  905. // Success; set cookie
  906. session, err := app.sessionStore.Get(r, blogPassCookieName)
  907. if err == nil {
  908. session.Values[readReq.Alias] = true
  909. err = session.Save(r, w)
  910. if err != nil {
  911. log.Error("Didn't save unlocked blog '%s': %v", readReq.Alias, err)
  912. }
  913. }
  914. next := "/" + readReq.Next
  915. if !app.cfg.App.SingleUser {
  916. next = "/" + readReq.Alias + next
  917. }
  918. return impart.HTTPError{http.StatusFound, next}
  919. }
  920. func isAuthorizedForCollection(app *app, alias string, r *http.Request) bool {
  921. authd := false
  922. session, err := app.sessionStore.Get(r, blogPassCookieName)
  923. if err == nil {
  924. _, authd = session.Values[alias]
  925. }
  926. return authd
  927. }