Skip to content

Commit 22a917f

Browse files
Created query framework to speed up and extend query functionality, added dhcp configuration endpoints, updated documentation, staged for merge
1 parent e14aeb8 commit 22a917f

18 files changed

Lines changed: 3083 additions & 93 deletions

README.md

Lines changed: 566 additions & 17 deletions
Large diffs are not rendered by default.

docs/CONTRIBUTING.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ not specified, the default values are assumed:
263263
Please note that this URL should start with a `/` but not end with one. This will recursively create any directory
264264
within the URL path and overwrite any existing index.php file at this location. Defaults to `null` which will throw an
265265
error when building endpoints.
266+
- `$this-query_excludes` : Specify parameters to exclude from queries on GET requests. This is typically only necessary
267+
if your GET request requires parameters to locate data.
266268

267269
#### Overriding Base Model Methods ####
268270
There are class methods that you will need to override to map API models to specific HTTP methods. If any of these

docs/documentation.json

Lines changed: 426 additions & 7 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
// Copyright 2020 Jared Hendrickson
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
require_once("api/framework/APIEndpoint.inc");
17+
18+
class APIServicesDHCPd extends APIEndpoint {
19+
public function __construct() {
20+
$this->url = "/api/v1/services/dhcpd";
21+
}
22+
23+
protected function get() {
24+
return (new APIServicesDHCPdRead())->call();
25+
}
26+
27+
protected function put() {
28+
return (new APIServicesDHCPdUpdate())->call();
29+
}
30+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
// Copyright 2020 Jared Hendrickson
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
require_once("api/framework/APIEndpoint.inc");
17+
18+
class APIServicesDHCPdStaticMapping extends APIEndpoint {
19+
public function __construct() {
20+
$this->url = "/api/v1/services/dhcpd/static_mapping";
21+
$this->query_excludes = ["interface"];
22+
}
23+
24+
protected function get() {
25+
return (new APIServicesDHCPdStaticMappingRead())->call();
26+
}
27+
28+
protected function post() {
29+
return (new APIServicesDHCPdStaticMappingCreate())->call();
30+
}
31+
32+
protected function put() {
33+
return (new APIServicesDHCPdStaticMappingUpdate())->call();
34+
}
35+
36+
protected function delete() {
37+
return (new APIServicesDHCPdStaticMappingDelete())->call();
38+
}
39+
}

pfSense-pkg-API/files/etc/inc/api/framework/APIEndpoint.inc

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// limitations under the License.
1515

1616
require_once("api/framework/APIModel.inc");
17+
require_once("api/framework/APIQuery.inc");
1718

1819
# Allow endpoints to access any API model class
1920
foreach(glob("/etc/inc/api/models/*.inc") as $model) {
@@ -22,10 +23,12 @@ foreach(glob("/etc/inc/api/models/*.inc") as $model) {
2223

2324
class APIEndpoint {
2425
public $url;
26+
public $query_excludes;
2527

2628
# Set class contructor defaults
2729
public function __construct() {
2830
$this->url = null;
31+
$this->query_excludes = [];
2932
}
3033

3134
# Model to run when endpoint receives a GET request
@@ -48,57 +51,10 @@ class APIEndpoint {
4851
return APIResponse\get(2);
4952
}
5053

51-
# Limit GET results using search queries
52-
private function query_get_data($resp) {
53-
$excluded_keys = ["client-token", "client-id"];
54-
# Check if response is eligible for search querying
55-
if ($resp["return"] === 0) {
56-
$new_resp = $resp;
57-
$new_resp["data"] = [];
58-
# Loop through each request and ensure it is an array, if so, check for query matches
59-
foreach ($resp["data"] as $id=>$data) {
60-
if (is_array($data)) {
61-
$is_query = false;
62-
# Loop through each payload value and check for matches
63-
foreach (APITools\get_request_data() as $key=>$value) {
64-
# Ensure key is not excluded
65-
if (!in_array($key, $excluded_keys)) {
66-
$is_query = true;
67-
if (array_key_exists($key, $data) and $data[$key] === $value) {
68-
$match = true;
69-
} else {
70-
$match = false;
71-
break;
72-
}
73-
}
74-
}
75-
76-
# If this entry matched our query, add it to our new response.
77-
if ($match === true) {
78-
$new_resp["data"][$id] = $data;
79-
}
80-
81-
} else {
82-
return $resp;
83-
}
84-
}
85-
86-
# Only return our query results if user passed in query parameters
87-
if ($is_query) {
88-
return $new_resp;
89-
} else {
90-
return $resp;
91-
}
92-
93-
} else {
94-
return $resp;
95-
}
96-
}
97-
9854
# Listen for HTTP requests and call the corresponding method
9955
public function listen() {
10056
if ($_SERVER["REQUEST_METHOD"] === "GET") {
101-
$resp = $this->query_get_data($this->get());
57+
$resp = (new APIQuery($this->get(), $this->query_excludes))->query();
10258
} elseif ($_SERVER["REQUEST_METHOD"] === "POST") {
10359
$resp = $this->post();
10460
} elseif ($_SERVER["REQUEST_METHOD"] === "PUT") {
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
require_once("api/framework/APITools.inc");
3+
4+
class APIQuery {
5+
public $response;
6+
public $data;
7+
public $payload;
8+
public $excluded;
9+
10+
public function __construct($response, $excluded=[], $payload=null) {
11+
$this->response = $response;
12+
$this->data = [];
13+
$this->payload = (is_null($payload)) ? APITools\get_request_data() : $payload;
14+
$this->excluded = $excluded;
15+
}
16+
17+
# Executes our query
18+
public function query() {
19+
$q_response = [];
20+
$q_count = 0;
21+
22+
# First check that our response was successful
23+
if ($this->response["return"] === 0) {
24+
# Ensure our data field is an array and format our data property
25+
if (is_array($this->response["data"])) {
26+
$this->data = $this->response["data"];
27+
# Loop through each data item and check for query matches
28+
foreach ($this->data as $id=>$entry) {
29+
$match = true;
30+
foreach ($this->payload as $q_key=>$q_value) {
31+
$q_result = $this->recurse($entry, $q_key, $q_value);
32+
# Add entry if query matched, if null skip iteration, otherwise break
33+
if ($q_result === true) {
34+
$q_count++;
35+
} elseif (is_null($q_result)) {
36+
continue;
37+
} else {
38+
$match = false;
39+
$q_count++;
40+
break;
41+
}
42+
}
43+
# If this query matched, add it to our new response
44+
if ($match) {
45+
$q_response[$id] = $entry;
46+
}
47+
}
48+
}
49+
}
50+
51+
# Return our query response if at least 1 query was performed
52+
if ($q_count > 0) {
53+
$this->response["data"] = $q_response;
54+
return $this->response;
55+
} else {
56+
return $this->response;
57+
}
58+
}
59+
60+
# Checks if we can find a value match using array recursion
61+
private function recurse($entry, $key, $value) {
62+
# Variables
63+
$q_count = 0;
64+
$q_params = explode("__", $key);
65+
$q_key = $q_params[0];
66+
$q_params_count = count($q_params) - 1;
67+
$q_actions = ["startswith", "endswith", "contains", "lt", "lte", "gt", "gte"];
68+
$q_action = null;
69+
70+
# Set our action if one was requested and remove the action from the parameters
71+
if ($q_params_count > 0 and in_array($q_params[$q_params_count], $q_actions)) {
72+
$q_action = $q_params[$q_params_count];
73+
unset($q_params[$q_params_count]);
74+
$q_params_count = count($q_params) - 1;
75+
}
76+
77+
78+
# Only proceed if key is not excluded from queries
79+
if (!$this->is_excluded($q_key)) {
80+
# Always prefer exact matches first
81+
if ($entry[$key] === $value) {
82+
return true;
83+
}
84+
85+
# Loop through each query parameter and ensure it exists/matches the end value
86+
foreach ($q_params as $q) {
87+
if (array_key_exists($q, $entry)) {
88+
# Check if we've reach the last entry
89+
if ($q_count === $q_params_count) {
90+
# Determine our corresponding query action and return it's result
91+
switch ($q_action) {
92+
case "startswith":
93+
return $this->startswith($entry[$q], $value);
94+
case "endswith":
95+
return $this->endswith($entry[$q], $value);
96+
case "contains":
97+
return $this->contains($entry[$q], $value);
98+
case "lt":
99+
return $this->lt($entry[$q], $value);
100+
case "lte":
101+
return $this->lte($entry[$q], $value);
102+
case "gt":
103+
return $this->gt($entry[$q], $value);
104+
case "gte":
105+
return $this->gte($entry[$q], $value);
106+
default:
107+
return false;
108+
}
109+
} elseif (is_array($entry[$q])) {
110+
$entry = $entry[$q];
111+
} else {
112+
return false;
113+
}
114+
} else {
115+
return false;
116+
}
117+
$q_count++;
118+
}
119+
} else {
120+
return true;
121+
}
122+
}
123+
124+
# Checks if this value is excluded from queries
125+
private function is_excluded($value) {
126+
$excluded_keys = array_merge(["client-token", "client-id"], (array)$this->excluded);
127+
return in_array($value, $excluded_keys);
128+
}
129+
130+
# Checks if our target data starts with a specific sub string
131+
private function startswith($value, $substr) {
132+
if (substr(strval($value), 0, strlen(strval($substr))) === strval($substr)) {
133+
return true;
134+
}
135+
return false;
136+
}
137+
138+
# Checks if our target data ends with a specific sub string
139+
private function endswith($value, $substr) {
140+
if (substr(strval($value), -strlen(strval($substr))) === strval($substr)) {
141+
return true;
142+
}
143+
return false;
144+
}
145+
146+
# Checks if our target data contains a specific sub string
147+
private function contains($value, $substr) {
148+
if (strpos(strval($value), strval($substr)) !== false) {
149+
return true;
150+
}
151+
return false;
152+
}
153+
154+
# Checks if an integer or numeric string is less than a given value
155+
private function lt($value, $limit) {
156+
if (is_numeric($value) and is_numeric($value)) {
157+
if (intval($value) < intval($limit)) {
158+
return true;
159+
}
160+
}
161+
return false;
162+
}
163+
164+
# Checks if an integer or numeric string is less than or equal to a given value
165+
private function lte($value, $limit) {
166+
if (is_numeric($value) and is_numeric($value)) {
167+
if (intval($value) <= intval($limit)) {
168+
return true;
169+
}
170+
}
171+
return false;
172+
}
173+
174+
# Checks if an integer or numeric string is greater than a given value
175+
private function gt($value, $limit) {
176+
if (is_numeric($value) and is_numeric($value)) {
177+
if (intval($value) > intval($limit)) {
178+
return true;
179+
}
180+
}
181+
return false;
182+
}
183+
184+
# Checks if an integer or numeric string is greater than or equal to a given value
185+
private function gte($value, $limit) {
186+
if (is_numeric($value) and is_numeric($value)) {
187+
if (intval($value) >= intval($limit)) {
188+
return true;
189+
}
190+
}
191+
return false;
192+
}
193+
}

0 commit comments

Comments
 (0)