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