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.
 
 
 
 
 

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