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.
 
 
 
 
 

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