1package cloudflare
2
3import (
4	"context"
5	"encoding/json"
6	"fmt"
7	"net/http"
8	"net/url"
9	"strconv"
10	"strings"
11
12	"github.com/pkg/errors"
13)
14
15// Filter holds the structure of the filter type.
16type Filter struct {
17	ID          string `json:"id,omitempty"`
18	Expression  string `json:"expression"`
19	Paused      bool   `json:"paused"`
20	Description string `json:"description"`
21
22	// Property is mentioned in documentation however isn't populated in
23	// any of the API requests. For now, let's just omit it unless it's
24	// provided.
25	Ref string `json:"ref,omitempty"`
26}
27
28// FiltersDetailResponse is the API response that is returned
29// for requesting all filters on a zone.
30type FiltersDetailResponse struct {
31	Result     []Filter `json:"result"`
32	ResultInfo `json:"result_info"`
33	Response
34}
35
36// FilterDetailResponse is the API response that is returned
37// for requesting a single filter on a zone.
38type FilterDetailResponse struct {
39	Result     Filter `json:"result"`
40	ResultInfo `json:"result_info"`
41	Response
42}
43
44// FilterValidateExpression represents the JSON payload for checking
45// an expression.
46type FilterValidateExpression struct {
47	Expression string `json:"expression"`
48}
49
50// FilterValidateExpressionResponse represents the API response for
51// checking the expression. It conforms to the JSON API approach however
52// we don't need all of the fields exposed.
53type FilterValidateExpressionResponse struct {
54	Success bool                                `json:"success"`
55	Errors  []FilterValidationExpressionMessage `json:"errors"`
56}
57
58// FilterValidationExpressionMessage represents the API error message.
59type FilterValidationExpressionMessage struct {
60	Message string `json:"message"`
61}
62
63// Filter returns a single filter in a zone based on the filter ID.
64//
65// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-by-filter-id
66func (api *API) Filter(ctx context.Context, zoneID, filterID string) (Filter, error) {
67	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID)
68
69	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
70	if err != nil {
71		return Filter{}, err
72	}
73
74	var filterResponse FilterDetailResponse
75	err = json.Unmarshal(res, &filterResponse)
76	if err != nil {
77		return Filter{}, errors.Wrap(err, errUnmarshalError)
78	}
79
80	return filterResponse.Result, nil
81}
82
83// Filters returns all filters for a zone.
84//
85// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/get/#get-all-filters
86func (api *API) Filters(ctx context.Context, zoneID string, pageOpts PaginationOptions) ([]Filter, error) {
87	uri := fmt.Sprintf("/zones/%s/filters", zoneID)
88	v := url.Values{}
89
90	if pageOpts.PerPage > 0 {
91		v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
92	}
93
94	if pageOpts.Page > 0 {
95		v.Set("page", strconv.Itoa(pageOpts.Page))
96	}
97
98	if len(v) > 0 {
99		uri = fmt.Sprintf("%s?%s", uri, v.Encode())
100	}
101
102	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
103	if err != nil {
104		return []Filter{}, err
105	}
106
107	var filtersResponse FiltersDetailResponse
108	err = json.Unmarshal(res, &filtersResponse)
109	if err != nil {
110		return []Filter{}, errors.Wrap(err, errUnmarshalError)
111	}
112
113	return filtersResponse.Result, nil
114}
115
116// CreateFilters creates new filters.
117//
118// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/post/
119func (api *API) CreateFilters(ctx context.Context, zoneID string, filters []Filter) ([]Filter, error) {
120	uri := fmt.Sprintf("/zones/%s/filters", zoneID)
121
122	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, filters)
123	if err != nil {
124		return []Filter{}, err
125	}
126
127	var filtersResponse FiltersDetailResponse
128	err = json.Unmarshal(res, &filtersResponse)
129	if err != nil {
130		return []Filter{}, errors.Wrap(err, errUnmarshalError)
131	}
132
133	return filtersResponse.Result, nil
134}
135
136// UpdateFilter updates a single filter.
137//
138// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-a-single-filter
139func (api *API) UpdateFilter(ctx context.Context, zoneID string, filter Filter) (Filter, error) {
140	if filter.ID == "" {
141		return Filter{}, errors.Errorf("filter ID cannot be empty")
142	}
143
144	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filter.ID)
145
146	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, filter)
147	if err != nil {
148		return Filter{}, err
149	}
150
151	var filterResponse FilterDetailResponse
152	err = json.Unmarshal(res, &filterResponse)
153	if err != nil {
154		return Filter{}, errors.Wrap(err, errUnmarshalError)
155	}
156
157	return filterResponse.Result, nil
158}
159
160// UpdateFilters updates many filters at once.
161//
162// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/put/#update-multiple-filters
163func (api *API) UpdateFilters(ctx context.Context, zoneID string, filters []Filter) ([]Filter, error) {
164	for _, filter := range filters {
165		if filter.ID == "" {
166			return []Filter{}, errors.Errorf("filter ID cannot be empty")
167		}
168	}
169
170	uri := fmt.Sprintf("/zones/%s/filters", zoneID)
171
172	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, filters)
173	if err != nil {
174		return []Filter{}, err
175	}
176
177	var filtersResponse FiltersDetailResponse
178	err = json.Unmarshal(res, &filtersResponse)
179	if err != nil {
180		return []Filter{}, errors.Wrap(err, errUnmarshalError)
181	}
182
183	return filtersResponse.Result, nil
184}
185
186// DeleteFilter deletes a single filter.
187//
188// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-a-single-filter
189func (api *API) DeleteFilter(ctx context.Context, zoneID, filterID string) error {
190	if filterID == "" {
191		return errors.Errorf("filter ID cannot be empty")
192	}
193
194	uri := fmt.Sprintf("/zones/%s/filters/%s", zoneID, filterID)
195
196	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
197	if err != nil {
198		return err
199	}
200
201	return nil
202}
203
204// DeleteFilters deletes multiple filters.
205//
206// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/delete/#delete-multiple-filters
207func (api *API) DeleteFilters(ctx context.Context, zoneID string, filterIDs []string) error {
208	ids := strings.Join(filterIDs, ",")
209	uri := fmt.Sprintf("/zones/%s/filters?id=%s", zoneID, ids)
210
211	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
212	if err != nil {
213		return err
214	}
215
216	return nil
217}
218
219// ValidateFilterExpression checks correctness of a filter expression.
220//
221// API reference: https://developers.cloudflare.com/firewall/api/cf-filters/validation/
222func (api *API) ValidateFilterExpression(ctx context.Context, expression string) error {
223	expressionPayload := FilterValidateExpression{Expression: expression}
224
225	_, err := api.makeRequestContext(ctx, http.MethodPost, "/filters/validate-expr", expressionPayload)
226	if err != nil {
227		var filterValidationResponse FilterValidateExpressionResponse
228
229		jsonErr := json.Unmarshal([]byte(err.Error()), &filterValidationResponse)
230		if jsonErr != nil {
231			return errors.Wrap(jsonErr, errUnmarshalError)
232		}
233
234		if !filterValidationResponse.Success {
235			// Unsure why but the API returns `errors` as an array but it only
236			// ever shows the issue with one problem at a time ¯\_(ツ)_/¯
237			return errors.Errorf(filterValidationResponse.Errors[0].Message)
238		}
239	}
240
241	return nil
242}
243