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.
 
 
 
 
 

481 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/gogits/gogs/pkg/tool"
  19. "github.com/gorilla/mux"
  20. "github.com/writeas/impart"
  21. "github.com/writeas/web-core/auth"
  22. "github.com/writeas/web-core/log"
  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)
  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 {
  281. p.Content, err = app.db.GetDynamicContent(slug)
  282. }
  283. if err != nil {
  284. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get page: %v", err)}
  285. }
  286. title := "New page"
  287. if p.Content != nil {
  288. title = "Edit " + p.Content.ID
  289. } else {
  290. p.Content = &instanceContent{}
  291. }
  292. p.UserPage = NewUserPage(app, r, u, title, nil)
  293. showUserPage(w, "view-page", p)
  294. return nil
  295. }
  296. func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  297. vars := mux.Vars(r)
  298. id := vars["page"]
  299. // Validate
  300. if id != "about" && id != "privacy" && id != "landing" {
  301. return impart.HTTPError{http.StatusNotFound, "No such page."}
  302. }
  303. var err error
  304. m := ""
  305. if id == "landing" {
  306. // Handle special landing page
  307. err = app.db.UpdateDynamicContent("landing-banner", "", r.FormValue("banner"), "section")
  308. if err != nil {
  309. m = "?m=" + err.Error()
  310. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  311. }
  312. err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section")
  313. } else {
  314. // Update page
  315. err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page")
  316. }
  317. if err != nil {
  318. m = "?m=" + err.Error()
  319. }
  320. return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m}
  321. }
  322. func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error {
  323. apper.App().cfg.App.SiteName = r.FormValue("site_name")
  324. apper.App().cfg.App.SiteDesc = r.FormValue("site_desc")
  325. apper.App().cfg.App.Landing = r.FormValue("landing")
  326. apper.App().cfg.App.OpenRegistration = r.FormValue("open_registration") == "on"
  327. mul, err := strconv.Atoi(r.FormValue("min_username_len"))
  328. if err == nil {
  329. apper.App().cfg.App.MinUsernameLen = mul
  330. }
  331. mb, err := strconv.Atoi(r.FormValue("max_blogs"))
  332. if err == nil {
  333. apper.App().cfg.App.MaxBlogs = mb
  334. }
  335. apper.App().cfg.App.Federation = r.FormValue("federation") == "on"
  336. apper.App().cfg.App.PublicStats = r.FormValue("public_stats") == "on"
  337. apper.App().cfg.App.Private = r.FormValue("private") == "on"
  338. apper.App().cfg.App.LocalTimeline = r.FormValue("local_timeline") == "on"
  339. if apper.App().cfg.App.LocalTimeline && apper.App().timeline == nil {
  340. log.Info("Initializing local timeline...")
  341. initLocalTimeline(apper.App())
  342. }
  343. apper.App().cfg.App.UserInvites = r.FormValue("user_invites")
  344. if apper.App().cfg.App.UserInvites == "none" {
  345. apper.App().cfg.App.UserInvites = ""
  346. }
  347. apper.App().cfg.App.DefaultVisibility = r.FormValue("default_visibility")
  348. m := "?cm=Configuration+saved."
  349. err = apper.SaveConfig(apper.App().cfg)
  350. if err != nil {
  351. m = "?cm=" + err.Error()
  352. }
  353. return impart.HTTPError{http.StatusFound, "/admin" + m + "#config"}
  354. }
  355. func updateAppStats() {
  356. sysStatus.Uptime = tool.TimeSincePro(appStartTime)
  357. m := new(runtime.MemStats)
  358. runtime.ReadMemStats(m)
  359. sysStatus.NumGoroutine = runtime.NumGoroutine()
  360. sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc))
  361. sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc))
  362. sysStatus.MemSys = tool.FileSize(int64(m.Sys))
  363. sysStatus.Lookups = m.Lookups
  364. sysStatus.MemMallocs = m.Mallocs
  365. sysStatus.MemFrees = m.Frees
  366. sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc))
  367. sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys))
  368. sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle))
  369. sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse))
  370. sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased))
  371. sysStatus.HeapObjects = m.HeapObjects
  372. sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse))
  373. sysStatus.StackSys = tool.FileSize(int64(m.StackSys))
  374. sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse))
  375. sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys))
  376. sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse))
  377. sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys))
  378. sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys))
  379. sysStatus.GCSys = tool.FileSize(int64(m.GCSys))
  380. sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys))
  381. sysStatus.NextGC = tool.FileSize(int64(m.NextGC))
  382. sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
  383. sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
  384. sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
  385. sysStatus.NumGC = m.NumGC
  386. }
  387. func adminResetPassword(app *App, u *User, newPass string) error {
  388. hashedPass, err := auth.HashPass([]byte(newPass))
  389. if err != nil {
  390. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not create password hash: %v", err)}
  391. }
  392. err = app.db.ChangePassphrase(u.ID, true, "", hashedPass)
  393. if err != nil {
  394. return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not update passphrase: %v", err)}
  395. }
  396. return nil
  397. }
  398. func handleViewAdminUpdates(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
  399. check := r.URL.Query().Get("check")
  400. if check == "now" && app.cfg.App.UpdateChecks {
  401. app.updates.CheckNow()
  402. }
  403. p := struct {
  404. *UserPage
  405. LastChecked string
  406. LatestVersion string
  407. LatestReleaseURL string
  408. UpdateAvailable bool
  409. }{
  410. UserPage: NewUserPage(app, r, u, "Updates", nil),
  411. }
  412. if app.cfg.App.UpdateChecks {
  413. p.LastChecked = app.updates.lastCheck.Format("January 2, 2006, 3:04 PM")
  414. p.LatestVersion = app.updates.LatestVersion()
  415. p.LatestReleaseURL = app.updates.ReleaseURL()
  416. p.UpdateAvailable = app.updates.AreAvailable()
  417. }
  418. showUserPage(w, "app-updates", p)
  419. return nil
  420. }