PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / services / RestApiRequestParser.php

RestApiRequestParser.php in 404 Solution 4.3.0, at includes/services/RestApiRequestParser.php

220 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Parses and validates REST API request parameters for the abj404/v1 routes.
9 *
10 * This keeps scalar coercion, pagination limits, status filter mapping,
11 * redirect-code defaulting, and destination safety in one boundary module
12 * instead of scattering request parsing across route callbacks.
13 */
14 final class ABJ_404_Solution_RestApiRequestParser {
15
16 /**
17 * @param \WP_REST_Request $request
18 * @return array{page: int, per_page: int}
19 */
20 public function pagination($request): array {
21 $rawPage = $request->get_param('page');
22 $rawPerPage = $request->get_param('per_page');
23
24 return array(
25 'page' => max(1, absint(is_scalar($rawPage) ? $rawPage : 1)),
26 'per_page' => min(100, max(1, absint(is_scalar($rawPerPage) ? $rawPerPage : 20))),
27 );
28 }
29
30 /**
31 * @param \WP_REST_Request $request
32 * @return array{page: int, per_page: int, status_filter: string, filter_text: string}|WP_Error
33 */
34 public function redirectsList($request) {
35 $pagination = $this->pagination($request);
36 $rawStatus = $request->get_param('status');
37 $rawFilter = $request->get_param('filter');
38 $status = sanitize_text_field(is_scalar($rawStatus) ? (string)$rawStatus : '');
39 $filter = sanitize_text_field(is_scalar($rawFilter) ? (string)$rawFilter : '');
40
41 $statusFilter = $this->statusStringToNumericFilter($status);
42 if ($statusFilter === null) {
43 return new \WP_Error(
44 'invalid_status',
45 __('Unknown status filter. Valid values are: manual, auto, regex (or omit for all active).', '404-solution'),
46 array('status' => 400)
47 );
48 }
49
50 return array(
51 'page' => $pagination['page'],
52 'per_page' => $pagination['per_page'],
53 'status_filter' => $statusFilter,
54 'filter_text' => $filter,
55 );
56 }
57
58 /**
59 * @param \WP_REST_Request $request
60 * @return array{from: string, to: string, code: int, regex: bool}|WP_Error
61 */
62 public function createRedirect($request) {
63 $rawFrom = $request->get_param('from');
64 $rawTo = $request->get_param('to');
65 $rawCode = $request->get_param('code');
66 $rawRegex = $request->get_param('regex');
67
68 $from = trim(is_scalar($rawFrom) ? (string)$rawFrom : '');
69 $to = trim(is_scalar($rawTo) ? (string)$rawTo : '');
70 if ($from === '') {
71 return new \WP_Error('missing_from', __('The "from" URL is required.', '404-solution'), array('status' => 400));
72 }
73 if ($to === '') {
74 return new \WP_Error('missing_to', __('The "to" URL is required.', '404-solution'), array('status' => 400));
75 }
76 if (!$this->isSafeDestination($to)) {
77 return new \WP_Error('invalid_destination', __('The "to" URL must be a relative path or an http/https URL.', '404-solution'), array('status' => 400));
78 }
79
80 return array(
81 'from' => $from,
82 'to' => $to,
83 'code' => $this->redirectCode($rawCode),
84 'regex' => (bool)$rawRegex,
85 );
86 }
87
88 /**
89 * @param \WP_REST_Request $request
90 * @return array{id: int, from: string, to: string, code: int, regex: bool}|WP_Error
91 */
92 public function updateRedirect($request) {
93 $rawId = $request->get_param('id');
94 $rawFrom = $request->get_param('from');
95 $rawTo = $request->get_param('to');
96 $rawCode = $request->get_param('code');
97 $rawRegex = $request->get_param('regex');
98
99 $id = absint(is_scalar($rawId) ? $rawId : 0);
100 $from = trim(is_scalar($rawFrom) ? (string)$rawFrom : '');
101 $to = trim(is_scalar($rawTo) ? (string)$rawTo : '');
102
103 if ($id <= 0) {
104 return new \WP_Error('invalid_id', __('Invalid redirect ID.', '404-solution'), array('status' => 400));
105 }
106 if ($from === '' || $to === '') {
107 return new \WP_Error('missing_params', __('Both "from" and "to" parameters are required.', '404-solution'), array('status' => 400));
108 }
109 if (!$this->isSafeDestination($to)) {
110 return new \WP_Error('invalid_destination', __('The "to" URL must be a relative path or an http/https URL.', '404-solution'), array('status' => 400));
111 }
112
113 return array(
114 'id' => $id,
115 'from' => $from,
116 'to' => $to,
117 'code' => $this->redirectCode($rawCode),
118 'regex' => (bool)$rawRegex,
119 );
120 }
121
122 /**
123 * @param \WP_REST_Request $request
124 * @return array{id: int, to: string, code: int}|WP_Error
125 */
126 public function capturedRedirect($request) {
127 $rawId = $request->get_param('id');
128 $rawTo = $request->get_param('to');
129 $rawCode = $request->get_param('code');
130
131 $id = absint(is_scalar($rawId) ? $rawId : 0);
132 $to = trim(is_scalar($rawTo) ? (string)$rawTo : '');
133
134 if ($id <= 0) {
135 return new \WP_Error('invalid_id', __('Invalid captured 404 ID.', '404-solution'), array('status' => 400));
136 }
137 if ($to === '') {
138 return new \WP_Error('missing_to', __('The "to" URL is required.', '404-solution'), array('status' => 400));
139 }
140 if (!$this->isSafeDestination($to)) {
141 return new \WP_Error('invalid_destination', __('The "to" URL must be a relative path or an http/https URL.', '404-solution'), array('status' => 400));
142 }
143
144 return array(
145 'id' => $id,
146 'to' => $to,
147 'code' => $this->redirectCode($rawCode),
148 );
149 }
150
151 /**
152 * @param \WP_REST_Request $request
153 * @return array{id: int}|WP_Error
154 */
155 public function redirectId($request) {
156 $rawId = $request->get_param('id');
157 $id = absint(is_scalar($rawId) ? $rawId : 0);
158
159 if ($id <= 0) {
160 return new \WP_Error('invalid_id', __('Invalid redirect ID.', '404-solution'), array('status' => 400));
161 }
162
163 return array('id' => $id);
164 }
165
166 /**
167 * @param \WP_REST_Request $request
168 * @return array{url: string}|WP_Error
169 */
170 public function testRedirect($request) {
171 $rawUrl = $request->get_param('url');
172 if ($rawUrl === null) {
173 return new \WP_Error('missing_url', __('The "url" parameter is required.', '404-solution'), array('status' => 400));
174 }
175
176 return array('url' => trim(is_scalar($rawUrl) ? (string)$rawUrl : ''));
177 }
178
179 /**
180 * @param string $status
181 * @return string|null
182 */
183 public function statusStringToNumericFilter($status) {
184 if ($status === '') {
185 return '0';
186 }
187 switch (strtolower($status)) {
188 case 'manual':
189 return (string)ABJ404_STATUS_MANUAL;
190 case 'auto':
191 return (string)ABJ404_STATUS_AUTO;
192 case 'regex':
193 return (string)ABJ404_STATUS_REGEX;
194 }
195
196 return null;
197 }
198
199 /**
200 * @param mixed $rawCode
201 * @return int
202 */
203 private function redirectCode($rawCode): int {
204 $code = absint(is_scalar($rawCode) ? $rawCode : 301);
205 return in_array($code, array(301, 302), true) ? $code : 301;
206 }
207
208 private function isSafeDestination(string $to): bool {
209 if (strncasecmp($to, 'http://', 7) === 0 || strncasecmp($to, 'https://', 8) === 0) {
210 return true;
211 }
212 if (strpos($to, '//') === 0) {
213 return false;
214 }
215
216 $scheme = parse_url($to, PHP_URL_SCHEME);
217 return $scheme === null || $scheme === false || $scheme === '';
218 }
219 }
220