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.
 
 
 
 
 

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