4
0

entry.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. // Copyright 2017 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package ui
  5. import (
  6. "errors"
  7. "github.com/miniflux/miniflux/http/handler"
  8. "github.com/miniflux/miniflux/integration"
  9. "github.com/miniflux/miniflux/logger"
  10. "github.com/miniflux/miniflux/model"
  11. "github.com/miniflux/miniflux/reader/sanitizer"
  12. "github.com/miniflux/miniflux/reader/scraper"
  13. "github.com/miniflux/miniflux/storage"
  14. )
  15. // FetchContent downloads the original HTML page and returns relevant contents.
  16. func (c *Controller) FetchContent(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  17. entryID, err := request.IntegerParam("entryID")
  18. if err != nil {
  19. response.HTML().BadRequest(err)
  20. return
  21. }
  22. user := ctx.LoggedUser()
  23. builder := c.store.NewEntryQueryBuilder(user.ID)
  24. builder.WithEntryID(entryID)
  25. builder.WithoutStatus(model.EntryStatusRemoved)
  26. entry, err := builder.GetEntry()
  27. if err != nil {
  28. response.JSON().ServerError(err)
  29. return
  30. }
  31. if entry == nil {
  32. response.JSON().NotFound(errors.New("Entry not found"))
  33. return
  34. }
  35. content, err := scraper.Fetch(entry.URL, entry.Feed.ScraperRules)
  36. if err != nil {
  37. response.JSON().ServerError(err)
  38. return
  39. }
  40. entry.Content = sanitizer.Sanitize(entry.URL, content)
  41. c.store.UpdateEntryContent(entry)
  42. response.JSON().Created(map[string]string{"content": entry.Content})
  43. }
  44. // SaveEntry send the link to external services.
  45. func (c *Controller) SaveEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  46. entryID, err := request.IntegerParam("entryID")
  47. if err != nil {
  48. response.HTML().BadRequest(err)
  49. return
  50. }
  51. user := ctx.LoggedUser()
  52. builder := c.store.NewEntryQueryBuilder(user.ID)
  53. builder.WithEntryID(entryID)
  54. builder.WithoutStatus(model.EntryStatusRemoved)
  55. entry, err := builder.GetEntry()
  56. if err != nil {
  57. response.JSON().ServerError(err)
  58. return
  59. }
  60. if entry == nil {
  61. response.JSON().NotFound(errors.New("Entry not found"))
  62. return
  63. }
  64. settings, err := c.store.Integration(user.ID)
  65. if err != nil {
  66. response.JSON().ServerError(err)
  67. return
  68. }
  69. go func() {
  70. integration.SendEntry(entry, settings)
  71. }()
  72. response.JSON().Created(map[string]string{"message": "saved"})
  73. }
  74. // ShowFeedEntry shows a single feed entry in "feed" mode.
  75. func (c *Controller) ShowFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  76. user := ctx.LoggedUser()
  77. entryID, err := request.IntegerParam("entryID")
  78. if err != nil {
  79. response.HTML().BadRequest(err)
  80. return
  81. }
  82. feedID, err := request.IntegerParam("feedID")
  83. if err != nil {
  84. response.HTML().BadRequest(err)
  85. return
  86. }
  87. builder := c.store.NewEntryQueryBuilder(user.ID)
  88. builder.WithFeedID(feedID)
  89. builder.WithEntryID(entryID)
  90. builder.WithoutStatus(model.EntryStatusRemoved)
  91. entry, err := builder.GetEntry()
  92. if err != nil {
  93. response.HTML().ServerError(err)
  94. return
  95. }
  96. if entry == nil {
  97. response.HTML().NotFound()
  98. return
  99. }
  100. if entry.Status == model.EntryStatusUnread {
  101. err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
  102. if err != nil {
  103. logger.Error("[Controller:ShowFeedEntry] %v", err)
  104. response.HTML().ServerError(nil)
  105. return
  106. }
  107. }
  108. args, err := c.getCommonTemplateArgs(ctx)
  109. if err != nil {
  110. response.HTML().ServerError(err)
  111. return
  112. }
  113. builder = c.store.NewEntryQueryBuilder(user.ID)
  114. builder.WithFeedID(feedID)
  115. prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
  116. if err != nil {
  117. response.HTML().ServerError(err)
  118. return
  119. }
  120. nextEntryRoute := ""
  121. if nextEntry != nil {
  122. nextEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", nextEntry.ID)
  123. }
  124. prevEntryRoute := ""
  125. if prevEntry != nil {
  126. prevEntryRoute = ctx.Route("feedEntry", "feedID", feedID, "entryID", prevEntry.ID)
  127. }
  128. response.HTML().Render("entry", args.Merge(tplParams{
  129. "entry": entry,
  130. "prevEntry": prevEntry,
  131. "nextEntry": nextEntry,
  132. "nextEntryRoute": nextEntryRoute,
  133. "prevEntryRoute": prevEntryRoute,
  134. "menu": "feeds",
  135. }))
  136. }
  137. // ShowCategoryEntry shows a single feed entry in "category" mode.
  138. func (c *Controller) ShowCategoryEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  139. user := ctx.LoggedUser()
  140. categoryID, err := request.IntegerParam("categoryID")
  141. if err != nil {
  142. response.HTML().BadRequest(err)
  143. return
  144. }
  145. entryID, err := request.IntegerParam("entryID")
  146. if err != nil {
  147. response.HTML().BadRequest(err)
  148. return
  149. }
  150. builder := c.store.NewEntryQueryBuilder(user.ID)
  151. builder.WithCategoryID(categoryID)
  152. builder.WithEntryID(entryID)
  153. builder.WithoutStatus(model.EntryStatusRemoved)
  154. entry, err := builder.GetEntry()
  155. if err != nil {
  156. response.HTML().ServerError(err)
  157. return
  158. }
  159. if entry == nil {
  160. response.HTML().NotFound()
  161. return
  162. }
  163. if entry.Status == model.EntryStatusUnread {
  164. err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
  165. if err != nil {
  166. logger.Error("[Controller:ShowCategoryEntry] %v", err)
  167. response.HTML().ServerError(nil)
  168. return
  169. }
  170. }
  171. args, err := c.getCommonTemplateArgs(ctx)
  172. if err != nil {
  173. response.HTML().ServerError(err)
  174. return
  175. }
  176. builder = c.store.NewEntryQueryBuilder(user.ID)
  177. builder.WithCategoryID(categoryID)
  178. prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
  179. if err != nil {
  180. response.HTML().ServerError(err)
  181. return
  182. }
  183. nextEntryRoute := ""
  184. if nextEntry != nil {
  185. nextEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", nextEntry.ID)
  186. }
  187. prevEntryRoute := ""
  188. if prevEntry != nil {
  189. prevEntryRoute = ctx.Route("categoryEntry", "categoryID", categoryID, "entryID", prevEntry.ID)
  190. }
  191. response.HTML().Render("entry", args.Merge(tplParams{
  192. "entry": entry,
  193. "prevEntry": prevEntry,
  194. "nextEntry": nextEntry,
  195. "nextEntryRoute": nextEntryRoute,
  196. "prevEntryRoute": prevEntryRoute,
  197. "menu": "categories",
  198. }))
  199. }
  200. // ShowUnreadEntry shows a single feed entry in "unread" mode.
  201. func (c *Controller) ShowUnreadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  202. user := ctx.LoggedUser()
  203. entryID, err := request.IntegerParam("entryID")
  204. if err != nil {
  205. response.HTML().BadRequest(err)
  206. return
  207. }
  208. builder := c.store.NewEntryQueryBuilder(user.ID)
  209. builder.WithEntryID(entryID)
  210. builder.WithoutStatus(model.EntryStatusRemoved)
  211. entry, err := builder.GetEntry()
  212. if err != nil {
  213. response.HTML().ServerError(err)
  214. return
  215. }
  216. if entry == nil {
  217. response.HTML().NotFound()
  218. return
  219. }
  220. builder = c.store.NewEntryQueryBuilder(user.ID)
  221. builder.WithStatus(model.EntryStatusUnread)
  222. prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
  223. if err != nil {
  224. response.HTML().ServerError(err)
  225. return
  226. }
  227. nextEntryRoute := ""
  228. if nextEntry != nil {
  229. nextEntryRoute = ctx.Route("unreadEntry", "entryID", nextEntry.ID)
  230. }
  231. prevEntryRoute := ""
  232. if prevEntry != nil {
  233. prevEntryRoute = ctx.Route("unreadEntry", "entryID", prevEntry.ID)
  234. }
  235. // We change the status here, otherwise we cannot get the pagination for unread items.
  236. if entry.Status == model.EntryStatusUnread {
  237. err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
  238. if err != nil {
  239. logger.Error("[Controller:ShowUnreadEntry] %v", err)
  240. response.HTML().ServerError(nil)
  241. return
  242. }
  243. }
  244. // The unread counter have to be fetched after changing the entry status
  245. args, err := c.getCommonTemplateArgs(ctx)
  246. if err != nil {
  247. response.HTML().ServerError(err)
  248. return
  249. }
  250. response.HTML().Render("entry", args.Merge(tplParams{
  251. "entry": entry,
  252. "prevEntry": prevEntry,
  253. "nextEntry": nextEntry,
  254. "nextEntryRoute": nextEntryRoute,
  255. "prevEntryRoute": prevEntryRoute,
  256. "menu": "unread",
  257. }))
  258. }
  259. // ShowReadEntry shows a single feed entry in "history" mode.
  260. func (c *Controller) ShowReadEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  261. user := ctx.LoggedUser()
  262. entryID, err := request.IntegerParam("entryID")
  263. if err != nil {
  264. response.HTML().BadRequest(err)
  265. return
  266. }
  267. builder := c.store.NewEntryQueryBuilder(user.ID)
  268. builder.WithEntryID(entryID)
  269. builder.WithoutStatus(model.EntryStatusRemoved)
  270. entry, err := builder.GetEntry()
  271. if err != nil {
  272. response.HTML().ServerError(err)
  273. return
  274. }
  275. if entry == nil {
  276. response.HTML().NotFound()
  277. return
  278. }
  279. args, err := c.getCommonTemplateArgs(ctx)
  280. if err != nil {
  281. response.HTML().ServerError(err)
  282. return
  283. }
  284. builder = c.store.NewEntryQueryBuilder(user.ID)
  285. builder.WithStatus(model.EntryStatusRead)
  286. prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
  287. if err != nil {
  288. response.HTML().ServerError(err)
  289. return
  290. }
  291. nextEntryRoute := ""
  292. if nextEntry != nil {
  293. nextEntryRoute = ctx.Route("readEntry", "entryID", nextEntry.ID)
  294. }
  295. prevEntryRoute := ""
  296. if prevEntry != nil {
  297. prevEntryRoute = ctx.Route("readEntry", "entryID", prevEntry.ID)
  298. }
  299. response.HTML().Render("entry", args.Merge(tplParams{
  300. "entry": entry,
  301. "prevEntry": prevEntry,
  302. "nextEntry": nextEntry,
  303. "nextEntryRoute": nextEntryRoute,
  304. "prevEntryRoute": prevEntryRoute,
  305. "menu": "history",
  306. }))
  307. }
  308. // ShowStarredEntry shows a single feed entry in "starred" mode.
  309. func (c *Controller) ShowStarredEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  310. user := ctx.LoggedUser()
  311. entryID, err := request.IntegerParam("entryID")
  312. if err != nil {
  313. response.HTML().BadRequest(err)
  314. return
  315. }
  316. builder := c.store.NewEntryQueryBuilder(user.ID)
  317. builder.WithEntryID(entryID)
  318. builder.WithoutStatus(model.EntryStatusRemoved)
  319. entry, err := builder.GetEntry()
  320. if err != nil {
  321. response.HTML().ServerError(err)
  322. return
  323. }
  324. if entry == nil {
  325. response.HTML().NotFound()
  326. return
  327. }
  328. if entry.Status == model.EntryStatusUnread {
  329. err = c.store.SetEntriesStatus(user.ID, []int64{entry.ID}, model.EntryStatusRead)
  330. if err != nil {
  331. logger.Error("[Controller:ShowReadEntry] %v", err)
  332. response.HTML().ServerError(nil)
  333. return
  334. }
  335. }
  336. args, err := c.getCommonTemplateArgs(ctx)
  337. if err != nil {
  338. response.HTML().ServerError(err)
  339. return
  340. }
  341. builder = c.store.NewEntryQueryBuilder(user.ID)
  342. builder.WithStarred()
  343. prevEntry, nextEntry, err := c.getEntryPrevNext(user, builder, entry.ID)
  344. if err != nil {
  345. response.HTML().ServerError(err)
  346. return
  347. }
  348. nextEntryRoute := ""
  349. if nextEntry != nil {
  350. nextEntryRoute = ctx.Route("starredEntry", "entryID", nextEntry.ID)
  351. }
  352. prevEntryRoute := ""
  353. if prevEntry != nil {
  354. prevEntryRoute = ctx.Route("starredEntry", "entryID", prevEntry.ID)
  355. }
  356. response.HTML().Render("entry", args.Merge(tplParams{
  357. "entry": entry,
  358. "prevEntry": prevEntry,
  359. "nextEntry": nextEntry,
  360. "nextEntryRoute": nextEntryRoute,
  361. "prevEntryRoute": prevEntryRoute,
  362. "menu": "starred",
  363. }))
  364. }
  365. // UpdateEntriesStatus handles Ajax request to update the status for a list of entries.
  366. func (c *Controller) UpdateEntriesStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
  367. user := ctx.LoggedUser()
  368. entryIDs, status, err := decodeEntryStatusPayload(request.Body())
  369. if err != nil {
  370. logger.Error("[Controller:UpdateEntryStatus] %v", err)
  371. response.JSON().BadRequest(nil)
  372. return
  373. }
  374. if len(entryIDs) == 0 {
  375. response.JSON().BadRequest(errors.New("The list of entryID is empty"))
  376. return
  377. }
  378. err = c.store.SetEntriesStatus(user.ID, entryIDs, status)
  379. if err != nil {
  380. logger.Error("[Controller:UpdateEntryStatus] %v", err)
  381. response.JSON().ServerError(nil)
  382. return
  383. }
  384. response.JSON().Standard("OK")
  385. }
  386. func (c *Controller) getEntryPrevNext(user *model.User, builder *storage.EntryQueryBuilder, entryID int64) (prev *model.Entry, next *model.Entry, err error) {
  387. builder.WithoutStatus(model.EntryStatusRemoved)
  388. builder.WithOrder(model.DefaultSortingOrder)
  389. builder.WithDirection(user.EntryDirection)
  390. entries, err := builder.GetEntries()
  391. if err != nil {
  392. return nil, nil, err
  393. }
  394. n := len(entries)
  395. for i := 0; i < n; i++ {
  396. if entries[i].ID == entryID {
  397. if i-1 >= 0 {
  398. prev = entries[i-1]
  399. }
  400. if i+1 < n {
  401. next = entries[i+1]
  402. }
  403. }
  404. }
  405. return prev, next, nil
  406. }