A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 

1288 lignes
34 KiB

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