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.
 
 
 
 
 

485 lines
14 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 handleAdminToggleUserSuspended(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  199. vars := mux.Vars(r)
  200. username := vars["username"]
  201. if username == "" {
  202. return impart.HTTPError{http.StatusFound, "/admin/users"}
  203. }
  204. userToToggle, err := app.db.GetUserForAuth(username)
  205. if err != nil {
  206. log.Error("failed to get user: %v", err)
  207. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user from username: %v", err)}
  208. }
  209. if userToToggle.Suspended {
  210. err = app.db.SetUserSuspended(userToToggle.ID, false)
  211. } else {
  212. err = app.db.SetUserSuspended(userToToggle.ID, true)
  213. }
  214. if err != nil {
  215. log.Error("toggle user suspended: %v", err)
  216. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not toggle user suspended: %v")}
  217. }
  218. // TODO: invalidate sessions
  219. return impart.HTTPError{http.StatusFound, fmt.Sprintf("/admin/user/%s#status", username)}
  220. }
  221. func handleViewAdminPages(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  222. p := struct {
  223. *UserPage
  224. Config config.AppCfg
  225. Message string
  226. Pages []*instanceContent
  227. }{
  228. UserPage: NewUserPage(app, r, u, "Pages", nil),
  229. Config: app.cfg.App,
  230. Message: r.FormValue("m"),
  231. }
  232. var err error
  233. p.Pages, err = app.db.GetInstancePages()
  234. if err != nil {
  235. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get pages: %v", err)}
  236. }
  237. // Add in default pages
  238. var hasAbout, hasPrivacy bool
  239. for i, c := range p.Pages {
  240. if hasAbout && hasPrivacy {
  241. break
  242. }
  243. if c.ID == "about" {
  244. hasAbout = true
  245. if !c.Title.Valid {
  246. p.Pages[i].Title = defaultAboutTitle(app.cfg)
  247. }
  248. } else if c.ID == "privacy" {
  249. hasPrivacy = true
  250. if !c.Title.Valid {
  251. p.Pages[i].Title = defaultPrivacyTitle()
  252. }
  253. }
  254. }
  255. if !hasAbout {
  256. p.Pages = append(p.Pages, &instanceContent{
  257. ID: "about",
  258. Title: defaultAboutTitle(app.cfg),
  259. Content: defaultAboutPage(app.cfg),
  260. Updated: defaultPageUpdatedTime,
  261. })
  262. }
  263. if !hasPrivacy {
  264. p.Pages = append(p.Pages, &instanceContent{
  265. ID: "privacy",
  266. Title: defaultPrivacyTitle(),
  267. Content: defaultPrivacyPolicy(app.cfg),
  268. Updated: defaultPageUpdatedTime,
  269. })
  270. }
  271. showUserPage(w, "pages", p)
  272. return nil
  273. }
  274. func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  275. vars := mux.Vars(r)
  276. slug := vars["slug"]
  277. if slug == "" {
  278. return impart.HTTPError{http.StatusFound, "/admin/pages"}
  279. }
  280. p := struct {
  281. *UserPage
  282. Config config.AppCfg
  283. Message string
  284. Banner *instanceContent
  285. Content *instanceContent
  286. }{
  287. Config: app.cfg.App,
  288. Message: r.FormValue("m"),
  289. }
  290. var err error
  291. // Get pre-defined pages, or select slug
  292. if slug == "about" {
  293. p.Content, err = getAboutPage(app)
  294. } else if slug == "privacy" {
  295. p.Content, err = getPrivacyPage(app)
  296. } else if slug == "landing" {
  297. p.Banner, err = getLandingBanner(app)
  298. if err != nil {
  299. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)}
  300. }
  301. p.Content, err = getLandingBody(app)
  302. p.Content.ID = "landing"
  303. } else if slug == "reader" {
  304. p.Content, err = getReaderSection(app)
  305. } else {
  306. p.Content, err = app.db.GetDynamicContent(slug)
  307. }
  308. if err != nil {
  309. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
  310. }
  311. title := "New page"
  312. if p.Content != nil {
  313. title = "Edit " + p.Content.ID
  314. } else {
  315. p.Content = &instanceContent{}
  316. }
  317. p.UserPage = NewUserPage(app, r, u, title, nil)
  318. showUserPage(w, "view-page", p)
  319. return nil
  320. }
  321. func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  322. vars := mux.Vars(r)
  323. id := vars["page"]
  324. // Validate
  325. if id != "about" && id != "privacy" && id != "landing" && id != "reader" {
  326. return impart.HTTPError{http.StatusNotFound, "No such page."}
  327. }
  328. var err error
  329. m := ""
  330. if id == "landing" {
  331. // Handle special landing page
  332. err = app.db.UpdateDynamicContent("landing-banner", "", r.FormValue("banner"), "section")
  333. if err != nil {
  334. m = "?m=" + err.Error()
  335. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  336. }
  337. err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
  338. } else if id == "reader" {
  339. // Update sections with titles
  340. err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section")
  341. } else {
  342. // Update page
  343. err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
  344. }
  345. if err != nil {
  346. m = "?m=" + err.Error()
  347. }
  348. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  349. }
  350. func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
  351. apper.App().cfg.App.SiteName = r.FormValue("site_name")
  352. apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
  353. apper.App().cfg.App.Landing = r.FormValue("landing")
  354. apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
  355. mul, err := strconv.Atoi(r.FormValue("min_username_len"))
  356. if err == nil {
  357. apper.App().cfg.App.MinUsernameLen = mul
  358. }
  359. mb, err := strconv.Atoi(r.FormValue("max_blogs"))
  360. if err == nil {
  361. apper.App().cfg.App.MaxBlogs = mb
  362. }
  363. apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
  364. apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
  365. apper.App().cfg.App.Private = r.FormValue("private") == "on"
  366. apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
  367. if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
  368. log.Info("Initializing local timeline...")
  369. initLocalTimeline(apper.App())
  370. }
  371. apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
  372. if apper.App().cfg.App.UserInvites == "none" {
  373. apper.App().cfg.App.UserInvites = ""
  374. }
  375. apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
  376. m := "?cm=Configuration+saved."
  377. err = apper.SaveConfig(apper.App().cfg)
  378. if err != nil {
  379. m = "?cm=" + err.Error()
  380. }
  381. return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
  382. }
  383. func updateAppStats() {
  384. sysStatus.Uptime = appstats.TimeSincePro(appStartTime)
  385. m := new(runtime.MemStats)
  386. runtime.ReadMemStats(m)
  387. sysStatus.NumGoroutine = runtime.NumGoroutine()
  388. sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc))
  389. sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc))
  390. sysStatus.MemSys = appstats.FileSize(int64(m.Sys))
  391. sysStatus.Lookups = m.Lookups
  392. sysStatus.MemMallocs = m.Mallocs
  393. sysStatus.MemFrees = m.Frees
  394. sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc))
  395. sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys))
  396. sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle))
  397. sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse))
  398. sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased))
  399. sysStatus.HeapObjects = m.HeapObjects
  400. sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse))
  401. sysStatus.StackSys = appstats.FileSize(int64(m.StackSys))
  402. sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse))
  403. sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys))
  404. sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse))
  405. sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys))
  406. sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys))
  407. sysStatus.GCSys = appstats.FileSize(int64(m.GCSys))
  408. sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys))
  409. sysStatus.NextGC = appstats.FileSize(int64(m.NextGC))
  410. sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
  411. sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
  412. sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
  413. sysStatus.NumGC = m.NumGC
  414. }
  415. func adminResetPassword(app *App, u *User, newPass string) error {
  416. hashedPass, err := auth.HashPass([]byte(newPass))
  417. if err != nil {
  418. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
  419. }
  420. err = app.db.ChangePassphrase(u.ID, true, "", hashedPass)
  421. if err != nil {
  422. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
  423. }
  424. return nil
  425. }