A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 

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