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.
 
 
 
 
 

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