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.
 
 
 
 
 

1161 lines
31 KiB

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