diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 8285038efa6..8a621ca7e2a 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -960,4 +960,13 @@ var migrations = []func(tx *sql.Tx) error{ _, err = tx.Exec(sql) return err }, + func(tx *sql.Tx) (err error) { + sql := ` + ALTER TABLE integrations DROP COLUMN googlereader_enabled; + ALTER TABLE integrations DROP COLUMN googlereader_username; + ALTER TABLE integrations DROP COLUMN googlereader_password; + ` + _, err = tx.Exec(sql) + return err + }, } diff --git a/internal/googlereader/handler.go b/internal/googlereader/handler.go deleted file mode 100644 index c3eb70afc46..00000000000 --- a/internal/googlereader/handler.go +++ /dev/null @@ -1,1501 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package googlereader // import "miniflux.app/v2/internal/googlereader" - -import ( - "errors" - "fmt" - "log/slog" - "net/http" - "strconv" - "strings" - "time" - - "miniflux.app/v2/internal/config" - "miniflux.app/v2/internal/http/request" - "miniflux.app/v2/internal/http/response" - "miniflux.app/v2/internal/http/response/json" - "miniflux.app/v2/internal/http/route" - "miniflux.app/v2/internal/integration" - "miniflux.app/v2/internal/mediaproxy" - "miniflux.app/v2/internal/model" - "miniflux.app/v2/internal/reader/fetcher" - mff "miniflux.app/v2/internal/reader/handler" - mfs "miniflux.app/v2/internal/reader/subscription" - "miniflux.app/v2/internal/storage" - "miniflux.app/v2/internal/validator" - - "github.com/gorilla/mux" -) - -type handler struct { - store *storage.Storage - router *mux.Router -} - -const ( - // StreamPrefix is the prefix for astreams (read/starred/reading list and so on) - StreamPrefix = "user/-/state/com.google/" - // UserStreamPrefix is the user specific prefix for streams (read/starred/reading list and so on) - UserStreamPrefix = "user/%d/state/com.google/" - // LabelPrefix is the prefix for a label stream - LabelPrefix = "user/-/label/" - // UserLabelPrefix is the user specific prefix prefix for a label stream - UserLabelPrefix = "user/%d/label/" - // FeedPrefix is the prefix for a feed stream - FeedPrefix = "feed/" - // Read is the suffix for read stream - Read = "read" - // Starred is the suffix for starred stream - Starred = "starred" - // ReadingList is the suffix for reading list stream - ReadingList = "reading-list" - // KeptUnread is the suffix for kept unread stream - KeptUnread = "kept-unread" - // Broadcast is the suffix for broadcast stream - Broadcast = "broadcast" - // BroadcastFriends is the suffix for broadcast friends stream - BroadcastFriends = "broadcast-friends" - // Like is the suffix for like stream - Like = "like" - // EntryIDLong is the long entry id representation - EntryIDLong = "tag:google.com,2005:reader/item/%016x" -) - -const ( - // ParamItemIDs - name of the parameter with the item ids - ParamItemIDs = "i" - // ParamStreamID - name of the parameter containing the stream to be included - ParamStreamID = "s" - // ParamStreamExcludes - name of the parameter containing streams to be excluded - ParamStreamExcludes = "xt" - // ParamStreamFilters - name of the parameter containing streams to be included - ParamStreamFilters = "it" - // ParamStreamMaxItems - name of the parameter containing number of items per page/max items returned - ParamStreamMaxItems = "n" - // ParamStreamOrder - name of the parameter containing the sort criteria - ParamStreamOrder = "r" - // ParamStreamStartTime - name of the parameter containing epoch timestamp, filtering items older than - ParamStreamStartTime = "ot" - // ParamStreamStopTime - name of the parameter containing epoch timestamp, filtering items newer than - ParamStreamStopTime = "nt" - // ParamTagsRemove - name of the parameter containing tags (streams) to be removed - ParamTagsRemove = "r" - // ParamTagsAdd - name of the parameter containing tags (streams) to be added - ParamTagsAdd = "a" - // ParamSubscribeAction - name of the parameter indicating the action to take for subscription/edit - ParamSubscribeAction = "ac" - // ParamTitle - name of the parameter for the title of the subscription - ParamTitle = "t" - // ParamQuickAdd - name of the parameter for a URL being quick subscribed to - ParamQuickAdd = "quickadd" - // ParamDestination - name of the parameter for the new name of a tag - ParamDestination = "dest" - // ParamContinuation - name of the parameter for callers to pass to receive the next page of results - ParamContinuation = "c" -) - -// StreamType represents the possible stream types -type StreamType int - -const ( - // NoStream - no stream type - NoStream StreamType = iota - // ReadStream - read stream type - ReadStream - // StarredStream - starred stream type - StarredStream - // ReadingListStream - reading list stream type - ReadingListStream - // KeptUnreadStream - kept unread stream type - KeptUnreadStream - // BroadcastStream - broadcast stream type - BroadcastStream - // BroadcastFriendsStream - broadcast friends stream type - BroadcastFriendsStream - // LabelStream - label stream type - LabelStream - // FeedStream - feed stream type - FeedStream - // LikeStream - like stream type - LikeStream -) - -// Stream defines a stream type and its ID. -type Stream struct { - Type StreamType - ID string -} - -func (s Stream) String() string { - return fmt.Sprintf("%v - '%s'", s.Type, s.ID) -} - -func (st StreamType) String() string { - switch st { - case NoStream: - return "NoStream" - case ReadStream: - return "ReadStream" - case StarredStream: - return "StarredStream" - case ReadingListStream: - return "ReadingListStream" - case KeptUnreadStream: - return "KeptUnreadStream" - case BroadcastStream: - return "BroadcastStream" - case BroadcastFriendsStream: - return "BroadcastFriendsStream" - case LabelStream: - return "LabelStream" - case FeedStream: - return "FeedStream" - case LikeStream: - return "LikeStream" - default: - return st.String() - } -} - -// RequestModifiers are the parsed request parameters. -type RequestModifiers struct { - ExcludeTargets []Stream - FilterTargets []Stream - Streams []Stream - Count int - Offset int - SortDirection string - StartTime int64 - StopTime int64 - ContinuationToken string - UserID int64 -} - -func (r RequestModifiers) String() string { - var results []string - - results = append(results, fmt.Sprintf("UserID: %d", r.UserID)) - - var streamStr []string - for _, s := range r.Streams { - streamStr = append(streamStr, s.String()) - } - results = append(results, fmt.Sprintf("Streams: [%s]", strings.Join(streamStr, ", "))) - - var exclusions []string - for _, s := range r.ExcludeTargets { - exclusions = append(exclusions, s.String()) - } - results = append(results, fmt.Sprintf("Exclusions: [%s]", strings.Join(exclusions, ", "))) - - var filters []string - for _, s := range r.FilterTargets { - filters = append(filters, s.String()) - } - results = append(results, fmt.Sprintf("Filters: [%s]", strings.Join(filters, ", "))) - - results = append(results, fmt.Sprintf("Count: %d", r.Count)) - results = append(results, fmt.Sprintf("Offset: %d", r.Offset)) - results = append(results, fmt.Sprintf("Sort Direction: %s", r.SortDirection)) - results = append(results, fmt.Sprintf("Continuation Token: %s", r.ContinuationToken)) - results = append(results, fmt.Sprintf("Start Time: %d", r.StartTime)) - results = append(results, fmt.Sprintf("Stop Time: %d", r.StopTime)) - - return strings.Join(results, "; ") -} - -// Serve handles Google Reader API calls. -func Serve(router *mux.Router, store *storage.Storage) { - handler := &handler{store, router} - router.HandleFunc("/accounts/ClientLogin", handler.clientLoginHandler).Methods(http.MethodPost).Name("ClientLogin") - - middleware := newMiddleware(store) - sr := router.PathPrefix("/reader/api/0").Subrouter() - sr.Use(middleware.handleCORS) - sr.Use(middleware.apiKeyAuth) - sr.Methods(http.MethodOptions) - sr.HandleFunc("/token", handler.tokenHandler).Methods(http.MethodGet).Name("Token") - sr.HandleFunc("/edit-tag", handler.editTagHandler).Methods(http.MethodPost).Name("EditTag") - sr.HandleFunc("/rename-tag", handler.renameTagHandler).Methods(http.MethodPost).Name("Rename Tag") - sr.HandleFunc("/disable-tag", handler.disableTagHandler).Methods(http.MethodPost).Name("Disable Tag") - sr.HandleFunc("/tag/list", handler.tagListHandler).Methods(http.MethodGet).Name("TagList") - sr.HandleFunc("/user-info", handler.userInfoHandler).Methods(http.MethodGet).Name("UserInfo") - sr.HandleFunc("/subscription/list", handler.subscriptionListHandler).Methods(http.MethodGet).Name("SubscriptonList") - sr.HandleFunc("/subscription/edit", handler.editSubscriptionHandler).Methods(http.MethodPost).Name("SubscriptionEdit") - sr.HandleFunc("/subscription/quickadd", handler.quickAddHandler).Methods(http.MethodPost).Name("QuickAdd") - sr.HandleFunc("/stream/items/ids", handler.streamItemIDsHandler).Methods(http.MethodGet).Name("StreamItemIDs") - sr.HandleFunc("/stream/items/contents", handler.streamItemContentsHandler).Methods(http.MethodPost).Name("StreamItemsContents") - sr.PathPrefix("/").HandlerFunc(handler.serveHandler).Methods(http.MethodPost, http.MethodGet).Name("GoogleReaderApiEndpoint") -} - -func getStreamFilterModifiers(r *http.Request) (RequestModifiers, error) { - userID := request.UserID(r) - - result := RequestModifiers{ - SortDirection: "desc", - UserID: userID, - } - streamOrder := request.QueryStringParam(r, ParamStreamOrder, "d") - if streamOrder == "o" { - result.SortDirection = "asc" - } - var err error - result.Streams, err = getStreams(request.QueryStringParamList(r, ParamStreamID), userID) - if err != nil { - return RequestModifiers{}, err - } - result.ExcludeTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamExcludes), userID) - if err != nil { - return RequestModifiers{}, err - } - - result.FilterTargets, err = getStreams(request.QueryStringParamList(r, ParamStreamFilters), userID) - if err != nil { - return RequestModifiers{}, err - } - - result.Count = request.QueryIntParam(r, ParamStreamMaxItems, 0) - result.Offset = request.QueryIntParam(r, ParamContinuation, 0) - result.StartTime = request.QueryInt64Param(r, ParamStreamStartTime, int64(0)) - result.StopTime = request.QueryInt64Param(r, ParamStreamStopTime, int64(0)) - return result, nil -} - -func getStream(streamID string, userID int64) (Stream, error) { - switch { - case strings.HasPrefix(streamID, FeedPrefix): - return Stream{Type: FeedStream, ID: strings.TrimPrefix(streamID, FeedPrefix)}, nil - case strings.HasPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) || strings.HasPrefix(streamID, StreamPrefix): - id := strings.TrimPrefix(streamID, fmt.Sprintf(UserStreamPrefix, userID)) - id = strings.TrimPrefix(id, StreamPrefix) - switch id { - case Read: - return Stream{ReadStream, ""}, nil - case Starred: - return Stream{StarredStream, ""}, nil - case ReadingList: - return Stream{ReadingListStream, ""}, nil - case KeptUnread: - return Stream{KeptUnreadStream, ""}, nil - case Broadcast: - return Stream{BroadcastStream, ""}, nil - case BroadcastFriends: - return Stream{BroadcastFriendsStream, ""}, nil - case Like: - return Stream{LikeStream, ""}, nil - default: - return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream with id: %s", id) - } - case strings.HasPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) || strings.HasPrefix(streamID, LabelPrefix): - id := strings.TrimPrefix(streamID, fmt.Sprintf(UserLabelPrefix, userID)) - id = strings.TrimPrefix(id, LabelPrefix) - return Stream{LabelStream, id}, nil - case streamID == "": - return Stream{NoStream, ""}, nil - default: - return Stream{NoStream, ""}, fmt.Errorf("googlereader: unknown stream type: %s", streamID) - } -} - -func getStreams(streamIDs []string, userID int64) ([]Stream, error) { - streams := make([]Stream, 0) - for _, streamID := range streamIDs { - stream, err := getStream(streamID, userID) - if err != nil { - return []Stream{}, err - } - streams = append(streams, stream) - } - return streams, nil -} - -func checkAndSimplifyTags(addTags []Stream, removeTags []Stream) (map[StreamType]bool, error) { - tags := make(map[StreamType]bool) - for _, s := range addTags { - switch s.Type { - case ReadStream: - if _, ok := tags[KeptUnreadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) - } - tags[ReadStream] = true - case KeptUnreadStream: - if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) - } - tags[ReadStream] = false - case StarredStream: - tags[StarredStream] = true - case BroadcastStream, LikeStream: - slog.Debug("Broadcast & Like tags are not implemented!") - default: - return nil, fmt.Errorf("googlereader: unsupported tag type: %s", s.Type) - } - } - for _, s := range removeTags { - switch s.Type { - case ReadStream: - if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) - } - tags[ReadStream] = false - case KeptUnreadStream: - if _, ok := tags[ReadStream]; ok { - return nil, fmt.Errorf("googlereader: %s ad %s should not be supplied simultaneously", KeptUnread, Read) - } - tags[ReadStream] = true - case StarredStream: - if _, ok := tags[StarredStream]; ok { - return nil, fmt.Errorf("googlereader: %s should not be supplied for add and remove simultaneously", Starred) - } - tags[StarredStream] = false - case BroadcastStream, LikeStream: - slog.Debug("Broadcast & Like tags are not implemented!") - default: - return nil, fmt.Errorf("googlereader: unsupported tag type: %s", s.Type) - } - } - - return tags, nil -} - -func getItemIDs(r *http.Request) ([]int64, error) { - items := r.Form[ParamItemIDs] - if len(items) == 0 { - return nil, fmt.Errorf("googlereader: no items requested") - } - - itemIDs := make([]int64, len(items)) - - for i, item := range items { - var itemID int64 - _, err := fmt.Sscanf(item, EntryIDLong, &itemID) - if err != nil { - itemID, err = strconv.ParseInt(item, 16, 64) - if err != nil { - return nil, fmt.Errorf("googlereader: could not parse item: %v", item) - } - } - itemIDs[i] = itemID - } - return itemIDs, nil -} - -func checkOutputFormat(r *http.Request) error { - var output string - if r.Method == http.MethodPost { - err := r.ParseForm() - if err != nil { - return err - } - output = r.Form.Get("output") - } else { - output = request.QueryStringParam(r, "output", "") - } - if output != "json" { - err := fmt.Errorf("googlereader: only json output is supported") - return err - } - return nil -} - -func (h *handler) clientLoginHandler(w http.ResponseWriter, r *http.Request) { - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /accounts/ClientLogin", - slog.String("handler", "clientLoginHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - if err := r.ParseForm(); err != nil { - slog.Warn("[GoogleReader] Could not parse request form data", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("error", err), - ) - json.Unauthorized(w, r) - return - } - - username := r.Form.Get("Email") - password := r.Form.Get("Passwd") - output := r.Form.Get("output") - - if username == "" || password == "" { - slog.Warn("[GoogleReader] Empty username or password", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - json.Unauthorized(w, r) - return - } - - if err := h.store.GoogleReaderUserCheckPassword(username, password); err != nil { - slog.Warn("[GoogleReader] Invalid username or password", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.String("username", username), - slog.Any("error", err), - ) - json.Unauthorized(w, r) - return - } - - slog.Info("[GoogleReader] User authenticated successfully", - slog.Bool("authentication_successful", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.String("username", username), - ) - - integration, err := h.store.GoogleReaderUserGetIntegration(username) - if err != nil { - json.ServerError(w, r, err) - return - } - - h.store.SetLastLogin(integration.UserID) - - token := getAuthToken(integration.GoogleReaderUsername, integration.GoogleReaderPassword) - slog.Debug("[GoogleReader] Created token", - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.String("username", username), - ) - - result := login{SID: token, LSID: token, Auth: token} - if output == "json" { - json.OK(w, r, result) - return - } - - builder := response.New(w, r) - builder.WithHeader("Content-Type", "text/plain; charset=UTF-8") - builder.WithBody(result.String()) - builder.Write() -} - -func (h *handler) tokenHandler(w http.ResponseWriter, r *http.Request) { - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /token", - slog.String("handler", "tokenHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - if !request.IsAuthenticated(r) { - slog.Warn("[GoogleReader] User is not authenticated", - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - json.Unauthorized(w, r) - return - } - - token := request.GoolgeReaderToken(r) - if token == "" { - slog.Warn("[GoogleReader] User does not have token", - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", request.UserID(r)), - ) - json.Unauthorized(w, r) - return - } - - slog.Debug("[GoogleReader] Token handler", - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", request.UserID(r)), - slog.String("token", token), - ) - - w.Header().Add("Content-Type", "text/plain; charset=UTF-8") - w.WriteHeader(http.StatusOK) - w.Write([]byte(token)) -} - -func (h *handler) editTagHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /edit-tag", - slog.String("handler", "editTagHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - if err := r.ParseForm(); err != nil { - json.ServerError(w, r, err) - return - } - - addTags, err := getStreams(r.PostForm[ParamTagsAdd], userID) - if err != nil { - json.ServerError(w, r, err) - return - } - removeTags, err := getStreams(r.PostForm[ParamTagsRemove], userID) - if err != nil { - json.ServerError(w, r, err) - return - } - if len(addTags) == 0 && len(removeTags) == 0 { - err = fmt.Errorf("googlreader: add or/and remove tags should be supplied") - json.ServerError(w, r, err) - return - } - tags, err := checkAndSimplifyTags(addTags, removeTags) - if err != nil { - json.ServerError(w, r, err) - return - } - - itemIDs, err := getItemIDs(r) - if err != nil { - json.ServerError(w, r, err) - return - } - - slog.Debug("[GoogleReader] Edited tags", - slog.String("handler", "editTagHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - slog.Any("item_ids", itemIDs), - slog.Any("tags", tags), - ) - - builder := h.store.NewEntryQueryBuilder(userID) - builder.WithEntryIDs(itemIDs) - builder.WithoutStatus(model.EntryStatusRemoved) - - entries, err := builder.GetEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - - n := 0 - readEntryIDs := make([]int64, 0) - unreadEntryIDs := make([]int64, 0) - starredEntryIDs := make([]int64, 0) - unstarredEntryIDs := make([]int64, 0) - for _, entry := range entries { - if read, exists := tags[ReadStream]; exists { - if read && entry.Status == model.EntryStatusUnread { - readEntryIDs = append(readEntryIDs, entry.ID) - } else if entry.Status == model.EntryStatusRead { - unreadEntryIDs = append(unreadEntryIDs, entry.ID) - } - } - if starred, exists := tags[StarredStream]; exists { - if starred && !entry.Starred { - starredEntryIDs = append(starredEntryIDs, entry.ID) - // filter the original array - entries[n] = entry - n++ - } else if entry.Starred { - unstarredEntryIDs = append(unstarredEntryIDs, entry.ID) - } - } - } - entries = entries[:n] - if len(readEntryIDs) > 0 { - err = h.store.SetEntriesStatus(userID, readEntryIDs, model.EntryStatusRead) - if err != nil { - json.ServerError(w, r, err) - return - } - } - - if len(unreadEntryIDs) > 0 { - err = h.store.SetEntriesStatus(userID, unreadEntryIDs, model.EntryStatusUnread) - if err != nil { - json.ServerError(w, r, err) - return - } - } - - if len(unstarredEntryIDs) > 0 { - err = h.store.SetEntriesBookmarkedState(userID, unstarredEntryIDs, false) - if err != nil { - json.ServerError(w, r, err) - return - } - } - - if len(starredEntryIDs) > 0 { - err = h.store.SetEntriesBookmarkedState(userID, starredEntryIDs, true) - if err != nil { - json.ServerError(w, r, err) - return - } - } - - if len(entries) > 0 { - settings, err := h.store.Integration(userID) - if err != nil { - json.ServerError(w, r, err) - return - } - - for _, entry := range entries { - e := entry - go func() { - integration.SendEntry(e, settings) - }() - } - } - - OK(w, r) -} - -func (h *handler) quickAddHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /subscription/quickadd", - slog.String("handler", "quickAddHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - err := r.ParseForm() - if err != nil { - json.BadRequest(w, r, err) - return - } - - feedURL := r.Form.Get(ParamQuickAdd) - if !validator.IsValidURL(feedURL) { - json.BadRequest(w, r, fmt.Errorf("googlereader: invalid URL: %s", feedURL)) - return - } - - requestBuilder := fetcher.NewRequestBuilder() - requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout()) - requestBuilder.WithProxy(config.Opts.HTTPClientProxy()) - - var rssBridgeURL string - if intg, err := h.store.Integration(userID); err == nil && intg != nil && intg.RSSBridgeEnabled { - rssBridgeURL = intg.RSSBridgeURL - } - - subscriptions, localizedError := mfs.NewSubscriptionFinder(requestBuilder).FindSubscriptions(feedURL, rssBridgeURL) - if localizedError != nil { - json.ServerError(w, r, localizedError.Error()) - return - } - - if len(subscriptions) == 0 { - json.OK(w, r, quickAddResponse{ - NumResults: 0, - }) - return - } - - toSubscribe := Stream{FeedStream, subscriptions[0].URL} - category := Stream{NoStream, ""} - newFeed, err := subscribe(toSubscribe, category, "", h.store, userID) - if err != nil { - json.ServerError(w, r, err) - return - } - - slog.Debug("[GoogleReader] Added a new feed", - slog.String("handler", "quickAddHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - slog.String("feed_url", newFeed.FeedURL), - ) - - json.OK(w, r, quickAddResponse{ - NumResults: 1, - Query: newFeed.FeedURL, - StreamID: fmt.Sprintf(FeedPrefix+"%d", newFeed.ID), - StreamName: newFeed.Title, - }) -} - -func getFeed(stream Stream, store *storage.Storage, userID int64) (*model.Feed, error) { - feedID, err := strconv.ParseInt(stream.ID, 10, 64) - if err != nil { - return nil, err - } - return store.FeedByID(userID, feedID) -} - -func getOrCreateCategory(category Stream, store *storage.Storage, userID int64) (*model.Category, error) { - switch { - case category.ID == "": - return store.FirstCategory(userID) - case store.CategoryTitleExists(userID, category.ID): - return store.CategoryByTitle(userID, category.ID) - default: - catRequest := model.CategoryRequest{ - Title: category.ID, - } - return store.CreateCategory(userID, &catRequest) - } -} - -func subscribe(newFeed Stream, category Stream, title string, store *storage.Storage, userID int64) (*model.Feed, error) { - destCategory, err := getOrCreateCategory(category, store, userID) - if err != nil { - return nil, err - } - - feedRequest := model.FeedCreationRequest{ - FeedURL: newFeed.ID, - CategoryID: destCategory.ID, - } - verr := validator.ValidateFeedCreation(store, userID, &feedRequest) - if verr != nil { - return nil, verr.Error() - } - - created, localizedError := mff.CreateFeed(store, userID, &feedRequest) - if localizedError != nil { - return nil, localizedError.Error() - } - - if title != "" { - feedModification := model.FeedModificationRequest{ - Title: &title, - } - feedModification.Patch(created) - if err := store.UpdateFeed(created); err != nil { - return nil, err - } - } - - return created, nil -} - -func unsubscribe(streams []Stream, store *storage.Storage, userID int64) error { - for _, stream := range streams { - feedID, err := strconv.ParseInt(stream.ID, 10, 64) - if err != nil { - return err - } - err = store.RemoveFeed(userID, feedID) - if err != nil { - return err - } - } - return nil -} - -func rename(stream Stream, title string, store *storage.Storage, userID int64) error { - if title == "" { - return errors.New("empty title") - } - feed, err := getFeed(stream, store, userID) - if err != nil { - return err - } - feedModification := model.FeedModificationRequest{ - Title: &title, - } - feedModification.Patch(feed) - return store.UpdateFeed(feed) -} - -func move(stream Stream, destination Stream, store *storage.Storage, userID int64) error { - feed, err := getFeed(stream, store, userID) - if err != nil { - return err - } - category, err := getOrCreateCategory(destination, store, userID) - if err != nil { - return err - } - feedModification := model.FeedModificationRequest{ - CategoryID: &category.ID, - } - feedModification.Patch(feed) - return store.UpdateFeed(feed) -} - -func (h *handler) editSubscriptionHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /subscription/edit", - slog.String("handler", "editSubscriptionHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - if err := r.ParseForm(); err != nil { - json.BadRequest(w, r, err) - return - } - - streamIds, err := getStreams(r.Form[ParamStreamID], userID) - if err != nil || len(streamIds) == 0 { - json.BadRequest(w, r, errors.New("googlereader: no valid stream IDs provided")) - return - } - - newLabel, err := getStream(r.Form.Get(ParamTagsAdd), userID) - if err != nil { - json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamTagsAdd)) - return - } - - title := r.Form.Get(ParamTitle) - action := r.Form.Get(ParamSubscribeAction) - - switch action { - case "subscribe": - _, err := subscribe(streamIds[0], newLabel, title, h.store, userID) - if err != nil { - json.ServerError(w, r, err) - return - } - case "unsubscribe": - err := unsubscribe(streamIds, h.store, userID) - if err != nil { - json.ServerError(w, r, err) - return - } - case "edit": - if title != "" { - if err := rename(streamIds[0], title, h.store, userID); err != nil { - json.ServerError(w, r, err) - return - } - } - - if r.Form.Has(ParamTagsAdd) { - if newLabel.Type != LabelStream { - json.BadRequest(w, r, errors.New("destination must be a label")) - return - } - - if err := move(streamIds[0], newLabel, h.store, userID); err != nil { - json.ServerError(w, r, err) - return - } - } - default: - json.ServerError(w, r, fmt.Errorf("googlereader: unrecognized action %s", action)) - return - } - - OK(w, r) -} - -func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /stream/items/contents", - slog.String("handler", "streamItemContentsHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - if err := checkOutputFormat(r); err != nil { - json.BadRequest(w, r, err) - return - } - - err := r.ParseForm() - if err != nil { - json.ServerError(w, r, err) - return - } - var user *model.User - if user, err = h.store.UserByID(userID); err != nil { - json.ServerError(w, r, err) - return - } - - requestModifiers, err := getStreamFilterModifiers(r) - if err != nil { - json.ServerError(w, r, err) - return - } - - userReadingList := fmt.Sprintf(UserStreamPrefix, userID) + ReadingList - userRead := fmt.Sprintf(UserStreamPrefix, userID) + Read - userStarred := fmt.Sprintf(UserStreamPrefix, userID) + Starred - - itemIDs, err := getItemIDs(r) - if err != nil { - json.ServerError(w, r, err) - return - } - - slog.Debug("[GoogleReader] Fetching item contents", - slog.String("handler", "streamItemContentsHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - slog.Any("item_ids", itemIDs), - ) - - builder := h.store.NewEntryQueryBuilder(userID) - builder.WithoutStatus(model.EntryStatusRemoved) - builder.WithEntryIDs(itemIDs) - builder.WithSorting(model.DefaultSortingOrder, requestModifiers.SortDirection) - - entries, err := builder.GetEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - - if len(entries) == 0 { - json.BadRequest(w, r, fmt.Errorf("googlereader: no items returned from the database for item IDs: %v", itemIDs)) - return - } - - result := streamContentItems{ - Direction: "ltr", - ID: fmt.Sprintf("feed/%d", entries[0].FeedID), - Title: entries[0].Feed.Title, - Alternate: []contentHREFType{ - { - HREF: entries[0].Feed.SiteURL, - Type: "text/html", - }, - }, - Updated: time.Now().Unix(), - Self: []contentHREF{ - { - HREF: config.Opts.RootURL() + route.Path(h.router, "StreamItemsContents"), - }, - }, - Author: user.Username, - } - contentItems := make([]contentItem, len(entries)) - for i, entry := range entries { - enclosures := make([]contentItemEnclosure, 0, len(entry.Enclosures)) - for _, enclosure := range entry.Enclosures { - enclosures = append(enclosures, contentItemEnclosure{URL: enclosure.URL, Type: enclosure.MimeType}) - } - categories := make([]string, 0) - categories = append(categories, userReadingList) - if entry.Feed.Category.Title != "" { - categories = append(categories, fmt.Sprintf(UserLabelPrefix, userID)+entry.Feed.Category.Title) - } - if entry.Status == model.EntryStatusRead { - categories = append(categories, userRead) - } - - if entry.Starred { - categories = append(categories, userStarred) - } - - entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content) - - entry.Enclosures.ProxifyEnclosureURL(h.router) - - contentItems[i] = contentItem{ - ID: fmt.Sprintf(EntryIDLong, entry.ID), - Title: entry.Title, - Author: entry.Author, - TimestampUsec: fmt.Sprintf("%d", entry.Date.UnixMicro()), - CrawlTimeMsec: fmt.Sprintf("%d", entry.CreatedAt.UnixMilli()), - Published: entry.Date.Unix(), - Updated: entry.ChangedAt.Unix(), - Categories: categories, - Canonical: []contentHREF{ - { - HREF: entry.URL, - }, - }, - Alternate: []contentHREFType{ - { - HREF: entry.URL, - Type: "text/html", - }, - }, - Content: contentItemContent{ - Direction: "ltr", - Content: entry.Content, - }, - Summary: contentItemContent{ - Direction: "ltr", - Content: entry.Content, - }, - Origin: contentItemOrigin{ - StreamID: fmt.Sprintf("feed/%d", entry.FeedID), - Title: entry.Feed.Title, - HTMLUrl: entry.Feed.SiteURL, - }, - Enclosure: enclosures, - } - } - result.Items = contentItems - json.OK(w, r, result) -} - -func (h *handler) disableTagHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /disable-tags", - slog.String("handler", "disableTagHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - err := r.ParseForm() - if err != nil { - json.BadRequest(w, r, err) - return - } - - streams, err := getStreams(r.Form[ParamStreamID], userID) - if err != nil { - json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamStreamID)) - return - } - - titles := make([]string, len(streams)) - for i, stream := range streams { - if stream.Type != LabelStream { - json.BadRequest(w, r, errors.New("googlereader: only labels are supported")) - return - } - titles[i] = stream.ID - } - - err = h.store.RemoveAndReplaceCategoriesByName(userID, titles) - if err != nil { - json.ServerError(w, r, err) - return - } - - OK(w, r) -} - -func (h *handler) renameTagHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /rename-tag", - slog.String("handler", "renameTagHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - err := r.ParseForm() - if err != nil { - json.BadRequest(w, r, err) - return - } - - source, err := getStream(r.Form.Get(ParamStreamID), userID) - if err != nil { - json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamStreamID)) - return - } - - destination, err := getStream(r.Form.Get(ParamDestination), userID) - if err != nil { - json.BadRequest(w, r, fmt.Errorf("googlereader: invalid data in %s", ParamDestination)) - return - } - - if source.Type != LabelStream || destination.Type != LabelStream { - json.BadRequest(w, r, errors.New("googlereader: only labels supported")) - return - } - - if destination.ID == "" { - json.BadRequest(w, r, errors.New("googlereader: empty destination name")) - return - } - - category, err := h.store.CategoryByTitle(userID, source.ID) - if err != nil { - json.ServerError(w, r, err) - return - } - if category == nil { - json.NotFound(w, r) - return - } - categoryRequest := model.CategoryRequest{ - Title: destination.ID, - } - verr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest) - if verr != nil { - json.BadRequest(w, r, verr.Error()) - return - } - categoryRequest.Patch(category) - err = h.store.UpdateCategory(category) - if err != nil { - json.ServerError(w, r, err) - return - } - OK(w, r) -} - -func (h *handler) tagListHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /tags/list", - slog.String("handler", "tagListHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - if err := checkOutputFormat(r); err != nil { - json.BadRequest(w, r, err) - return - } - - var result tagsResponse - categories, err := h.store.Categories(userID) - if err != nil { - json.ServerError(w, r, err) - return - } - result.Tags = make([]subscriptionCategory, 0) - result.Tags = append(result.Tags, subscriptionCategory{ - ID: fmt.Sprintf(UserStreamPrefix, userID) + Starred, - }) - for _, category := range categories { - result.Tags = append(result.Tags, subscriptionCategory{ - ID: fmt.Sprintf(UserLabelPrefix, userID) + category.Title, - Label: category.Title, - Type: "folder", - }) - } - json.OK(w, r, result) -} - -func (h *handler) subscriptionListHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /subscription/list", - slog.String("handler", "subscriptionListHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - if err := checkOutputFormat(r); err != nil { - json.BadRequest(w, r, err) - return - } - - var result subscriptionsResponse - feeds, err := h.store.Feeds(userID) - if err != nil { - json.ServerError(w, r, err) - return - } - result.Subscriptions = make([]subscription, 0) - for _, feed := range feeds { - result.Subscriptions = append(result.Subscriptions, subscription{ - ID: fmt.Sprintf(FeedPrefix+"%d", feed.ID), - Title: feed.Title, - URL: feed.FeedURL, - Categories: []subscriptionCategory{{fmt.Sprintf(UserLabelPrefix, userID) + feed.Category.Title, feed.Category.Title, "folder"}}, - HTMLURL: feed.SiteURL, - IconURL: "", // TODO: Icons are base64 encoded in the DB. - }) - } - json.OK(w, r, result) -} - -func (h *handler) serveHandler(w http.ResponseWriter, r *http.Request) { - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] API endpoint not implemented yet", - slog.Any("url", r.RequestURI), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - json.OK(w, r, []string{}) -} - -func (h *handler) userInfoHandler(w http.ResponseWriter, r *http.Request) { - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /user-info", - slog.String("handler", "userInfoHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - if err := checkOutputFormat(r); err != nil { - json.BadRequest(w, r, err) - return - } - - user, err := h.store.UserByID(request.UserID(r)) - if err != nil { - json.ServerError(w, r, err) - return - } - userInfo := userInfo{UserID: fmt.Sprint(user.ID), UserName: user.Username, UserProfileID: fmt.Sprint(user.ID), UserEmail: user.Username} - json.OK(w, r, userInfo) -} - -func (h *handler) streamItemIDsHandler(w http.ResponseWriter, r *http.Request) { - userID := request.UserID(r) - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle /stream/items/ids", - slog.String("handler", "streamItemIDsHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", userID), - ) - - if err := checkOutputFormat(r); err != nil { - json.BadRequest(w, r, err) - return - } - - rm, err := getStreamFilterModifiers(r) - if err != nil { - json.ServerError(w, r, err) - return - } - - slog.Debug("[GoogleReader] Request Modifiers", - slog.String("handler", "streamItemIDsHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("modifiers", rm), - ) - - if len(rm.Streams) != 1 { - json.ServerError(w, r, fmt.Errorf("googlereader: only one stream type expected")) - return - } - switch rm.Streams[0].Type { - case ReadingListStream: - h.handleReadingListStreamHandler(w, r, rm) - case StarredStream: - h.handleStarredStreamHandler(w, r, rm) - case ReadStream: - h.handleReadStreamHandler(w, r, rm) - case FeedStream: - h.handleFeedStreamHandler(w, r, rm) - default: - slog.Warn("[GoogleReader] Unknown Stream", - slog.String("handler", "streamItemIDsHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("stream_type", rm.Streams[0].Type), - ) - json.ServerError(w, r, fmt.Errorf("googlereader: unknown stream type %s", rm.Streams[0].Type)) - } -} - -func (h *handler) handleReadingListStreamHandler(w http.ResponseWriter, r *http.Request, rm RequestModifiers) { - clientIP := request.ClientIP(r) - - slog.Debug("[GoogleReader] Handle ReadingListStream", - slog.String("handler", "handleReadingListStreamHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - - builder := h.store.NewEntryQueryBuilder(rm.UserID) - for _, s := range rm.ExcludeTargets { - switch s.Type { - case ReadStream: - builder.WithStatus(model.EntryStatusUnread) - default: - slog.Warn("[GoogleReader] Unknown ExcludeTargets filter type", - slog.String("handler", "handleReadingListStreamHandler"), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("filter_type", s.Type), - ) - } - } - - builder.WithoutStatus(model.EntryStatusRemoved) - builder.WithLimit(rm.Count) - builder.WithOffset(rm.Offset) - builder.WithSorting(model.DefaultSortingOrder, rm.SortDirection) - if rm.StartTime > 0 { - builder.AfterPublishedDate(time.Unix(rm.StartTime, 0)) - } - if rm.StopTime > 0 { - builder.BeforePublishedDate(time.Unix(rm.StopTime, 0)) - } - - rawEntryIDs, err := builder.GetEntryIDs() - if err != nil { - json.ServerError(w, r, err) - return - } - var itemRefs = make([]itemRef, 0) - for _, entryID := range rawEntryIDs { - formattedID := strconv.FormatInt(entryID, 10) - itemRefs = append(itemRefs, itemRef{ID: formattedID}) - } - - totalEntries, err := builder.CountEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - continuation := 0 - if len(itemRefs)+rm.Offset < totalEntries { - continuation = len(itemRefs) + rm.Offset - } - - json.OK(w, r, streamIDResponse{itemRefs, continuation}) -} - -func (h *handler) handleStarredStreamHandler(w http.ResponseWriter, r *http.Request, rm RequestModifiers) { - builder := h.store.NewEntryQueryBuilder(rm.UserID) - builder.WithoutStatus(model.EntryStatusRemoved) - builder.WithStarred(true) - builder.WithLimit(rm.Count) - builder.WithOffset(rm.Offset) - builder.WithSorting(model.DefaultSortingOrder, rm.SortDirection) - if rm.StartTime > 0 { - builder.AfterPublishedDate(time.Unix(rm.StartTime, 0)) - } - if rm.StopTime > 0 { - builder.BeforePublishedDate(time.Unix(rm.StopTime, 0)) - } - - rawEntryIDs, err := builder.GetEntryIDs() - if err != nil { - json.ServerError(w, r, err) - return - } - var itemRefs = make([]itemRef, 0) - for _, entryID := range rawEntryIDs { - formattedID := strconv.FormatInt(entryID, 10) - itemRefs = append(itemRefs, itemRef{ID: formattedID}) - } - - totalEntries, err := builder.CountEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - continuation := 0 - if len(itemRefs)+rm.Offset < totalEntries { - continuation = len(itemRefs) + rm.Offset - } - - json.OK(w, r, streamIDResponse{itemRefs, continuation}) -} - -func (h *handler) handleReadStreamHandler(w http.ResponseWriter, r *http.Request, rm RequestModifiers) { - builder := h.store.NewEntryQueryBuilder(rm.UserID) - builder.WithoutStatus(model.EntryStatusRemoved) - builder.WithStatus(model.EntryStatusRead) - builder.WithLimit(rm.Count) - builder.WithOffset(rm.Offset) - builder.WithSorting(model.DefaultSortingOrder, rm.SortDirection) - if rm.StartTime > 0 { - builder.AfterPublishedDate(time.Unix(rm.StartTime, 0)) - } - if rm.StopTime > 0 { - builder.BeforePublishedDate(time.Unix(rm.StopTime, 0)) - } - - rawEntryIDs, err := builder.GetEntryIDs() - if err != nil { - json.ServerError(w, r, err) - return - } - var itemRefs = make([]itemRef, 0) - for _, entryID := range rawEntryIDs { - formattedID := strconv.FormatInt(entryID, 10) - itemRefs = append(itemRefs, itemRef{ID: formattedID}) - } - - totalEntries, err := builder.CountEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - continuation := 0 - if len(itemRefs)+rm.Offset < totalEntries { - continuation = len(itemRefs) + rm.Offset - } - - json.OK(w, r, streamIDResponse{itemRefs, continuation}) -} - -func (h *handler) handleFeedStreamHandler(w http.ResponseWriter, r *http.Request, rm RequestModifiers) { - feedID, err := strconv.ParseInt(rm.Streams[0].ID, 10, 64) - if err != nil { - json.ServerError(w, r, err) - return - } - - builder := h.store.NewEntryQueryBuilder(rm.UserID) - builder.WithoutStatus(model.EntryStatusRemoved) - builder.WithFeedID(feedID) - builder.WithLimit(rm.Count) - builder.WithOffset(rm.Offset) - builder.WithSorting(model.DefaultSortingOrder, rm.SortDirection) - - if rm.StartTime > 0 { - builder.AfterPublishedDate(time.Unix(rm.StartTime, 0)) - } - - if rm.StopTime > 0 { - builder.BeforePublishedDate(time.Unix(rm.StopTime, 0)) - } - - if len(rm.ExcludeTargets) > 0 { - for _, s := range rm.ExcludeTargets { - if s.Type == ReadStream { - builder.WithoutStatus(model.EntryStatusRead) - } - } - } - - rawEntryIDs, err := builder.GetEntryIDs() - if err != nil { - json.ServerError(w, r, err) - return - } - - var itemRefs = make([]itemRef, 0) - for _, entryID := range rawEntryIDs { - formattedID := strconv.FormatInt(entryID, 10) - itemRefs = append(itemRefs, itemRef{ID: formattedID}) - } - - totalEntries, err := builder.CountEntries() - if err != nil { - json.ServerError(w, r, err) - return - } - - continuation := 0 - if len(itemRefs)+rm.Offset < totalEntries { - continuation = len(itemRefs) + rm.Offset - } - - json.OK(w, r, streamIDResponse{itemRefs, continuation}) -} diff --git a/internal/googlereader/middleware.go b/internal/googlereader/middleware.go deleted file mode 100644 index 280868b821e..00000000000 --- a/internal/googlereader/middleware.go +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package googlereader // import "miniflux.app/v2/internal/googlereader" - -import ( - "context" - "crypto/hmac" - "crypto/sha1" - "encoding/hex" - "log/slog" - "net/http" - "strings" - - "miniflux.app/v2/internal/http/request" - "miniflux.app/v2/internal/model" - "miniflux.app/v2/internal/storage" -) - -type middleware struct { - store *storage.Storage -} - -func newMiddleware(s *storage.Storage) *middleware { - return &middleware{s} -} - -func (m *middleware) handleCORS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Authorization") - if r.Method == http.MethodOptions { - w.WriteHeader(http.StatusOK) - return - } - next.ServeHTTP(w, r) - }) -} - -func (m *middleware) apiKeyAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - clientIP := request.ClientIP(r) - - var token string - if r.Method == http.MethodPost { - if err := r.ParseForm(); err != nil { - slog.Warn("[GoogleReader] Could not parse request form data", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("error", err), - ) - Unauthorized(w, r) - return - } - - token = r.Form.Get("T") - if token == "" { - slog.Warn("[GoogleReader] Post-Form T field is empty", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - } else { - authorization := r.Header.Get("Authorization") - - if authorization == "" { - slog.Warn("[GoogleReader] No token provided", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - fields := strings.Fields(authorization) - if len(fields) != 2 { - slog.Warn("[GoogleReader] Authorization header does not have the expected GoogleLogin format auth=xxxxxx", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - if fields[0] != "GoogleLogin" { - slog.Warn("[GoogleReader] Authorization header does not begin with GoogleLogin", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - auths := strings.Split(fields[1], "=") - if len(auths) != 2 { - slog.Warn("[GoogleReader] Authorization header does not have the expected GoogleLogin format auth=xxxxxx", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - if auths[0] != "auth" { - slog.Warn("[GoogleReader] Authorization header does not have the expected GoogleLogin format auth=xxxxxx", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - token = auths[1] - } - - parts := strings.Split(token, "/") - if len(parts) != 2 { - slog.Warn("[GoogleReader] Auth token does not have the expected structure username/hash", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.String("token", token), - ) - Unauthorized(w, r) - return - } - var integration *model.Integration - var user *model.User - var err error - if integration, err = m.store.GoogleReaderUserGetIntegration(parts[0]); err != nil { - slog.Warn("[GoogleReader] No user found with the given Google Reader username", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("error", err), - ) - Unauthorized(w, r) - return - } - expectedToken := getAuthToken(integration.GoogleReaderUsername, integration.GoogleReaderPassword) - if expectedToken != token { - slog.Warn("[GoogleReader] Token does not match", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - if user, err = m.store.UserByID(integration.UserID); err != nil { - slog.Error("[GoogleReader] Unable to fetch user from database", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Any("error", err), - ) - Unauthorized(w, r) - return - } - - if user == nil { - slog.Warn("[GoogleReader] No user found with the given Google Reader credentials", - slog.Bool("authentication_failed", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - ) - Unauthorized(w, r) - return - } - - slog.Info("[GoogleReader] User authenticated successfully", - slog.Bool("authentication_successful", true), - slog.String("client_ip", clientIP), - slog.String("user_agent", r.UserAgent()), - slog.Int64("user_id", user.ID), - slog.String("username", user.Username), - ) - - m.store.SetLastLogin(integration.UserID) - - ctx := r.Context() - ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID) - ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone) - ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin) - ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true) - ctx = context.WithValue(ctx, request.GoogleReaderToken, token) - - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func getAuthToken(username, password string) string { - token := hex.EncodeToString(hmac.New(sha1.New, []byte(username+password)).Sum(nil)) - token = username + "/" + token - return token -} diff --git a/internal/googlereader/response.go b/internal/googlereader/response.go deleted file mode 100644 index 9a6ad378fc2..00000000000 --- a/internal/googlereader/response.go +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -package googlereader // import "miniflux.app/v2/internal/googlereader" - -import ( - "fmt" - "net/http" - - "miniflux.app/v2/internal/http/response" -) - -type login struct { - SID string `json:"SID,omitempty"` - LSID string `json:"LSID,omitempty"` - Auth string `json:"Auth,omitempty"` -} - -func (l login) String() string { - return fmt.Sprintf("SID=%s\nLSID=%s\nAuth=%s\n", l.SID, l.LSID, l.Auth) -} - -type userInfo struct { - UserID string `json:"userId"` - UserName string `json:"userName"` - UserProfileID string `json:"userProfileId"` - UserEmail string `json:"userEmail"` -} - -type subscription struct { - ID string `json:"id"` - Title string `json:"title"` - Categories []subscriptionCategory `json:"categories"` - URL string `json:"url"` - HTMLURL string `json:"htmlUrl"` - IconURL string `json:"iconUrl"` -} - -type quickAddResponse struct { - NumResults int64 `json:"numResults"` - Query string `json:"query,omitempty"` - StreamID string `json:"streamId,omitempty"` - StreamName string `json:"streamName,omitempty"` -} - -type subscriptionCategory struct { - ID string `json:"id"` - Label string `json:"label,omitempty"` - Type string `json:"type,omitempty"` -} -type subscriptionsResponse struct { - Subscriptions []subscription `json:"subscriptions"` -} - -type itemRef struct { - ID string `json:"id"` - DirectStreamIDs string `json:"directStreamIds,omitempty"` - TimestampUsec string `json:"timestampUsec,omitempty"` -} - -type streamIDResponse struct { - ItemRefs []itemRef `json:"itemRefs"` - Continuation int `json:"continuation,omitempty,string"` -} - -type tagsResponse struct { - Tags []subscriptionCategory `json:"tags"` -} - -type streamContentItems struct { - Direction string `json:"direction"` - ID string `json:"id"` - Title string `json:"title"` - Self []contentHREF `json:"self"` - Alternate []contentHREFType `json:"alternate"` - Updated int64 `json:"updated"` - Items []contentItem `json:"items"` - Author string `json:"author"` -} - -type contentItem struct { - ID string `json:"id"` - Categories []string `json:"categories"` - Title string `json:"title"` - CrawlTimeMsec string `json:"crawlTimeMsec"` - TimestampUsec string `json:"timestampUsec"` - Published int64 `json:"published"` - Updated int64 `json:"updated"` - Author string `json:"author"` - Alternate []contentHREFType `json:"alternate"` - Summary contentItemContent `json:"summary"` - Content contentItemContent `json:"content"` - Origin contentItemOrigin `json:"origin"` - Enclosure []contentItemEnclosure `json:"enclosure"` - Canonical []contentHREF `json:"canonical"` -} - -type contentHREFType struct { - HREF string `json:"href"` - Type string `json:"type"` -} - -type contentHREF struct { - HREF string `json:"href"` -} - -type contentItemEnclosure struct { - URL string `json:"url"` - Type string `json:"type"` -} -type contentItemContent struct { - Direction string `json:"direction"` - Content string `json:"content"` -} - -type contentItemOrigin struct { - StreamID string `json:"streamId"` - Title string `json:"title"` - HTMLUrl string `json:"htmlUrl"` -} - -// Unauthorized sends a not authorized error to the client. -func Unauthorized(w http.ResponseWriter, r *http.Request) { - builder := response.New(w, r) - builder.WithStatus(http.StatusUnauthorized) - builder.WithHeader("Content-Type", "text/plain") - builder.WithHeader("X-Reader-Google-Bad-Token", "true") - builder.WithBody("Unauthorized") - builder.Write() -} - -// OK sends a ok response to the client. -func OK(w http.ResponseWriter, r *http.Request) { - builder := response.New(w, r) - builder.WithStatus(http.StatusOK) - builder.WithHeader("Content-Type", "text/plain") - builder.WithBody("OK") - builder.Write() -} diff --git a/internal/http/request/context.go b/internal/http/request/context.go index ba6ee40de97..926f4b9d804 100644 --- a/internal/http/request/context.go +++ b/internal/http/request/context.go @@ -31,7 +31,6 @@ const ( PocketRequestTokenContextKey LastForceRefreshContextKey ClientIPContextKey - GoogleReaderToken WebAuthnDataContextKey ) @@ -44,11 +43,6 @@ func WebAuthnSessionData(r *http.Request) *model.WebAuthnSession { return nil } -// GoolgeReaderToken returns the google reader token if it exists. -func GoolgeReaderToken(r *http.Request) string { - return getContextStringValue(r, GoogleReaderToken) -} - // IsAdminUser checks if the logged user is administrator. func IsAdminUser(r *http.Request) bool { return getContextBoolValue(r, IsAdminUserContextKey) diff --git a/internal/http/server/httpd.go b/internal/http/server/httpd.go index c7428a32f4b..9ca4dbd6f2b 100644 --- a/internal/http/server/httpd.go +++ b/internal/http/server/httpd.go @@ -17,7 +17,6 @@ import ( "miniflux.app/v2/internal/api" "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/fever" - "miniflux.app/v2/internal/googlereader" "miniflux.app/v2/internal/http/request" "miniflux.app/v2/internal/storage" "miniflux.app/v2/internal/ui" @@ -190,7 +189,6 @@ func setupHandler(store *storage.Storage, pool *worker.Pool) *mux.Router { router.Use(middleware) fever.Serve(router, store) - googlereader.Serve(router, store) api.Serve(router, store, pool) ui.Serve(router, store, pool) diff --git a/internal/model/integration.go b/internal/model/integration.go index 5b8d70abcba..fb7ede5ce17 100644 --- a/internal/model/integration.go +++ b/internal/model/integration.go @@ -19,9 +19,6 @@ type Integration struct { FeverEnabled bool FeverUsername string FeverToken string - GoogleReaderEnabled bool - GoogleReaderUsername string - GoogleReaderPassword string WallabagEnabled bool WallabagOnlyURL bool WallabagURL string diff --git a/internal/storage/integration.go b/internal/storage/integration.go index dea3369d1bd..0192b773864 100644 --- a/internal/storage/integration.go +++ b/internal/storage/integration.go @@ -19,14 +19,6 @@ func (s *Storage) HasDuplicateFeverUsername(userID int64, feverUsername string) return result } -// HasDuplicateGoogleReaderUsername checks if another user have the same Google Reader username. -func (s *Storage) HasDuplicateGoogleReaderUsername(userID int64, googleReaderUsername string) bool { - query := `SELECT true FROM integrations WHERE user_id != $1 AND googlereader_username=$2` - var result bool - s.db.QueryRow(query, userID, googleReaderUsername).Scan(&result) - return result -} - // UserByFeverToken returns a user by using the Fever API token. func (s *Storage) UserByFeverToken(token string) (*model.User, error) { query := ` @@ -52,59 +44,6 @@ func (s *Storage) UserByFeverToken(token string) (*model.User, error) { } } -// GoogleReaderUserCheckPassword validates the Google Reader hashed password. -func (s *Storage) GoogleReaderUserCheckPassword(username, password string) error { - var hash string - - query := ` - SELECT - googlereader_password - FROM - integrations - WHERE - integrations.googlereader_enabled='t' AND integrations.googlereader_username=$1 - ` - - err := s.db.QueryRow(query, username).Scan(&hash) - if err == sql.ErrNoRows { - return fmt.Errorf(`store: unable to find this user: %s`, username) - } else if err != nil { - return fmt.Errorf(`store: unable to fetch user: %v`, err) - } - - if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil { - return fmt.Errorf(`store: invalid password for "%s" (%v)`, username, err) - } - - return nil -} - -// GoogleReaderUserGetIntegration returns part of the Google Reader parts of the integration struct. -func (s *Storage) GoogleReaderUserGetIntegration(username string) (*model.Integration, error) { - var integration model.Integration - - query := ` - SELECT - user_id, - googlereader_enabled, - googlereader_username, - googlereader_password - FROM - integrations - WHERE - integrations.googlereader_enabled='t' AND integrations.googlereader_username=$1 - ` - - err := s.db.QueryRow(query, username).Scan(&integration.UserID, &integration.GoogleReaderEnabled, &integration.GoogleReaderUsername, &integration.GoogleReaderPassword) - if err == sql.ErrNoRows { - return &integration, fmt.Errorf(`store: unable to find this user: %s`, username) - } else if err != nil { - return &integration, fmt.Errorf(`store: unable to fetch user: %v`, err) - } - - return &integration, nil -} - // Integration returns user integration settings. func (s *Storage) Integration(userID int64) (*model.Integration, error) { query := ` @@ -120,9 +59,6 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { fever_enabled, fever_username, fever_token, - googlereader_enabled, - googlereader_username, - googlereader_password, wallabag_enabled, wallabag_only_url, wallabag_url, @@ -228,9 +164,6 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) { &integration.FeverEnabled, &integration.FeverUsername, &integration.FeverToken, - &integration.GoogleReaderEnabled, - &integration.GoogleReaderUsername, - &integration.GoogleReaderPassword, &integration.WallabagEnabled, &integration.WallabagOnlyURL, &integration.WallabagURL, @@ -358,9 +291,6 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { pocket_enabled=$21, pocket_access_token=$22, pocket_consumer_key=$23, - googlereader_enabled=$24, - googlereader_username=$25, - googlereader_password=$26, telegram_bot_enabled=$27, telegram_bot_token=$28, telegram_bot_chat_id=$29, @@ -463,9 +393,6 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error { integration.PocketEnabled, integration.PocketAccessToken, integration.PocketConsumerKey, - integration.GoogleReaderEnabled, - integration.GoogleReaderUsername, - integration.GoogleReaderPassword, integration.TelegramBotEnabled, integration.TelegramBotToken, integration.TelegramBotChatID, diff --git a/internal/template/templates/views/integrations.html b/internal/template/templates/views/integrations.html index 469abcd5e87..43d9e09af1e 100644 --- a/internal/template/templates/views/integrations.html +++ b/internal/template/templates/views/integrations.html @@ -116,27 +116,6 @@

