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.
 
 
 
 
 

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