~netlandish/links

054a080a69bbacfe7a420ac5ccbf6b8b8df0b75c — Peter Sanchez 24 days ago 49656ca
Revisiting the search work around from previous abuse. Properly escaping search input for PostgreSQL FTS now.
6 files changed, 34 insertions(+), 26 deletions(-)

M admin/routes.go
M api/graph/schema.resolvers.go
M api/loaders/loaders.go
M core/routes.go
M helpers.go
M values.go
M admin/routes.go => admin/routes.go +2 -2
@@ 64,10 64,10 @@ func (s *Service) Autocomplete(c echo.Context) error {
	org := c.QueryParam("org")
	items := []Item{}
	if org != "" {
		s := links.ParseSearch(org, true)
		s := links.ParseSearch(org)
		opts := &database.FilterOptions{
			Filter: sq.And{
				sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug) @@ websearch_to_tsquery('simple', ?)`, s),
				sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug) @@ to_tsquery('simple', ?)`, s),
				sq.Eq{"o.is_active": true},
			},
		}

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +13 -13
@@ 4292,11 4292,11 @@ func (r *queryResolver) GetOrganizations(ctx context.Context, input *model.GetOr
	//    OrderBy: "o.created_on ASC",
	//}
	if input.Search != nil && *input.Search != "" {
		s := links.ParseSearch(*input.Search, true)
		s := links.ParseSearch(*input.Search)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug )
				@@ websearch_to_tsquery('simple', ?)`, s),
				@@ to_tsquery('simple', ?)`, s),
		}
	}



@@ 4735,11 4735,11 @@ func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInp
	}

	if input.Search != nil && *input.Search != "" {
		s := links.ParseSearch(*input.Search, true)
		s := links.ParseSearch(*input.Search)
		linkOpts.Filter = sq.And{
			linkOpts.Filter,
			sq.Expr(`to_tsvector('simple', ol.title || ' ' || ol.description || ' ' || ol.url)
				@@ websearch_to_tsquery('simple', ?)`, s),
				@@ to_tsquery('simple', ?)`, s),
		}

	}


@@ 6015,11 6015,11 @@ func (r *queryResolver) GetFeed(ctx context.Context, input *model.GetFeedInput) 
	}

	if input.Search != nil && *input.Search != "" {
		s := links.ParseSearch(*input.Search, true)
		s := links.ParseSearch(*input.Search)
		linkOpts.Filter = sq.And{
			linkOpts.Filter,
			sq.Expr(`to_tsvector('simple', ol.title || ' ' || ol.description || ' ' || ol.url)
				@@ websearch_to_tsquery('simple', ?)`, s),
				@@ to_tsquery('simple', ?)`, s),
		}

	}


@@ 6159,11 6159,11 @@ func (r *queryResolver) GetUsers(ctx context.Context, input *model.GetUserInput)

	if input.Search != nil && *input.Search != "" {
		// We want to search for partial match
		s := links.ParseSearch(*input.Search, true)
		s := links.ParseSearch(*input.Search)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr(`to_tsvector('simple', u.full_name || ' ' || u.email || ' ' || o.slug)
				@@ websearch_to_tsquery('simple', ?)`, s),
				@@ to_tsquery('simple', ?)`, s),
		}

	}


@@ 6312,11 6312,11 @@ func (r *queryResolver) GetAdminOrganizations(ctx context.Context, input *model.

	if input.Search != nil && *input.Search != "" {
		// We want to search for partial match
		s := links.ParseSearch(*input.Search, true)
		s := links.ParseSearch(*input.Search)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr(`to_tsvector('simple', o.name || ' ' || o.slug )
				@@ websearch_to_tsquery('simple', ?)`, s),
				@@ to_tsquery('simple', ?)`, s),
		}
	}



@@ 6593,12 6593,12 @@ func (r *queryResolver) GetAdminDomains(ctx context.Context, input *model.GetAdm

	if input.Search != nil && *input.Search != "" {
		// NOTE: full text search (FTS) only support partial prefix search :*
		// it does not support preffix based search
		s := links.ParseSearch(*input.Search, true)
		// it does not support prefix based search
		s := links.ParseSearch(*input.Search)
		opts.Filter = sq.And{
			opts.Filter,
			sq.Or{
				sq.Expr(`to_tsvector('simple', d.name) @@ websearch_to_tsquery('simple', ?)`, s),
				sq.Expr(`to_tsvector('simple', d.name) @@ to_tsquery('simple', ?)`, s),
				sq.ILike{"lookup_name": fmt.Sprintf("%%%s%%", *input.Search)},
			},
		}

M api/loaders/loaders.go => api/loaders/loaders.go +2 -2
@@ 72,11 72,11 @@ func getPopularLinks(ctx context.Context) func(key []string) ([][]*models.BaseUR
		}

		if q != "" {
			s := links.ParseSearch(q, true)
			s := links.ParseSearch(q)
			opts.Filter = sq.And{
				opts.Filter,
				sq.Expr(`to_tsvector('simple', b.title ||  ' ' || b.url)
					@@ websearch_to_tsquery('simple', ?)`, s),
					@@ to_tsquery('simple', ?)`, s),
			}
		}


M core/routes.go => core/routes.go +1 -1
@@ 3143,7 3143,7 @@ func (s *Service) TagAutocomplete(c echo.Context) error {
	var tags []*models.Tag
	var err error
	if q != "" {
		s := links.ParseSearch(q, false)
		s := links.ParseSearch(q)
		opts := &database.FilterOptions{
			Filter: sq.Expr(`to_tsvector('simple', t.name) @@ to_tsquery('simple', ?)`, s),
		}

M helpers.go => helpers.go +15 -8
@@ 824,15 824,22 @@ func (t TagQuery) GetSubQuery(inputTag, inputExcludeTag *string) (string, []inte

// We need to do some parsing to avoid systax error
// for some string chars for non websearch queries
func ParseSearch(s string, forWeb bool) string {
	if forWeb {
		return s
func ParseSearch(s string) string {
	re := regexp.MustCompile(`[^a-zA-Z0-9 &|!-]`)
	s = re.ReplaceAllString(s, "")

	var words []string
	for _, word := range strings.Split(s, " ") {
		// This is used for to_tsquery searches (tag autocomplete)
		word = strings.TrimSpace(word)
		word = strings.Replace(word, ":", "\\:", -1)
		if !strings.HasPrefix(word, "-") {
			word = word + ":*"
		}
		words = append(words, word)
	}

	// This is used for to_tsquery searches (tag autocomplete)
	s = strings.TrimSpace(s)
	s = strings.Replace(s, ":", "\\:", -1)
	return strings.Replace(s, " ", ":* & ", -1) + ":*"
	s = strings.Join(words, " & ")
	return s
}

func AddQueryElement(q template.URL, param, val string) template.URL {

M values.go => values.go +1 -0
@@ 82,6 82,7 @@ var InvalidSlugs = []string{
	"weblog",
	"well-known",
	"api",
	"faq",
}

// Interval used to display data in analytics engagement chart