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 / admin / actions / RedirectFormResolver.php

RedirectFormResolver.php in 404 Solution 4.3.0, at includes/admin/actions/RedirectFormResolver.php

182 lines 7.3 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 * Owns parsing of the admin redirect form's destination field and the
9 * regex auto-promote policy. Used by both AddRedirectHandler (handler
10 * for the Add Redirect form) and EditRedirectHandler (handler for the
11 * Edit Redirect form). Extracted from PluginLogicAdminActions as part
12 * of M201 split (design-audit-2026-06-02).
13 *
14 * Two responsibilities, both genuinely shared:
15 *
16 * - getRedirectTypeAndDest(): translate the form's
17 * redirect_to_data_field_id POST value into a {type, dest, message}
18 * triple. Handles the external-URL branch (parse, validate scheme,
19 * filter through abj404_validate_external_redirect) and the
20 * internal post/page branch (split on '|').
21 *
22 * - maybeAutoPromoteRegex() + saveRegexAutoPromoteNotice(): if a
23 * manually entered URL pattern looks like an unambiguous regex,
24 * promote its status to ABJ404_STATUS_REGEX and apply a glob
25 * rewrite. The transient notice the admin sees afterwards is
26 * persisted by saveRegexAutoPromoteNotice().
27 *
28 * Neither belongs uniquely to Add or Edit; both must apply both. A
29 * shared resolver collapses the duplicated wiring previously inlined
30 * in addAdminRedirect() and updateRedirectData() into one place.
31 */
32 class ABJ_404_Solution_RedirectFormResolver {
33
34 /** @var ABJ_404_Solution_Functions */
35 private $f;
36
37 /** @var ABJ_404_Solution_Logging */
38 private $logger;
39
40 /** @var ABJ_404_Solution_PluginLogicUrlNormalization */
41 private $urlNormalization;
42
43 /**
44 * Parameters are intentionally untyped: the legacy DI sites that flow
45 * through PluginLogicAdminActions pass test doubles that don't extend the
46 * canonical classes (anonymous-class logger spies, custom Functions
47 * subclasses). The @param docblocks document the intent for static
48 * analysis. Matches the pattern in AdminActionsDependencies.
49 *
50 * @param ABJ_404_Solution_Functions $f
51 * @param ABJ_404_Solution_Logging $logger
52 * @param ABJ_404_Solution_PluginLogicUrlNormalization $urlNormalization
53 */
54 public function __construct($f, $logger, $urlNormalization) {
55 $this->f = $f;
56 $this->logger = $logger;
57 $this->urlNormalization = $urlNormalization;
58 }
59
60 /**
61 * Parse the redirect destination field from $_POST.
62 *
63 * @return array<string, mixed> {type: string, dest: string, message: string}
64 */
65 public function getRedirectTypeAndDest(): array {
66
67 $response = array();
68 $response['type'] = "";
69 $response['dest'] = "";
70 $response['message'] = "";
71 $userEnteredURL = '';
72
73 $postedCode = isset($_POST['code']) && is_scalar($_POST['code']) ? (string)$_POST['code'] : '';
74 if ($postedCode === '410' || $postedCode === '451') {
75 $response['type'] = (string)ABJ404_TYPE_HOME;
76 $response['dest'] = '';
77 return $response;
78 }
79
80 if (!isset($_POST['redirect_to_data_field_id']) || $_POST['redirect_to_data_field_id'] === '') {
81 $response['message'] = __('Error: Redirect destination is required.', '404-solution') . "<BR/>";
82 return $response;
83 }
84
85 if ($_POST['redirect_to_data_field_id'] == ABJ404_TYPE_EXTERNAL . '|' . ABJ404_TYPE_EXTERNAL) {
86 $rawEnteredURLResult = $this->f->getPostOrGetSanitizeUrl('redirect_to_user_field');
87 $rawEnteredURL = is_string($rawEnteredURLResult) ? $rawEnteredURLResult : null;
88 $userEnteredURL = $this->urlNormalization->normalizeExternalDestinationUrl($rawEnteredURL);
89 $userEnteredURL = esc_url($userEnteredURL, array('http', 'https'));
90 if ($userEnteredURL == "") {
91 $response['message'] = __('Error: You selected external URL but did not enter a URL.', '404-solution') . "<BR/>";
92
93 } else if ($this->f->strlen($userEnteredURL) < 8) {
94 $response['message'] = __('Error: External URL is too short.', '404-solution') . "<BR/>";
95
96 } else if ($this->f->strpos($userEnteredURL, "://") === false) {
97 $response['message'] = __("Error: External URL doesn't contain ://", '404-solution') . "<BR/>";
98
99 } else {
100 $parsed_url = parse_url($userEnteredURL);
101 if (!is_array($parsed_url) || !isset($parsed_url['scheme']) || !in_array(strtolower($parsed_url['scheme']), array('http', 'https'))) {
102 $response['message'] = __('Error: External URL must use http:// or https:// protocol only.', '404-solution') . "<BR/>";
103 }
104
105 $validated_url = apply_filters('abj404_validate_external_redirect', $userEnteredURL);
106 if ($validated_url === false) {
107 $response['message'] = __('Error: External redirect URL failed validation.', '404-solution') . "<BR/>";
108 } else {
109 $userEnteredURL = $validated_url;
110 }
111 }
112 }
113
114 if ($response['message'] != "") {
115 return $response;
116 }
117 $info = explode("|", sanitize_text_field($_POST['redirect_to_data_field_id']));
118
119 if ($_POST['redirect_to_data_field_id'] == ABJ404_TYPE_EXTERNAL . '|' . ABJ404_TYPE_EXTERNAL) {
120 $response['type'] = ABJ404_TYPE_EXTERNAL;
121 $response['dest'] = $userEnteredURL;
122 } else {
123 if (count($info) == 2) {
124 $response['dest'] = absint($info[0]);
125 $response['type'] = $info[1];
126 } else {
127 $infoJson = json_encode($info);
128 $this->logger->errorMessage("Unexpected info while updating redirect: " .
129 wp_kses_post(is_string($infoJson) ? $infoJson : ''));
130 }
131 }
132
133 return $response;
134 }
135
136 /**
137 * Decide whether a manually entered URL pattern should be auto-promoted
138 * to ABJ404_STATUS_REGEX and apply a glob rewrite when so.
139 *
140 * @param int $statusTypeIn
141 * @param string $fromURL
142 * @return array{statusType: int, url: string, autoPromoted: bool, urlRewritten: bool}
143 */
144 public function maybeAutoPromoteRegex($statusTypeIn, $fromURL): array {
145 $result = array(
146 'statusType' => (int)$statusTypeIn,
147 'url' => is_string($fromURL) ? $fromURL : '',
148 'autoPromoted' => false,
149 'urlRewritten' => false,
150 );
151
152 if ((int)$statusTypeIn === ABJ404_STATUS_REGEX) {
153 return $result;
154 }
155 if (!ABJ_404_Solution_RegexAutoPromote::looksLikeUnambiguousRegex($result['url'])) {
156 return $result;
157 }
158
159 $result['statusType'] = ABJ404_STATUS_REGEX;
160 $result['autoPromoted'] = true;
161 $glob = ABJ_404_Solution_RegexAutoPromote::applyGlobFixup($result['url']);
162 $result['url'] = $glob['url'];
163 $result['urlRewritten'] = $glob['changed'];
164
165 return $result;
166 }
167
168 /**
169 * Persist the transient notice the admin sees after a regex auto-promotion
170 * (so they can undo it).
171 *
172 * @param int $redirectId
173 * @param string $originalURL
174 * @param string $newURL
175 * @param bool $urlRewritten
176 * @return void
177 */
178 public function saveRegexAutoPromoteNotice($redirectId, $originalURL, $newURL, $urlRewritten): void {
179 ABJ_404_Solution_RegexAutoPromote::saveNotice($redirectId, $originalURL, $newURL, $urlRewritten);
180 }
181 }
182