[MM-54357] Recent Mentions is showing posts for other similar named users. (#25010)
* Handle double quotes in Postgres * quote the username when performing the search
Этот коммит содержится в:
@@ -30,8 +30,8 @@ var searchPostStoreTests = []searchTest{
|
|||||||
Tags: []string{EngineAll},
|
Tags: []string{EngineAll},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Should be able to search for exact phrases in quotes",
|
Name: "Should be able to search for quoted patterns with AND OR combinations",
|
||||||
Fn: testSearchExactPhraseInQuotes,
|
Fn: testSearchANDORQuotesCombinations,
|
||||||
Tags: []string{EnginePostgres, EngineMySql, EngineElasticSearch},
|
Tags: []string{EnginePostgres, EngineMySql, EngineElasticSearch},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -352,24 +352,107 @@ func testSearchReturnPinnedAndUnpinned(t *testing.T, th *SearchTestHelper) {
|
|||||||
th.checkPostInSearchResults(t, p2.Id, results.Posts)
|
th.checkPostInSearchResults(t, p2.Id, results.Posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSearchExactPhraseInQuotes(t *testing.T, th *SearchTestHelper) {
|
func testSearchANDORQuotesCombinations(t *testing.T, th *SearchTestHelper) {
|
||||||
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 1 2 3", "", model.PostTypeDefault, 0, false)
|
p1, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two three four", "", model.PostTypeDefault, 0, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel test 123", "", model.PostTypeDefault, 0, false)
|
p2, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one two five", "", model.PostTypeDefault, 0, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel something test 1 2 3", "", model.PostTypeDefault, 0, false)
|
p3, err := th.createPost(th.User.Id, th.ChannelBasic.Id, "one five six", "", model.PostTypeDefault, 0, false)
|
||||||
require.NoError(t, err)
|
|
||||||
_, err = th.createPost(th.User.Id, th.ChannelBasic.Id, "channel 1 2 3", "", model.PostTypeDefault, 0, false)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
defer th.deleteUserPosts(th.User.Id)
|
defer th.deleteUserPosts(th.User.Id)
|
||||||
|
|
||||||
params := &model.SearchParams{Terms: "\"channel test 1 2 3\""}
|
testCases := []struct {
|
||||||
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
|
name string
|
||||||
require.NoError(t, err)
|
terms string
|
||||||
|
orTerms bool
|
||||||
|
expectedLen int
|
||||||
|
expectedIDs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "AND operator, No Quotes, Matches 1",
|
||||||
|
terms: `two four`,
|
||||||
|
orTerms: false,
|
||||||
|
expectedLen: 1,
|
||||||
|
expectedIDs: []string{p1.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AND operator, No Quotes, Matches 0",
|
||||||
|
terms: `two six`,
|
||||||
|
orTerms: false,
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedIDs: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AND operator, With Full Quotes, Matches 0",
|
||||||
|
terms: `"two four"`,
|
||||||
|
orTerms: false,
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedIDs: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AND operator, With Full Quotes, Matches 1",
|
||||||
|
terms: `"two three four"`,
|
||||||
|
orTerms: false,
|
||||||
|
expectedLen: 1,
|
||||||
|
expectedIDs: []string{p1.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AND operator, With Part Quotes, Matches 1",
|
||||||
|
terms: `two "three four"`,
|
||||||
|
orTerms: false,
|
||||||
|
expectedLen: 1,
|
||||||
|
expectedIDs: []string{p1.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR operator, No Quotes, Matches 2",
|
||||||
|
terms: `two four`,
|
||||||
|
orTerms: true,
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedIDs: []string{p1.Id, p2.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR operator, No Quotes, Matches 3",
|
||||||
|
terms: `two six`,
|
||||||
|
orTerms: true,
|
||||||
|
expectedLen: 3,
|
||||||
|
expectedIDs: []string{p1.Id, p2.Id, p3.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR operator, With Full Quotes, Matches 0",
|
||||||
|
terms: `"two four"`,
|
||||||
|
orTerms: true,
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedIDs: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR operator, With Full Quotes, Matches 1",
|
||||||
|
terms: `"two three four"`,
|
||||||
|
orTerms: true,
|
||||||
|
expectedLen: 1,
|
||||||
|
expectedIDs: []string{p1.Id},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OR operator, With Part Quotes, Matches 2",
|
||||||
|
terms: `two "three four"`,
|
||||||
|
orTerms: true,
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedIDs: []string{p1.Id, p2.Id},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
require.Len(t, results.Posts, 1)
|
for _, tc := range testCases {
|
||||||
th.checkPostInSearchResults(t, p1.Id, results.Posts)
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
params := &model.SearchParams{Terms: tc.terms, OrTerms: tc.orTerms}
|
||||||
|
results, err := th.Store.Post().SearchPostsForUser([]*model.SearchParams{params}, th.User.Id, th.Team.Id, 0, 20)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, results.Posts, tc.expectedLen)
|
||||||
|
for _, id := range tc.expectedIDs {
|
||||||
|
th.checkPostInSearchResults(t, id, results.Posts)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testSearchEmailAddresses(t *testing.T, th *SearchTestHelper) {
|
func testSearchEmailAddresses(t *testing.T, th *SearchTestHelper) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import (
|
|||||||
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
"github.com/mattermost/mattermost/server/v8/einterfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Regex to get quoted strings
|
||||||
|
var quotedStringsRegex = regexp.MustCompile(`("[^"]*")`)
|
||||||
|
|
||||||
type SqlPostStore struct {
|
type SqlPostStore struct {
|
||||||
*SqlStore
|
*SqlStore
|
||||||
metrics einterfaces.MetricsInterface
|
metrics einterfaces.MetricsInterface
|
||||||
@@ -2029,27 +2032,38 @@ func (s *SqlPostStore) search(teamId string, userId string, params *model.Search
|
|||||||
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
|
excludedTerms = wildcard.ReplaceAllLiteralString(excludedTerms, ":* ")
|
||||||
}
|
}
|
||||||
|
|
||||||
excludeClause := ""
|
// Replace spaces with to_tsquery symbols
|
||||||
if excludedTerms != "" {
|
replaceSpaces := func(input string, excludedInput bool) string {
|
||||||
excludeClause = " & !(" + strings.Join(strings.Fields(excludedTerms), " | ") + ")"
|
if input == "" {
|
||||||
}
|
return input
|
||||||
|
|
||||||
var termsClause string
|
|
||||||
if params.OrTerms {
|
|
||||||
termsClause = "(" + strings.Join(strings.Fields(terms), " | ") + ")" + excludeClause
|
|
||||||
} else if strings.HasPrefix(terms, `"`) && strings.HasSuffix(terms, `"`) {
|
|
||||||
termsClause = "(" + strings.Join(strings.Fields(terms), " <-> ") + ")" + excludeClause
|
|
||||||
} else {
|
|
||||||
tsVectorSearchQuery := strings.Join(strings.Fields(terms), " & ")
|
|
||||||
if params.IsHashtag {
|
|
||||||
tsVectorSearchQuery = terms
|
|
||||||
}
|
}
|
||||||
|
|
||||||
termsClause = "(" + tsVectorSearchQuery + ")" + excludeClause
|
// Remove extra spaces
|
||||||
|
input = strings.Join(strings.Fields(input), " ")
|
||||||
|
|
||||||
|
// Replace spaces within quoted strings with '<->'
|
||||||
|
input = quotedStringsRegex.ReplaceAllStringFunc(input, func(match string) string {
|
||||||
|
return strings.Replace(match, " ", "<->", -1)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Replace spaces outside of quoted substrings with '&' or '|'
|
||||||
|
replacer := "&"
|
||||||
|
if excludedInput || params.OrTerms {
|
||||||
|
replacer = "|"
|
||||||
|
}
|
||||||
|
input = strings.Replace(input, " ", replacer, -1)
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
tsQueryClause := replaceSpaces(terms, false)
|
||||||
|
excludedClause := replaceSpaces(excludedTerms, true)
|
||||||
|
if excludedClause != "" {
|
||||||
|
tsQueryClause += " &!(" + excludedClause + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
searchClause := fmt.Sprintf("to_tsvector('%[1]s', %[2]s) @@ to_tsquery('%[1]s', ?)", s.pgDefaultTextSearchConfig, searchType)
|
searchClause := fmt.Sprintf("to_tsvector('%[1]s', %[2]s) @@ to_tsquery('%[1]s', ?)", s.pgDefaultTextSearchConfig, searchType)
|
||||||
baseQuery = baseQuery.Where(searchClause, termsClause)
|
baseQuery = baseQuery.Where(searchClause, tsQueryClause)
|
||||||
} else if s.DriverName() == model.DatabaseDriverMysql {
|
} else if s.DriverName() == model.DatabaseDriverMysql {
|
||||||
if searchType == "Message" {
|
if searchType == "Message" {
|
||||||
terms, err = removeMysqlStopWordsFromTerms(terms)
|
terms, err = removeMysqlStopWordsFromTerms(terms)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
|||||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||||
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUser, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
||||||
import type {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions';
|
import type {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
@@ -193,21 +193,38 @@ function updateSearchResultsTerms(terms: string) {
|
|||||||
|
|
||||||
export function performSearch(terms: string, isMentionSearch?: boolean) {
|
export function performSearch(terms: string, isMentionSearch?: boolean) {
|
||||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
|
let searchTerms = terms;
|
||||||
const teamId = getCurrentTeamId(getState());
|
const teamId = getCurrentTeamId(getState());
|
||||||
const config = getConfig(getState());
|
const config = getConfig(getState());
|
||||||
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
|
||||||
const extensionsFilters = getFilesSearchExtFilter(getState() as GlobalState);
|
const extensionsFilters = getFilesSearchExtFilter(getState() as GlobalState);
|
||||||
|
|
||||||
const extensions = extensionsFilters?.map((ext) => `ext:${ext}`).join(' ');
|
const extensions = extensionsFilters?.map((ext) => `ext:${ext}`).join(' ');
|
||||||
let termsWithExtensionsFilters = terms;
|
let termsWithExtensionsFilters = searchTerms;
|
||||||
if (extensions?.trim().length > 0) {
|
if (extensions?.trim().length > 0) {
|
||||||
termsWithExtensionsFilters += ` ${extensions}`;
|
termsWithExtensionsFilters += ` ${extensions}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMentionSearch) {
|
||||||
|
// Username should be quoted to allow specific search
|
||||||
|
// in case username is made with multiple words splitted by dashes or other symbols.
|
||||||
|
const user = getCurrentUser(getState());
|
||||||
|
const termsArr = searchTerms.split(' ').filter((t) => Boolean(t && t.trim()));
|
||||||
|
const username = '@' + user.username;
|
||||||
|
const quotedUsername = `"${username}"`;
|
||||||
|
for (let i = 0; i < termsArr.length; i++) {
|
||||||
|
if (termsArr[i] === username) {
|
||||||
|
termsArr[i] = quotedUsername;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
searchTerms = termsArr.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
// timezone offset in seconds
|
// timezone offset in seconds
|
||||||
const userCurrentTimezone = getCurrentTimezone(getState());
|
const userCurrentTimezone = getCurrentTimezone(getState());
|
||||||
const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
|
const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
|
||||||
const messagesPromise = dispatch(searchPostsWithParams(isMentionSearch ? '' : teamId, {terms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
const messagesPromise = dispatch(searchPostsWithParams(isMentionSearch ? '' : teamId, {terms: searchTerms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||||
const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||||
return Promise.all([filesPromise, messagesPromise]);
|
return Promise.all([filesPromise, messagesPromise]);
|
||||||
};
|
};
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user