{{ t "page.integrations.title" }}

-
- Google Reader -
- - - - - - - - -

{{ t "form.integration.googlereader_endpoint" }} {{ rootURL }}{{ route "login" }}

- -
- -
-
-
-
Instapaper
diff --git a/internal/ui/form/integration.go b/internal/ui/form/integration.go index 0852123be6a..04efeabc083 100644 --- a/internal/ui/form/integration.go +++ b/internal/ui/form/integration.go @@ -22,9 +22,6 @@ type IntegrationForm struct { FeverEnabled bool FeverUsername string FeverPassword string - GoogleReaderEnabled bool - GoogleReaderUsername string - GoogleReaderPassword string WallabagEnabled bool WallabagOnlyURL bool WallabagURL string @@ -125,8 +122,6 @@ func (i IntegrationForm) Merge(integration *model.Integration) { integration.InstapaperPassword = i.InstapaperPassword integration.FeverEnabled = i.FeverEnabled integration.FeverUsername = i.FeverUsername - integration.GoogleReaderEnabled = i.GoogleReaderEnabled - integration.GoogleReaderUsername = i.GoogleReaderUsername integration.WallabagEnabled = i.WallabagEnabled integration.WallabagOnlyURL = i.WallabagOnlyURL integration.WallabagURL = i.WallabagURL @@ -228,9 +223,6 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm { FeverEnabled: r.FormValue("fever_enabled") == "1", FeverUsername: r.FormValue("fever_username"), FeverPassword: r.FormValue("fever_password"), - GoogleReaderEnabled: r.FormValue("googlereader_enabled") == "1", - GoogleReaderUsername: r.FormValue("googlereader_username"), - GoogleReaderPassword: r.FormValue("googlereader_password"), WallabagEnabled: r.FormValue("wallabag_enabled") == "1", WallabagOnlyURL: r.FormValue("wallabag_only_url") == "1", WallabagURL: r.FormValue("wallabag_url"), diff --git a/internal/ui/integration_show.go b/internal/ui/integration_show.go index 1395fab263c..dedb82b7d8e 100644 --- a/internal/ui/integration_show.go +++ b/internal/ui/integration_show.go @@ -37,8 +37,6 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) { InstapaperPassword: integration.InstapaperPassword, FeverEnabled: integration.FeverEnabled, FeverUsername: integration.FeverUsername, - GoogleReaderEnabled: integration.GoogleReaderEnabled, - GoogleReaderUsername: integration.GoogleReaderUsername, WallabagEnabled: integration.WallabagEnabled, WallabagOnlyURL: integration.WallabagOnlyURL, WallabagURL: integration.WallabagURL, diff --git a/internal/ui/integration_update.go b/internal/ui/integration_update.go index c54ec6256c1..c4f59b41c2b 100644 --- a/internal/ui/integration_update.go +++ b/internal/ui/integration_update.go @@ -49,24 +49,6 @@ func (h *handler) updateIntegration(w http.ResponseWriter, r *http.Request) { integration.FeverToken = "" } - if integration.GoogleReaderUsername != "" && h.store.HasDuplicateGoogleReaderUsername(user.ID, integration.GoogleReaderUsername) { - sess.NewFlashErrorMessage(printer.Print("error.duplicate_googlereader_username")) - html.Redirect(w, r, route.Path(h.router, "integrations")) - return - } - - if integration.GoogleReaderEnabled { - if integrationForm.GoogleReaderPassword != "" { - integration.GoogleReaderPassword, err = crypto.HashPassword(integrationForm.GoogleReaderPassword) - if err != nil { - html.ServerError(w, r, err) - return - } - } - } else { - integration.GoogleReaderPassword = "" - } - if integrationForm.WebhookEnabled { if integrationForm.WebhookURL == "" { integration.WebhookEnabled = false