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.
 
 
 
 
 

1439 lines
38 KiB

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