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

1049 řádky
27 KiB

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