handler.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  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 handler // import "miniflux.app/reader/handler"
  5. import (
  6. "fmt"
  7. "time"
  8. "miniflux.app/config"
  9. "miniflux.app/errors"
  10. "miniflux.app/http/client"
  11. "miniflux.app/locale"
  12. "miniflux.app/logger"
  13. "miniflux.app/model"
  14. "miniflux.app/reader/browser"
  15. "miniflux.app/reader/icon"
  16. "miniflux.app/reader/parser"
  17. "miniflux.app/reader/processor"
  18. "miniflux.app/storage"
  19. "miniflux.app/timer"
  20. )
  21. var (
  22. errDuplicate = "This feed already exists (%s)"
  23. errNotFound = "Feed %d not found"
  24. errCategoryNotFound = "Category not found for this user"
  25. )
  26. // FeedCreationArgs represents the arguments required to create a new feed.
  27. type FeedCreationArgs struct {
  28. UserID int64
  29. CategoryID int64
  30. FeedURL string
  31. UserAgent string
  32. Username string
  33. Password string
  34. Crawler bool
  35. Disabled bool
  36. IgnoreHTTPCache bool
  37. FetchViaProxy bool
  38. ScraperRules string
  39. RewriteRules string
  40. BlocklistRules string
  41. KeeplistRules string
  42. }
  43. // CreateFeed fetch, parse and store a new feed.
  44. func CreateFeed(store *storage.Storage, args *FeedCreationArgs) (*model.Feed, error) {
  45. defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[CreateFeed] FeedURL=%s", args.FeedURL))
  46. if !store.CategoryExists(args.UserID, args.CategoryID) {
  47. return nil, errors.NewLocalizedError(errCategoryNotFound)
  48. }
  49. request := client.NewClientWithConfig(args.FeedURL, config.Opts)
  50. request.WithCredentials(args.Username, args.Password)
  51. request.WithUserAgent(args.UserAgent)
  52. if args.FetchViaProxy {
  53. request.WithProxy()
  54. }
  55. response, requestErr := browser.Exec(request)
  56. if requestErr != nil {
  57. return nil, requestErr
  58. }
  59. if store.FeedURLExists(args.UserID, response.EffectiveURL) {
  60. return nil, errors.NewLocalizedError(errDuplicate, response.EffectiveURL)
  61. }
  62. subscription, parseErr := parser.ParseFeed(response.EffectiveURL, response.BodyAsString())
  63. if parseErr != nil {
  64. return nil, parseErr
  65. }
  66. subscription.UserID = args.UserID
  67. subscription.UserAgent = args.UserAgent
  68. subscription.Username = args.Username
  69. subscription.Password = args.Password
  70. subscription.Crawler = args.Crawler
  71. subscription.Disabled = args.Disabled
  72. subscription.IgnoreHTTPCache = args.IgnoreHTTPCache
  73. subscription.FetchViaProxy = args.FetchViaProxy
  74. subscription.ScraperRules = args.ScraperRules
  75. subscription.RewriteRules = args.RewriteRules
  76. subscription.BlocklistRules = args.BlocklistRules
  77. subscription.KeeplistRules = args.KeeplistRules
  78. subscription.WithCategoryID(args.CategoryID)
  79. subscription.WithClientResponse(response)
  80. subscription.CheckedNow()
  81. processor.ProcessFeedEntries(store, subscription)
  82. if storeErr := store.CreateFeed(subscription); storeErr != nil {
  83. return nil, storeErr
  84. }
  85. logger.Debug("[CreateFeed] Feed saved with ID: %d", subscription.ID)
  86. checkFeedIcon(store, subscription.ID, subscription.SiteURL, args.FetchViaProxy)
  87. return subscription, nil
  88. }
  89. // RefreshFeed refreshes a feed.
  90. func RefreshFeed(store *storage.Storage, userID, feedID int64) error {
  91. defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[RefreshFeed] feedID=%d", feedID))
  92. userLanguage := store.UserLanguage(userID)
  93. printer := locale.NewPrinter(userLanguage)
  94. originalFeed, storeErr := store.FeedByID(userID, feedID)
  95. if storeErr != nil {
  96. return storeErr
  97. }
  98. if originalFeed == nil {
  99. return errors.NewLocalizedError(errNotFound, feedID)
  100. }
  101. weeklyEntryCount := 0
  102. if config.Opts.PollingScheduler() == model.SchedulerEntryFrequency {
  103. var weeklyCountErr error
  104. weeklyEntryCount, weeklyCountErr = store.WeeklyFeedEntryCount(userID, feedID)
  105. if weeklyCountErr != nil {
  106. return weeklyCountErr
  107. }
  108. }
  109. originalFeed.CheckedNow()
  110. originalFeed.ScheduleNextCheck(weeklyEntryCount)
  111. request := client.NewClientWithConfig(originalFeed.FeedURL, config.Opts)
  112. request.WithCredentials(originalFeed.Username, originalFeed.Password)
  113. request.WithUserAgent(originalFeed.UserAgent)
  114. if !originalFeed.IgnoreHTTPCache {
  115. request.WithCacheHeaders(originalFeed.EtagHeader, originalFeed.LastModifiedHeader)
  116. }
  117. if originalFeed.FetchViaProxy {
  118. request.WithProxy()
  119. }
  120. response, requestErr := browser.Exec(request)
  121. if requestErr != nil {
  122. originalFeed.WithError(requestErr.Localize(printer))
  123. store.UpdateFeedError(originalFeed)
  124. return requestErr
  125. }
  126. if store.AnotherFeedURLExists(userID, originalFeed.ID, response.EffectiveURL) {
  127. storeErr := errors.NewLocalizedError(errDuplicate, response.EffectiveURL)
  128. originalFeed.WithError(storeErr.Error())
  129. store.UpdateFeedError(originalFeed)
  130. return storeErr
  131. }
  132. if originalFeed.IgnoreHTTPCache || response.IsModified(originalFeed.EtagHeader, originalFeed.LastModifiedHeader) {
  133. logger.Debug("[RefreshFeed] Feed #%d has been modified", feedID)
  134. updatedFeed, parseErr := parser.ParseFeed(response.EffectiveURL, response.BodyAsString())
  135. if parseErr != nil {
  136. originalFeed.WithError(parseErr.Localize(printer))
  137. store.UpdateFeedError(originalFeed)
  138. return parseErr
  139. }
  140. originalFeed.Entries = updatedFeed.Entries
  141. processor.ProcessFeedEntries(store, originalFeed)
  142. // We don't update existing entries when the crawler is enabled (we crawl only inexisting entries).
  143. if storeErr := store.RefreshFeedEntries(originalFeed.UserID, originalFeed.ID, originalFeed.Entries, !originalFeed.Crawler); storeErr != nil {
  144. originalFeed.WithError(storeErr.Error())
  145. store.UpdateFeedError(originalFeed)
  146. return storeErr
  147. }
  148. // We update caching headers only if the feed has been modified,
  149. // because some websites don't return the same headers when replying with a 304.
  150. originalFeed.WithClientResponse(response)
  151. checkFeedIcon(store, originalFeed.ID, originalFeed.SiteURL, originalFeed.FetchViaProxy)
  152. } else {
  153. logger.Debug("[RefreshFeed] Feed #%d not modified", feedID)
  154. }
  155. originalFeed.ResetErrorCounter()
  156. if storeErr := store.UpdateFeed(originalFeed); storeErr != nil {
  157. originalFeed.WithError(storeErr.Error())
  158. store.UpdateFeedError(originalFeed)
  159. return storeErr
  160. }
  161. return nil
  162. }
  163. func checkFeedIcon(store *storage.Storage, feedID int64, websiteURL string, fetchViaProxy bool) {
  164. if !store.HasIcon(feedID) {
  165. icon, err := icon.FindIcon(websiteURL, fetchViaProxy)
  166. if err != nil {
  167. logger.Debug(`[CheckFeedIcon] %v (feedID=%d websiteURL=%s)`, err, feedID, websiteURL)
  168. } else if icon == nil {
  169. logger.Debug(`[CheckFeedIcon] No icon found (feedID=%d websiteURL=%s)`, feedID, websiteURL)
  170. } else {
  171. if err := store.CreateFeedIcon(feedID, icon); err != nil {
  172. logger.Debug(`[CheckFeedIcon] %v (feedID=%d websiteURL=%s)`, err, feedID, websiteURL)
  173. }
  174. }
  175. }
  176. }