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.
 
 
 
 
 

460 lines
13 KiB

  1. /*
  2. * Copyright © 2018-2019 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. "fmt"
  14. "net/http"
  15. "runtime"
  16. "strconv"
  17. "time"
  18. "github.com/gorilla/mux"
  19. "github.com/writeas/impart"
  20. "github.com/writeas/web-core/auth"
  21. "github.com/writeas/web-core/log"
  22. "github.com/writeas/writefreely/appstats"
  23. "github.com/writeas/writefreely/config"
  24. )
  25. var (
  26. appStartTime = time.Now()
  27. sysStatus systemStatus
  28. )
  29. const adminUsersPerPage = 30
  30. type systemStatus struct {
  31. Uptime string
  32. NumGoroutine int
  33. // General statistics.
  34. MemAllocated string // bytes allocated and still in use
  35. MemTotal string // bytes allocated (even if freed)
  36. MemSys string // bytes obtained from system (sum of XxxSys below)
  37. Lookups uint64 // number of pointer lookups
  38. MemMallocs uint64 // number of mallocs
  39. MemFrees uint64 // number of frees
  40. // Main allocation heap statistics.
  41. HeapAlloc string // bytes allocated and still in use
  42. HeapSys string // bytes obtained from system
  43. HeapIdle string // bytes in idle spans
  44. HeapInuse string // bytes in non-idle span
  45. HeapReleased string // bytes released to the OS
  46. HeapObjects uint64 // total number of allocated objects
  47. // Low-level fixed-size structure allocator statistics.
  48. // Inuse is bytes used now.
  49. // Sys is bytes obtained from system.
  50. StackInuse string // bootstrap stacks
  51. StackSys string
  52. MSpanInuse string // mspan structures
  53. MSpanSys string
  54. MCacheInuse string // mcache structures
  55. MCacheSys string
  56. BuckHashSys string // profiling bucket hash table
  57. GCSys string // GC metadata
  58. OtherSys string // other system allocations
  59. // Garbage collector statistics.
  60. NextGC string // next run in HeapAlloc time (bytes)
  61. LastGC string // last run in absolute time (ns)
  62. PauseTotalNs string
  63. PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
  64. NumGC uint32
  65. }
  66. type inspectedCollection struct {
  67. CollectionObj
  68. Followers int
  69. LastPost string
  70. }
  71. type instanceContent struct {
  72. ID string
  73. Type string
  74. Title sql.NullString
  75. Content string
  76. Updated time.Time
  77. }
  78. func (c instanceContent) UpdatedFriendly() string {
  79. /*
  80. // TODO: accept a locale in this method and use that for the format
  81. var loc monday.Locale = monday.LocaleEnUS
  82. return monday.Format(u.Created, monday.DateTimeFormatsByLocale[loc], loc)
  83. */
  84. return c.Updated.Format("January 2, 2006, 3:04 PM")
  85. }
  86. func handleViewAdminDash(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  87. updateAppStats()
  88. p := struct {
  89. *UserPage
  90. SysStatus systemStatus
  91. Config config.AppCfg
  92. Message, ConfigMessage string
  93. }{
  94. UserPage: NewUserPage(app, r, u, "Admin", nil),
  95. SysStatus: sysStatus,
  96. Config: app.cfg.App,
  97. Message: r.FormValue("m"),
  98. ConfigMessage: r.FormValue("cm"),
  99. }
  100. showUserPage(w, "admin", p)
  101. return nil
  102. }
  103. func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  104. p := struct {
  105. *UserPage
  106. Config config.AppCfg
  107. Message string
  108. Users *[]User
  109. CurPage int
  110. TotalUsers int64
  111. TotalPages []int
  112. }{
  113. UserPage: NewUserPage(app, r, u, "Users", nil),
  114. Config: app.cfg.App,
  115. Message: r.FormValue("m"),
  116. }
  117. p.TotalUsers = app.db.GetAllUsersCount()
  118. ttlPages := p.TotalUsers / adminUsersPerPage
  119. p.TotalPages = []int{}
  120. for i := 1; i <= int(ttlPages); i++ {
  121. p.TotalPages = append(p.TotalPages, i)
  122. }
  123. var err error
  124. p.CurPage, err = strconv.Atoi(r.FormValue("p"))
  125. if err != nil || p.CurPage < 1 {
  126. p.CurPage = 1
  127. } else if p.CurPage > int(ttlPages) {
  128. p.CurPage = int(ttlPages)
  129. }
  130. p.Users, err = app.db.GetAllUsers(uint(p.CurPage))
  131. if err != nil {
  132. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get users: %v", err)}
  133. }
  134. showUserPage(w, "users", p)
  135. return nil
  136. }
  137. func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  138. vars := mux.Vars(r)
  139. username := vars["username"]
  140. if username == "" {
  141. return impart.HTTPError{http.StatusFound, "/admin/users"}
  142. }
  143. p := struct {
  144. *UserPage
  145. Config config.AppCfg
  146. Message string
  147. User *User
  148. Colls []inspectedCollection
  149. LastPost string
  150. TotalPosts int64
  151. }{
  152. Config: app.cfg.App,
  153. Message: r.FormValue("m"),
  154. Colls: []inspectedCollection{},
  155. }
  156. var err error
  157. p.User, err = app.db.GetUserForAuth(username)
  158. if err != nil {
  159. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user: %v", err)}
  160. }
  161. p.UserPage = NewUserPage(app, r, u, p.User.Username, nil)
  162. p.TotalPosts = app.db.GetUserPostsCount(p.User.ID)
  163. lp, err := app.db.GetUserLastPostTime(p.User.ID)
  164. if err != nil {
  165. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's last post time: %v", err)}
  166. }
  167. if lp != nil {
  168. p.LastPost = lp.Format("January 2, 2006, 3:04 PM")
  169. }
  170. colls, err := app.db.GetCollections(p.User, app.cfg.App.Host)
  171. if err != nil {
  172. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)}
  173. }
  174. for _, c := range *colls {
  175. ic := inspectedCollection{
  176. CollectionObj: CollectionObj{Collection: c},
  177. }
  178. if app.cfg.App.Federation {
  179. folls, err := app.db.GetAPFollowers(&c)
  180. if err == nil {
  181. // TODO: handle error here (at least log it)
  182. ic.Followers = len(*folls)
  183. }
  184. }
  185. app.db.GetPostsCount(&ic.CollectionObj, true)
  186. lp, err := app.db.GetCollectionLastPostTime(c.ID)
  187. if err != nil {
  188. log.Error("Didn't get last post time for collection %d: %v", c.ID, err)
  189. }
  190. if lp != nil {
  191. ic.LastPost = lp.Format("January 2, 2006, 3:04 PM")
  192. }
  193. p.Colls = append(p.Colls, ic)
  194. }
  195. showUserPage(w, "view-user", p)
  196. return nil
  197. }
  198. func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  199. p := struct {
  200. *UserPage
  201. Config config.AppCfg
  202. Message string
  203. Pages []*instanceContent
  204. }{
  205. UserPage: NewUserPage(app, r, u, "Pages", nil),
  206. Config: app.cfg.App,
  207. Message: r.FormValue("m"),
  208. }
  209. var err error
  210. p.Pages, err = app.db.GetInstancePages()
  211. if err != nil {
  212. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)}
  213. }
  214. // Add in default pages
  215. var hasAbout, hasPrivacy bool
  216. for i, c := range p.Pages {
  217. if hasAbout && hasPrivacy {
  218. break
  219. }
  220. if c.ID == "about" {
  221. hasAbout = true
  222. if !c.Title.Valid {
  223. p.Pages[i].Title = defaultAboutTitle(app.cfg)
  224. }
  225. } else if c.ID == "privacy" {
  226. hasPrivacy = true
  227. if !c.Title.Valid {
  228. p.Pages[i].Title = defaultPrivacyTitle()
  229. }
  230. }
  231. }
  232. if !hasAbout {
  233. p.Pages = append(p.Pages, &instanceContent{
  234. ID: "about",
  235. Title: defaultAboutTitle(app.cfg),
  236. Content: defaultAboutPage(app.cfg),
  237. Updated: defaultPageUpdatedTime,
  238. })
  239. }
  240. if !hasPrivacy {
  241. p.Pages = append(p.Pages, &instanceContent{
  242. ID: "privacy",
  243. Title: defaultPrivacyTitle(),
  244. Content: defaultPrivacyPolicy(app.cfg),
  245. Updated: defaultPageUpdatedTime,
  246. })
  247. }
  248. showUserPage(w, "pages", p)
  249. return nil
  250. }
  251. func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  252. vars := mux.Vars(r)
  253. slug := vars["slug"]
  254. if slug == "" {
  255. return impart.HTTPError{http.StatusFound, "/admin/pages"}
  256. }
  257. p := struct {
  258. *UserPage
  259. Config config.AppCfg
  260. Message string
  261. Banner *instanceContent
  262. Content *instanceContent
  263. }{
  264. Config: app.cfg.App,
  265. Message: r.FormValue("m"),
  266. }
  267. var err error
  268. // Get pre-defined pages, or select slug
  269. if slug == "about" {
  270. p.Content, err = getAboutPage(app)
  271. } else if slug == "privacy" {
  272. p.Content, err = getPrivacyPage(app)
  273. } else if slug == "landing" {
  274. p.Banner, err = getLandingBanner(app)
  275. if err != nil {
  276. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
  277. }
  278. p.Content, err = getLandingBody(app)
  279. p.Content.ID = "landing"
  280. } else if slug == "reader" {
  281. p.Content, err = getReaderSection(app)
  282. } else {
  283. p.Content, err = app.db.GetDynamicContent(slug)
  284. }
  285. if err != nil {
  286. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
  287. }
  288. title := "New page"
  289. if p.Content != nil {
  290. title = "Edit " + p.Content.ID
  291. } else {
  292. p.Content = &instanceContent{}
  293. }
  294. p.UserPage = NewUserPage(app, r, u, title, nil)
  295. showUserPage(w, "view-page", p)
  296. return nil
  297. }
  298. func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  299. vars := mux.Vars(r)
  300. id := vars["page"]
  301. // Validate
  302. if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
  303. return impart.HTTPError{http.StatusNotFound, "No such page."}
  304. }
  305. var err error
  306. m := ""
  307. if id == "landing" {
  308. // Handle special landing page
  309. err = app.db.UpdateDynamicContent("landing-banner", "", r.FormValue("banner"), "section")
  310. if err != nil {
  311. m = "?m=" + err.Error()
  312. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  313. }
  314. err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
  315. } else if id == "reader" {
  316. // Update sections with titles
  317. err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section")
  318. } else {
  319. // Update page
  320. err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
  321. }
  322. if err != nil {
  323. m = "?m=" + err.Error()
  324. }
  325. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  326. }
  327. func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
  328. apper.App().cfg.App.SiteName = r.FormValue("site_name")
  329. apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
  330. apper.App().cfg.App.Landing = r.FormValue("landing")
  331. apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
  332. mul, err := strconv.Atoi(r.FormValue("min_username_len"))
  333. if err == nil {
  334. apper.App().cfg.App.MinUsernameLen = mul
  335. }
  336. mb, err := strconv.Atoi(r.FormValue("max_blogs"))
  337. if err == nil {
  338. apper.App().cfg.App.MaxBlogs = mb
  339. }
  340. apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
  341. apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
  342. apper.App().cfg.App.Private = r.FormValue("private") == "on"
  343. apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
  344. if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
  345. log.Info("Initializing local timeline...")
  346. initLocalTimeline(apper.App())
  347. }
  348. apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
  349. if apper.App().cfg.App.UserInvites == "none" {
  350. apper.App().cfg.App.UserInvites = ""
  351. }
  352. apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
  353. m := "?cm=Configuration+saved."
  354. err = apper.SaveConfig(apper.App().cfg)
  355. if err != nil {
  356. m = "?cm=" + err.Error()
  357. }
  358. return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
  359. }
  360. func updateAppStats() {
  361. sysStatus.Uptime = appstats.TimeSincePro(appStartTime)
  362. m := new(runtime.MemStats)
  363. runtime.ReadMemStats(m)
  364. sysStatus.NumGoroutine = runtime.NumGoroutine()
  365. sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc))
  366. sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc))
  367. sysStatus.MemSys = appstats.FileSize(int64(m.Sys))
  368. sysStatus.Lookups = m.Lookups
  369. sysStatus.MemMallocs = m.Mallocs
  370. sysStatus.MemFrees = m.Frees
  371. sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc))
  372. sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys))
  373. sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle))
  374. sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse))
  375. sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased))
  376. sysStatus.HeapObjects = m.HeapObjects
  377. sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse))
  378. sysStatus.StackSys = appstats.FileSize(int64(m.StackSys))
  379. sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse))
  380. sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys))
  381. sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse))
  382. sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys))
  383. sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys))
  384. sysStatus.GCSys = appstats.FileSize(int64(m.GCSys))
  385. sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys))
  386. sysStatus.NextGC = appstats.FileSize(int64(m.NextGC))
  387. sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
  388. sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
  389. sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
  390. sysStatus.NumGC = m.NumGC
  391. }
  392. func adminResetPassword(app *App, u *User, newPass string) error {
  393. hashedPass, err := auth.HashPass([]byte(newPass))
  394. if err != nil {
  395. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
  396. }
  397. err = app.db.ChangePassphrase(u.ID, true, "", hashedPass)
  398. if err != nil {
  399. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
  400. }
  401. return nil
  402. }