A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

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