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.
 
 
 
 
 

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