PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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 / view / View_Redirects.php

View_Redirects.php in 404 Solution 4.3.3, at includes/view/View_Redirects.php

445 lines 19.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 * Edit redirect page and destination option helpers.
9 */
10 class ABJ_404_Solution_View_Redirects extends ABJ_404_Solution_ViewComponent {
11
12 /** @var ABJ_404_Solution_RedirectEditFormPresenter|null */
13 private $editFormPresenter = null;
14
15 /** @var ABJ_404_Solution_RedirectDestinationOptionsPresenter|null */
16 private $destinationOptionsPresenter = null;
17
18 /** @var ABJ_404_Solution_RedirectDestinationSuggestionService|null */
19 private $suggestionService = null;
20
21 /** @var ABJ_404_Solution_RedirectEngineLabeler|null */
22 private $engineLabeler = null;
23
24 /** @var ABJ_404_Solution_RedirectDestinationResolver|null */
25 private $destinationResolver = null;
26
27 /**
28 * @return ABJ_404_Solution_RedirectEditFormPresenter
29 */
30 private function editFormPresenter(): ABJ_404_Solution_RedirectEditFormPresenter {
31 if ($this->editFormPresenter === null) {
32 $this->editFormPresenter = new ABJ_404_Solution_RedirectEditFormPresenter(
33 $this->f,
34 $this->engineLabeler()
35 );
36 }
37 return $this->editFormPresenter;
38 }
39
40 /**
41 * @return ABJ_404_Solution_RedirectDestinationOptionsPresenter
42 */
43 private function destinationOptionsPresenter(): ABJ_404_Solution_RedirectDestinationOptionsPresenter {
44 if ($this->destinationOptionsPresenter === null) {
45 $this->destinationOptionsPresenter = new ABJ_404_Solution_RedirectDestinationOptionsPresenter();
46 }
47 return $this->destinationOptionsPresenter;
48 }
49
50 /**
51 * @return ABJ_404_Solution_RedirectDestinationSuggestionService
52 */
53 private function suggestionService(): ABJ_404_Solution_RedirectDestinationSuggestionService {
54 if ($this->suggestionService === null) {
55 $this->suggestionService = new ABJ_404_Solution_RedirectDestinationSuggestionService($this->logger);
56 }
57 return $this->suggestionService;
58 }
59
60 /**
61 * @return ABJ_404_Solution_RedirectEngineLabeler
62 */
63 private function engineLabeler(): ABJ_404_Solution_RedirectEngineLabeler {
64 if ($this->engineLabeler === null) {
65 $this->engineLabeler = new ABJ_404_Solution_RedirectEngineLabeler();
66 }
67 return $this->engineLabeler;
68 }
69
70
71 /**
72 * @return ABJ_404_Solution_RedirectDestinationResolver
73 */
74 private function destinationResolver(): ABJ_404_Solution_RedirectDestinationResolver {
75 if ($this->destinationResolver === null) {
76 $this->destinationResolver = new ABJ_404_Solution_RedirectDestinationResolver();
77 }
78 return $this->destinationResolver;
79 }
80
81 /**
82 * Resolve final destination, pageIDAndType, and redirect code from a redirect row.
83 *
84 * @param array<string, mixed> $redirect
85 * @param array<string, mixed> $options
86 * @return array{final: string, pageIDAndType: string, codeSelected: string}
87 */
88 public function resolveRedirectDestinationInfo(array $redirect, array $options): array {
89 return $this->destinationResolver()->resolveRedirectDestinationInfo($redirect, $options);
90 }
91
92 /**
93 * Build the redirect-to autocomplete dropdown HTML from the template.
94 *
95 * @param string $pageTitle
96 * @param string $pageIDAndType
97 * @return string
98 */
99 public function buildRedirectToDropdownHtml(string $pageTitle, string $pageIDAndType): string {
100 return $this->editFormPresenter()->buildRedirectToDropdownHtml($pageTitle, $pageIDAndType);
101 }
102
103
104 /**
105 * Build hidden input + form-table row HTML for bulk redirect editing.
106 *
107 * @param array<int, int> $recnums_multiple
108 * @return array{redirect: array<string, mixed>, redirects_multiple: array<int, array<string, mixed>>, hiddenInput: string, rowHtml: string}|null Null on error (already echoed).
109 */
110 public function renderBulkRedirectFormFields(array $recnums_multiple): ?array {
111 $redirects_multiple = $this->redirectsRepository->getRedirectsByIDs($recnums_multiple);
112 if ($redirects_multiple == null) {
113 echo "Error: Invalid ID Numbers! (ids: " . esc_html(implode(',', $recnums_multiple)) . ")";
114 $this->logger->debugMessage("Error: Invalid ID Numbers! (ids: " .
115 esc_html(implode(',', $recnums_multiple)) . ")");
116 return null;
117 }
118
119 $rowHtml = $this->editFormPresenter()->buildBulkUrlsRowHtml($redirects_multiple);
120 $hiddenInput = $this->editFormPresenter()->buildIdsMultipleHiddenInput($recnums_multiple);
121
122 // here we set the variable to the first value returned because it's used to set default values
123 // in the form data.
124 $redirect = reset($redirects_multiple);
125
126 return array(
127 'redirect' => $redirect,
128 'redirects_multiple' => $redirects_multiple,
129 'hiddenInput' => $hiddenInput,
130 'rowHtml' => $rowHtml,
131 );
132 }
133
134 /**
135 * Build the suggestion block HTML for a captured URL's best match.
136 *
137 * @param array{title: string, score: int, id_and_type: string, type_label: string} $suggestion
138 * @return string
139 */
140 public function buildSuggestionBlockHtml(array $suggestion): string {
141 return $this->editFormPresenter()->buildSuggestionBlockHtml($suggestion);
142 }
143
144 /**
145 * Render the suggestion block for a captured URL's best match.
146 *
147 * @param array{title: string, score: int, id_and_type: string, type_label: string} $suggestion
148 * @return void
149 */
150 public function renderSuggestionBlock(array $suggestion): void {
151 echo $this->buildSuggestionBlockHtml($suggestion);
152 }
153
154 /** @return void */
155 function echoAdminEditRedirectPage() {
156
157 $options = $this->optionsPresenter->getOptionsWithDefaults();
158 $context = $this->editRedirectPageContext();
159 $actionUrl = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_edit", "abj404editRedirect");
160 $content = $this->editRedirectRecordContent($context['isSimpleMode'], $context['hiddenInputs']);
161 if ($content === null) {
162 return;
163 }
164
165 $destInfo = $this->resolveRedirectDestinationInfo($content['redirect'], $options);
166 $final = $destInfo['final'];
167 $pageIDAndType = $destInfo['pageIDAndType'];
168 $codeSelected = $destInfo['codeSelected'];
169
170 $suggestion = null;
171 if ($context['isFromCaptured'] && !empty($content['redirectUrl'])) {
172 $suggestion = $this->getSuggestedDestination($content['redirectUrl'], $options);
173 }
174 $preTableBlock = '';
175 if ($suggestion !== null) {
176 $preTableBlock .= $this->buildSuggestionBlockHtml($suggestion);
177 }
178
179 // Redirect-to autocomplete row. When creating from captured URLs, clear the
180 // redirect_to field so the placeholder text is visible.
181 $rawFinalDest = $content['redirect']['final_dest'] ?? 0;
182 $redirectFinalDest = is_scalar($rawFinalDest) ? (string)$rawFinalDest : '0';
183 if ($context['isFromCaptured']) {
184 $pageTitle = '';
185 $pageIDAndType = '';
186 } else {
187 $pageTitle = $this->logic->pageOrdering()->getPageTitleFromIDAndType($pageIDAndType, $redirectFinalDest);
188 }
189 $manualPickerHiddenClass = ($suggestion !== null && $context['isSimpleMode']) ? ' abj404-hidden' : '';
190 $redirectToInner = $this->buildRedirectToDropdownHtml($pageTitle, $pageIDAndType);
191 $redirectToBody = $this->editFormPresenter()->buildManualPickerWrapperHtml($manualPickerHiddenClass, $redirectToInner);
192 $formRows = $content['formRows'];
193 $formRows .= $this->editFormPresenter()->buildFieldRowHtml(
194 'redirect_to_user_field',
195 $this->editFormPresenter()->buildRequiredLabel(__('Redirect to', '404-solution')),
196 $redirectToBody
197 );
198
199 // Capture the redirect-type button grid output and place it inside a form-table row.
200 ob_start();
201 $this->redirectTypeUI->echoRedirectTypeButtonGrid((string)$codeSelected);
202 $typeGridHtml = (string)ob_get_clean();
203 $formRows .= $this->editFormPresenter()->buildFieldRowHtml('code', esc_html__('Redirect Type', '404-solution'), $typeGridHtml);
204
205 // Build advanced options (dates + conditions).
206 $advancedOptions = $this->buildAdvancedOptionsHtml($content['startDate'], $content['endDate']);
207
208 // Compose the page using the shell template.
209 $cancelUrl = $this->editFormPresenter()->buildCancelUrl($context['sourcePage'], $context['filter'], $context['orderby'], $context['order']);
210
211 echo $this->editFormPresenter()->buildShellHtml(array(
212 '{title}' => esc_html($context['title']),
213 '{back_url}' => esc_url($context['backUrl']),
214 '{back_label}' => esc_html($context['backLabel']),
215 '{action_url}' => esc_attr($actionUrl),
216 '{hidden_inputs}' => $content['hiddenInputs'],
217 '{pre_table_block}' => $preTableBlock,
218 '{form_rows}' => $formRows,
219 '{advanced_options}' => $advancedOptions,
220 '{submit_label}' => esc_html__('Update Redirect', '404-solution'),
221 '{cancel_url}' => esc_url($cancelUrl),
222 '{cancel_label}' => esc_html__('Cancel', '404-solution'),
223 ));
224 }
225
226 /**
227 * @return array{sourcePage: string, backUrl: string, isSimpleMode: bool, isFromCaptured: bool, title: string, backLabel: string, filter: string, orderby: string, order: string, hiddenInputs: string}
228 */
229 private function editRedirectPageContext(): array {
230 $sourcePage = $this->shared->viewGetPostOrGetSanitize('source_page');
231 if ($sourcePage === '') {
232 $sourcePage = $this->shared->viewGetPostOrGetSanitize('subpage');
233 }
234 if ($sourcePage === '' || $sourcePage == 'abj404_edit') {
235 $sourcePage = 'abj404_redirects';
236 }
237
238 $isSimpleMode = abj_service('settings_mode_preference')->getMode() === 'simple';
239 $isFromCaptured = ($sourcePage === 'abj404_captured');
240 $filter = $this->shared->viewGetPostOrGetSanitize('filter');
241 $orderby = $this->shared->viewGetPostOrGetSanitize('orderby');
242 $order = $this->shared->viewGetPostOrGetSanitize('order');
243 $paged = $this->shared->viewGetPostOrGetSanitize('paged');
244
245 return array(
246 'sourcePage' => $sourcePage,
247 'backUrl' => '?page=' . ABJ404_PP . '&subpage=' . esc_attr($sourcePage),
248 'isSimpleMode' => $isSimpleMode,
249 'isFromCaptured' => $isFromCaptured,
250 'title' => ($isSimpleMode && $isFromCaptured) ? __('Create Redirect', '404-solution') : __('Edit Redirect', '404-solution'),
251 'backLabel' => ($isSimpleMode && $isFromCaptured) ? __('Back to Captured 404s', '404-solution') : __('Back to Redirects', '404-solution'),
252 'filter' => $filter,
253 'orderby' => $orderby,
254 'order' => $order,
255 'hiddenInputs' => $this->editFormPresenter()->buildSourceHiddenInputs($sourcePage, $filter, $orderby, $order, $paged),
256 );
257 }
258
259 /**
260 * @return array{redirect: array<string, mixed>, redirects_multiple: array<int, array<string, mixed>>, redirectUrl: string, startDate: string, endDate: string, hiddenInputs: string, formRows: string}|null
261 */
262 private function editRedirectRecordContent(bool $isSimpleMode, string $baseHiddenInputs): ?array {
263 $request = $this->editRedirectRequestedIds();
264 if ($request === null) {
265 echo __('Error: No ID(s) found for edit request.', '404-solution');
266 $this->logger->debugMessage("No ID(s) found in GET or POST data for edit request.");
267 return null;
268 }
269
270 if ($request['recnum'] !== null) {
271 $singleResult = $this->buildSingleRecordContent($request['recnum'], $isSimpleMode);
272 if ($singleResult === null) {
273 return null;
274 }
275 $singleResult['hiddenInputs'] = $baseHiddenInputs . $singleResult['hiddenInputs'];
276 return $singleResult;
277 }
278
279 $bulkResult = $this->renderBulkRedirectFormFields($request['recnumsMultiple']);
280 if ($bulkResult === null) {
281 return null;
282 }
283
284 return array(
285 'redirect' => $bulkResult['redirect'],
286 'redirects_multiple' => $bulkResult['redirects_multiple'],
287 'redirectUrl' => '',
288 'startDate' => '',
289 'endDate' => '',
290 'hiddenInputs' => $baseHiddenInputs . $bulkResult['hiddenInput'],
291 'formRows' => $bulkResult['rowHtml'],
292 );
293 }
294
295 /** @return array{recnum: int|null, recnumsMultiple: array<int, int>}|null */
296 private function editRedirectRequestedIds(): ?array {
297 if (isset($_GET['id']) && is_scalar($_GET['id']) && $this->f->regexMatch('[0-9]+', (string)$_GET['id'])) {
298 $this->logger->debugMessage("Edit redirect page. GET ID: " .
299 wp_kses_post((string)json_encode($_GET['id'])));
300 return array('recnum' => absint($_GET['id']), 'recnumsMultiple' => array());
301 }
302
303 if (isset($_POST['id']) && is_scalar($_POST['id']) && $this->f->regexMatch('[0-9]+', (string)$_POST['id'])) {
304 $this->logger->debugMessage("Edit redirect page. POST ID: " .
305 wp_kses_post((string)json_encode($_POST['id'])));
306 return array('recnum' => absint($_POST['id']), 'recnumsMultiple' => array());
307 }
308
309 if ($this->shared->viewGetPostOrGetSanitize('idnum') === '' && !isset($_GET['idnum']) && !isset($_POST['idnum'])) {
310 return null;
311 }
312
313 $rawIdnum = isset($_GET['idnum']) ? $_GET['idnum'] : (isset($_POST['idnum']) ? $_POST['idnum'] : $this->shared->viewGetPostOrGetSanitize('idnum'));
314 $recnumsMultiple = array_values(array_filter(array_map(function($v) { return absint($v); }, (array)$rawIdnum), function($v) { return $v > 0; }));
315 $this->logger->debugMessage("Edit redirect page. ids_multiple: " .
316 wp_kses_post((string)json_encode($recnumsMultiple)));
317 return array('recnum' => null, 'recnumsMultiple' => $recnumsMultiple);
318 }
319
320 /**
321 * Build the form-row HTML, hidden id input, and date strings for a single-record edit.
322 *
323 * @return array{redirect: array<string, mixed>, redirects_multiple: array<int, array<string, mixed>>, redirectUrl: string, startDate: string, endDate: string, hiddenInputs: string, formRows: string}|null Null on error (already echoed).
324 */
325 private function buildSingleRecordContent(int $recnum, bool $isSimpleMode): ?array {
326 $redirects_multiple = $this->redirectsRepository->getRedirectsByIDs(array($recnum));
327 if (empty($redirects_multiple)) {
328 echo "Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")";
329 $this->logger->errorMessage("Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")");
330 return null;
331 }
332
333 /** @var array<string, mixed> $redirect */
334 $redirect = reset($redirects_multiple);
335 $row = ABJ_404_Solution_RedirectRow::fromRaw($redirect);
336
337 $redirectId = $row !== null ? (string)$row->getId() : '';
338 $redirectUrl = $row !== null ? $row->getUrl() : '';
339 $redirectEngine = $row !== null ? $row->getEngine() : '';
340
341 $hiddenInputs = $this->editFormPresenter()->buildRedirectIdHiddenInput($redirectId);
342 $formRows = $this->editFormPresenter()->buildUrlRowHtml($redirectUrl, $redirectEngine);
343 $isRegexChecked = ($row !== null && $row->isRegex()) ? ' checked' : '';
344 $formRows .= $this->editFormPresenter()->buildRegexRowHtml($isRegexChecked);
345
346 $startTs = $row !== null ? $row->getStartTs() : 0;
347 $endTs = $row !== null ? $row->getEndTs() : 0;
348
349 return array(
350 'redirect' => $redirect,
351 'redirects_multiple' => $redirects_multiple,
352 'redirectUrl' => $redirectUrl,
353 'startDate' => $startTs > 0 ? ABJ_404_Solution_RedirectScheduleTimezone::toDateString($startTs) : '',
354 'endDate' => $endTs > 0 ? ABJ_404_Solution_RedirectScheduleTimezone::toDateString($endTs) : '',
355 'hiddenInputs' => $hiddenInputs,
356 'formRows' => $formRows,
357 );
358 }
359
360 /**
361 * Build the Advanced Options section (schedule + conditions) HTML using the template.
362 *
363 * @param string $startDate ISO date for "Active From", or empty.
364 * @param string $endDate ISO date for "Active Until", or empty.
365 * @return string
366 */
367 private function buildAdvancedOptionsHtml(string $startDate, string $endDate): string {
368 $redirectId = 0;
369 $rawId = $_GET['id'] ?? ($_POST['id'] ?? null);
370 if (is_scalar($rawId) && $this->f->regexMatch('[0-9]+', (string)$rawId)) {
371 $redirectId = absint((string)$rawId);
372 }
373 $hasExistingConditions = ($redirectId > 0) && !empty($this->redirectsRepository->getRedirectConditions($redirectId));
374 $hasAdvancedValues = ($startDate !== '' || $endDate !== '' || $hasExistingConditions);
375 $openAttr = $hasAdvancedValues ? ' open' : '';
376
377 ob_start();
378 $this->redirectConditions->echoRedirectConditionsSection();
379 $conditionsHtml = (string)ob_get_clean();
380
381 return $this->editFormPresenter()->buildAdvancedOptionsHtml(
382 $startDate,
383 $endDate,
384 $conditionsHtml,
385 $openAttr !== ''
386 );
387 }
388
389 /**
390 * @param string $dest
391 * @param array<int, object> $rows
392 * @return string
393 */
394 function echoRedirectDestinationOptionsOthers($dest, $rows) {
395 return $this->destinationOptionsPresenter()->buildPostOptions(
396 (string)$dest,
397 $rows,
398 function(string $debugInfo): void {
399 abj_service('request_context')->debug_info = $debugInfo;
400 }
401 );
402 }
403
404 /**
405 * @param string $dest
406 * @return string
407 */
408 function echoRedirectDestinationOptionsCatsTags($dest) {
409 $cats = $this->contentRepository->getPublishedCategories();
410 /** @var array<int, object{taxonomy: string, name?: string}> $cats */
411 $customTagsEtc = $this->logic->pageOrdering()->getMapOfCustomCategories($cats);
412 $tags = $this->contentRepository->getPublishedTags();
413 return $this->destinationOptionsPresenter()->buildTaxonomyOptions((string)$dest, $cats, $tags, $customTagsEtc);
414 }
415
416 /**
417 * Convert a raw engine class name to a human-readable label.
418 *
419 * Examples:
420 * TitleMatchingEngine → "Title Matching"
421 * SpellingMatchingEngine → "Spelling Matching"
422 * CategoryTagMatchingEngine → "Category/Tag Matching"
423 * UrlFixEngine → "URL Fix"
424 * ArchiveFallbackEngine → "Archive Fallback"
425 *
426 * @param string $rawName
427 * @return string
428 */
429 public function humanizeEngineName(string $rawName): string {
430 return $this->engineLabeler()->humanize($rawName);
431 }
432
433 /**
434 * Get the best suggested destination for a captured URL using the spell-checker.
435 *
436 * @param string $url The captured 404 URL.
437 * @param array<string, mixed> $options Plugin options.
438 * @return array{title: string, score: int, id_and_type: string, type_label: string}|null The best match, or null if none found.
439 */
440 public function getSuggestedDestination(string $url, array $options): ?array {
441 return $this->suggestionService()->getSuggestedDestination($url, $options);
442 }
443
444 }
445