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.
 
 
 
 
 

1166 regels
31 KiB

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