diff --git a/api4/channel.go b/api4/channel.go index 1f4bc4d031..b8eac37c98 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1613,7 +1613,7 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { } if ok && len(postRootId) == 26 { - rootPost, err := c.App.GetSinglePost(postRootId) + rootPost, err := c.App.GetSinglePost(postRootId, false) if err != nil { c.Err = err return diff --git a/api4/channel_local.go b/api4/channel_local.go index 8fe5f2e9b1..d51e169f6d 100644 --- a/api4/channel_local.go +++ b/api4/channel_local.go @@ -164,7 +164,7 @@ func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { } if ok && len(postRootId) == 26 { - rootPost, err := c.App.GetSinglePost(postRootId) + rootPost, err := c.App.GetSinglePost(postRootId, false) if err != nil { c.Err = err return diff --git a/api4/post.go b/api4/post.go index 3ee2fa8c13..8070055f30 100644 --- a/api4/post.go +++ b/api4/post.go @@ -387,7 +387,13 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - post, err := c.App.GetPostIfAuthorized(c.Params.PostId, c.AppContext.Session()) + includeDeleted, _ := strconv.ParseBool(r.URL.Query().Get("include_deleted")) + if includeDeleted && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { + c.SetPermissionError(model.PermissionManageSystem) + return + } + + post, err := c.App.GetPostIfAuthorized(c.Params.PostId, c.AppContext.Session(), includeDeleted) if err != nil { c.Err = err return @@ -471,7 +477,7 @@ func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) { defer c.LogAuditRecWithLevel(auditRec, app.LevelContent) auditRec.AddMeta("post_id", c.Params.PostId) - post, err := c.App.GetSinglePost(c.Params.PostId) + post, err := c.App.GetSinglePost(c.Params.PostId, false) if err != nil { c.SetPermissionError(model.PermissionDeletePost) return @@ -563,7 +569,7 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) { return } - if _, err = c.App.GetPostIfAuthorized(post.Id, c.AppContext.Session()); err != nil { + if _, err = c.App.GetPostIfAuthorized(post.Id, c.AppContext.Session(), false); err != nil { c.Err = err return } @@ -708,7 +714,7 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { return } - originalPost, err := c.App.GetSinglePost(c.Params.PostId) + originalPost, err := c.App.GetSinglePost(c.Params.PostId, false) if err != nil { c.SetPermissionError(model.PermissionEditPost) return @@ -759,7 +765,7 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) { // Updating the file_ids of a post is not a supported operation and will be ignored post.FileIds = nil - originalPost, err := c.App.GetSinglePost(c.Params.PostId) + originalPost, err := c.App.GetSinglePost(c.Params.PostId, false) if err != nil { c.SetPermissionError(model.PermissionEditPost) return @@ -834,7 +840,7 @@ func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) { return } - post, err := c.App.GetSinglePost(c.Params.PostId) + post, err := c.App.GetSinglePost(c.Params.PostId, false) if err != nil { c.Err = err return diff --git a/api4/post_test.go b/api4/post_test.go index f99be5f581..738a89ab95 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -996,7 +996,7 @@ func TestPinPost(t *testing.T) { _, err := client.PinPost(post.Id) require.NoError(t, err) - rpost, appErr := th.App.GetSinglePost(post.Id) + rpost, appErr := th.App.GetSinglePost(post.Id, false) require.Nil(t, appErr) require.True(t, rpost.IsPinned, "failed to pin post") @@ -1026,7 +1026,7 @@ func TestUnpinPost(t *testing.T) { _, err := client.UnpinPost(pinnedPost.Id) require.NoError(t, err) - rpost, appErr := th.App.GetSinglePost(pinnedPost.Id) + rpost, appErr := th.App.GetSinglePost(pinnedPost.Id, false) require.Nil(t, appErr) require.False(t, rpost.IsPinned) @@ -2000,6 +2000,29 @@ func TestGetPost(t *testing.T) { _, _, err = th.LocalClient.GetPost(privatePost.Id, "") require.NoError(t, err) + // Delete post + th.SystemAdminClient.DeletePost(th.BasicPost.Id) + + // Normal client should get 404 when trying to access deleted post normally + _, resp, err = client.GetPost(th.BasicPost.Id, "") + require.Error(t, err) + CheckNotFoundStatus(t, resp) + + // Normal client should get unauthorized when trying to access deleted post + _, resp, err = client.GetPostIncludeDeleted(th.BasicPost.Id, "") + require.Error(t, err) + CheckForbiddenStatus(t, resp) + + // System client should get 404 when trying to access deleted post normally + _, resp, err = th.SystemAdminClient.GetPost(th.BasicPost.Id, "") + require.Error(t, err) + CheckNotFoundStatus(t, resp) + + // System client should be able to access deleted post with include_deleted param + post, _, err := th.SystemAdminClient.GetPostIncludeDeleted(th.BasicPost.Id, "") + require.NoError(t, err) + require.Equal(t, th.BasicPost.Id, post.Id) + client.Logout() // Normal client should get unauthorized, but local client should get 404. diff --git a/api4/preference.go b/api4/preference.go index c76d2febcc..b89a96ecc3 100644 --- a/api4/preference.go +++ b/api4/preference.go @@ -110,7 +110,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) { for _, pref := range preferences { if pref.Category == model.PreferenceCategoryFlaggedPost { - post, err := c.App.GetSinglePost(pref.Name) + post, err := c.App.GetSinglePost(pref.Name, false) if err != nil { c.SetInvalidParam("preference.name") return diff --git a/api4/system.go b/api4/system.go index 52178519e0..c510286d1f 100644 --- a/api4/system.go +++ b/api4/system.go @@ -569,7 +569,7 @@ func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) { // Return post data only when PostId is passed. if ack.PostId != "" && ack.NotificationType == model.PushTypeMessage { - if _, appErr := c.App.GetPostIfAuthorized(ack.PostId, c.AppContext.Session()); appErr != nil { + if _, appErr := c.App.GetPostIfAuthorized(ack.PostId, c.AppContext.Session(), false); appErr != nil { c.Err = appErr return } diff --git a/app/app_iface.go b/app/app_iface.go index 2af4770b6a..28f6e34bda 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -680,7 +680,7 @@ type AppIface interface { GetPostAfterTime(channelID string, time int64, collapsedThreads bool) (*model.Post, *model.AppError) GetPostIdAfterTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) GetPostIdBeforeTime(channelID string, time int64, collapsedThreads bool) (string, *model.AppError) - GetPostIfAuthorized(postID string, session *model.Session) (*model.Post, *model.AppError) + GetPostIfAuthorized(postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) GetPostThread(postID string, opts model.GetPostsOptions, userID string) (*model.PostList, *model.AppError) GetPosts(channelID string, offset int, limit int) (*model.PostList, *model.AppError) GetPostsAfterPost(options model.GetPostsOptions) (*model.PostList, *model.AppError) @@ -735,7 +735,7 @@ type AppIface interface { GetSidebarCategories(userID, teamID string) (*model.OrderedSidebarCategories, *model.AppError) GetSidebarCategory(categoryId string) (*model.SidebarCategoryWithChannels, *model.AppError) GetSidebarCategoryOrder(userID, teamID string) ([]string, *model.AppError) - GetSinglePost(postID string) (*model.Post, *model.AppError) + GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) GetSiteURL() string GetStatus(userID string) (*model.Status, *model.AppError) GetStatusFromCache(userID string) *model.Status diff --git a/app/channel.go b/app/channel.go index bc11b89c09..c7104c7356 100644 --- a/app/channel.go +++ b/app/channel.go @@ -2570,7 +2570,7 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapse if !collapsedThreadsSupported || !a.IsCRTEnabledForUser(userID) { return a.markChannelAsUnreadFromPostCRTUnsupported(postID, userID) } - post, err := a.GetSinglePost(postID) + post, err := a.GetSinglePost(postID, false) if err != nil { return nil, err } @@ -2597,7 +2597,7 @@ func (a *App) MarkChannelAsUnreadFromPost(postID string, userID string, collapse } func (a *App) markChannelAsUnreadFromPostCRTUnsupported(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError) { - post, err := a.GetSinglePost(postID) + post, err := a.GetSinglePost(postID, false) if err != nil { return nil, err } @@ -2636,7 +2636,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(postID string, userID st // If there are replies with mentions below the marked reply in the thread, then sum the mentions for the threads mention badge. // In CRT Unsupported Client: Channel is marked as unread and new messages line inserted above the marked post. // Badge on channel sums mentions in all posts (root & replies) including and below the post that was marked unread. - rootPost, err := a.GetSinglePost(post.RootId) + rootPost, err := a.GetSinglePost(post.RootId, false) if err != nil { return nil, err } diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 545d3261dd..39dcd31510 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -7683,7 +7683,7 @@ func (a *OpenTracingAppLayer) GetPostIdBeforeTime(channelID string, time int64, return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetPostIfAuthorized(postID string, session *model.Session) (*model.Post, *model.AppError) { +func (a *OpenTracingAppLayer) GetPostIfAuthorized(postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostIfAuthorized") @@ -7695,7 +7695,7 @@ func (a *OpenTracingAppLayer) GetPostIfAuthorized(postID string, session *model. }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetPostIfAuthorized(postID, session) + resultVar0, resultVar1 := a.app.GetPostIfAuthorized(postID, session, includeDeleted) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) @@ -8995,7 +8995,7 @@ func (a *OpenTracingAppLayer) GetSidebarCategoryOrder(userID string, teamID stri return resultVar0, resultVar1 } -func (a *OpenTracingAppLayer) GetSinglePost(postID string) (*model.Post, *model.AppError) { +func (a *OpenTracingAppLayer) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSinglePost") @@ -9007,7 +9007,7 @@ func (a *OpenTracingAppLayer) GetSinglePost(postID string) (*model.Post, *model. }() defer span.Finish() - resultVar0, resultVar1 := a.app.GetSinglePost(postID) + resultVar0, resultVar1 := a.app.GetSinglePost(postID, includeDeleted) if resultVar1 != nil { span.LogFields(spanlog.Error(resultVar1)) diff --git a/app/plugin_api.go b/app/plugin_api.go index 0cb627cb70..ba4c5243a0 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -665,7 +665,7 @@ func (api *PluginAPI) GetPostThread(postID string) (*model.PostList, *model.AppE } func (api *PluginAPI) GetPost(postID string) (*model.Post, *model.AppError) { - return api.app.GetSinglePost(postID) + return api.app.GetSinglePost(postID, false) } func (api *PluginAPI) GetPostsSince(channelID string, time int64) (*model.PostList, *model.AppError) { diff --git a/app/post.go b/app/post.go index 56d1906d68..51dc176349 100644 --- a/app/post.go +++ b/app/post.go @@ -136,7 +136,7 @@ func (a *App) deduplicateCreatePost(post *model.Post) (foundPost *model.Post, er // If the other thread finished creating the post, return the created post back to the // client, making the API call feel idempotent. - actualPost, err := a.GetSinglePost(postID) + actualPost, err := a.GetSinglePost(postID, false) if err != nil { return nil, model.NewAppError("deduplicateCreatePost", "api.post.deduplicate_create_post.failed_to_get", nil, err.Error(), http.StatusInternalServerError) } @@ -718,7 +718,7 @@ func (a *App) publishWebsocketEventForPermalinkPost(post *model.Post, message *m return false, nil } - previewedPost, err := a.GetSinglePost(previewedPostID) + previewedPost, err := a.GetSinglePost(previewedPostID, false) if err != nil { if err.StatusCode == http.StatusNotFound { mlog.Warn("permalinked post not found", mlog.String("referenced_post_id", previewedPostID)) @@ -768,7 +768,7 @@ func (a *App) publishWebsocketEventForPermalinkPost(post *model.Post, message *m } func (a *App) PatchPost(c *request.Context, postID string, patch *model.PostPatch) (*model.Post, *model.AppError) { - post, err := a.GetSinglePost(postID) + post, err := a.GetSinglePost(postID, false) if err != nil { return nil, err } @@ -840,8 +840,8 @@ func (a *App) GetPostsSince(options model.GetPostsSinceOptions) (*model.PostList return postList, nil } -func (a *App) GetSinglePost(postID string) (*model.Post, *model.AppError) { - post, err := a.Srv().Store.Post().GetSingle(postID, false) +func (a *App) GetSinglePost(postID string, includeDeleted bool) (*model.Post, *model.AppError) { + post, err := a.Srv().Store.Post().GetSingle(postID, includeDeleted) if err != nil { var nfErr *store.ErrNotFound switch { @@ -1678,8 +1678,8 @@ func (a *App) GetThreadMembershipsForUser(userID, teamID string) ([]*model.Threa return a.Srv().Store.Thread().GetMembershipsForUser(userID, teamID) } -func (a *App) GetPostIfAuthorized(postID string, session *model.Session) (*model.Post, *model.AppError) { - post, err := a.GetSinglePost(postID) +func (a *App) GetPostIfAuthorized(postID string, session *model.Session, includeDeleted bool) (*model.Post, *model.AppError) { + post, err := a.GetSinglePost(postID, includeDeleted) if err != nil { return nil, err } diff --git a/app/post_metadata.go b/app/post_metadata.go index 9f38e18884..7ccd2f6198 100644 --- a/app/post_metadata.go +++ b/app/post_metadata.go @@ -540,7 +540,7 @@ func (a *App) getLinkMetadata(requestURL string, timestamp int64, isNewPost bool if looksLikeAPermalink(requestURL, a.GetSiteURL()) && *a.Config().ServiceSettings.EnablePermalinkPreviews && a.Config().FeatureFlags.PermalinkPreviews { referencedPostID := requestURL[len(requestURL)-26:] - referencedPost, appErr := a.GetSinglePost(referencedPostID) + referencedPost, appErr := a.GetSinglePost(referencedPostID, false) // TODO: Look into saving a value in the LinkMetadata.Data field to prevent perpetually re-querying for the deleted post. if appErr != nil { return nil, nil, nil, appErr diff --git a/app/post_test.go b/app/post_test.go index 2e0fb07b78..9a4a2857b2 100644 --- a/app/post_test.go +++ b/app/post_test.go @@ -250,7 +250,7 @@ func TestAttachFilesToPost(t *testing.T) { assert.Len(t, infos, 1) assert.Equal(t, info2.Id, infos[0].Id) - updated, appErr := th.App.GetSinglePost(post.Id) + updated, appErr := th.App.GetSinglePost(post.Id, false) require.Nil(t, appErr) assert.Len(t, updated.FileIds, 1) assert.Contains(t, updated.FileIds, info2.Id) @@ -2761,11 +2761,11 @@ func TestGetPostIfAuthorized(t *testing.T) { require.NotNil(t, session2) // User is not authorized to get post - _, err = th.App.GetPostIfAuthorized(post.Id, session2) + _, err = th.App.GetPostIfAuthorized(post.Id, session2, false) require.NotNil(t, err) // User is authorized to get post - _, err = th.App.GetPostIfAuthorized(post.Id, session1) + _, err = th.App.GetPostIfAuthorized(post.Id, session1, false) require.Nil(t, err) } diff --git a/app/reaction.go b/app/reaction.go index 6916fbbc8b..5f4c7557cd 100644 --- a/app/reaction.go +++ b/app/reaction.go @@ -15,7 +15,7 @@ import ( ) func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction) (*model.Reaction, *model.AppError) { - post, err := a.GetSinglePost(reaction.PostId) + post, err := a.GetSinglePost(reaction.PostId, false) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func (a *App) GetTopReactionsForUserSince(userID string, teamID string, opts *mo } func (a *App) DeleteReactionForPost(c *request.Context, reaction *model.Reaction) *model.AppError { - post, err := a.GetSinglePost(reaction.PostId) + post, err := a.GetSinglePost(reaction.PostId, false) if err != nil { return err } diff --git a/app/user.go b/app/user.go index 8e0166675f..9bd21cce07 100644 --- a/app/user.go +++ b/app/user.go @@ -2449,7 +2449,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID s return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) } - post, appErr := a.GetSinglePost(threadID) + post, appErr := a.GetSinglePost(threadID, false) if appErr != nil { return appErr } @@ -2495,7 +2495,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID s } func (a *App) UpdateThreadReadForUserByPost(currentSessionId, userID, teamID, threadID, postID string) (*model.ThreadResponse, *model.AppError) { - post, err := a.GetSinglePost(postID) + post, err := a.GetSinglePost(postID, false) if err != nil { return nil, err } @@ -2528,7 +2528,7 @@ func (a *App) UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID return nil, model.NewAppError("UpdateThreadReadForUser", "app.user.update_thread_read_for_user.app_error", nil, nErr.Error(), http.StatusInternalServerError) } - post, err := a.GetSinglePost(threadID) + post, err := a.GetSinglePost(threadID, false) if err != nil { return nil, err } diff --git a/model/client4.go b/model/client4.go index 8b15c31d8c..ef28ba7387 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3780,6 +3780,24 @@ func (c *Client4) GetPost(postId string, etag string) (*Post, *Response, error) return &post, BuildResponse(r), nil } +// GetPostIncludeDeleted gets a single post, including deleted. +func (c *Client4) GetPostIncludeDeleted(postId string, etag string) (*Post, *Response, error) { + r, err := c.DoAPIGet(c.postRoute(postId)+"?include_deleted="+c.boolString(true), etag) + if err != nil { + return nil, BuildResponse(r), err + } + defer closeBody(r) + + var post Post + if r.StatusCode == http.StatusNotModified { + return &post, BuildResponse(r), nil + } + if jsonErr := json.NewDecoder(r.Body).Decode(&post); jsonErr != nil { + return nil, nil, NewAppError("GetPostIncludeDeleted", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError) + } + return &post, BuildResponse(r), nil +} + // DeletePost deletes a post from the provided post id string. func (c *Client4) DeletePost(postId string) (*Response, error) { r, err := c.DoAPIDelete(c.postRoute(postId))