| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Edit redirect page and destination option helpers. |
| 9 |
*/ |
| 10 |
trait ViewTrait_Redirects { |
| 11 |
|
| 12 |
|
| 13 |
/** |
| 14 |
* Resolve final destination, pageIDAndType, and redirect code from a redirect row. |
| 15 |
* |
| 16 |
* @param array<string, mixed> $redirect |
| 17 |
* @param array<string, mixed> $options |
| 18 |
* @return array{final: string, pageIDAndType: string, codeSelected: string} |
| 19 |
*/ |
| 20 |
private function resolveRedirectDestinationInfo(array $redirect, array $options): array { |
| 21 |
$final = ""; |
| 22 |
$pageIDAndType = ""; |
| 23 |
$redirectType = $redirect['type'] ?? null; |
| 24 |
$redirectFinalDestRaw = $redirect['final_dest'] ?? 0; |
| 25 |
$redirectFinalDest = is_scalar($redirectFinalDestRaw) ? (string)$redirectFinalDestRaw : '0'; |
| 26 |
if ($redirectType == ABJ404_TYPE_EXTERNAL) { |
| 27 |
$final = $redirectFinalDest; |
| 28 |
$pageIDAndType = ABJ404_TYPE_EXTERNAL . "|" . ABJ404_TYPE_EXTERNAL; |
| 29 |
|
| 30 |
} else if ($redirectFinalDest != 0) { |
| 31 |
// if a destination has been specified then let's fill it in. |
| 32 |
$pageIDAndType = $redirectFinalDest . "|" . $redirectType; |
| 33 |
|
| 34 |
} else if ($redirectType == ABJ404_TYPE_404_DISPLAYED) { |
| 35 |
$pageIDAndType = ABJ404_TYPE_404_DISPLAYED . "|" . ABJ404_TYPE_404_DISPLAYED; |
| 36 |
} |
| 37 |
|
| 38 |
$rawCode = $redirect['code'] ?? ''; |
| 39 |
if ($rawCode == "") { |
| 40 |
$rawDefault = $options['default_redirect'] ?? '301'; |
| 41 |
$codeSelected = is_string($rawDefault) ? $rawDefault : '301'; |
| 42 |
} else { |
| 43 |
$codeSelected = is_string($rawCode) ? $rawCode : '301'; |
| 44 |
} |
| 45 |
|
| 46 |
return array('final' => $final, 'pageIDAndType' => $pageIDAndType, 'codeSelected' => $codeSelected); |
| 47 |
} |
| 48 |
|
| 49 |
/** |
| 50 |
* Build the redirect-to autocomplete dropdown HTML from the template. |
| 51 |
* |
| 52 |
* @param string $pageTitle |
| 53 |
* @param string $pageIDAndType |
| 54 |
* @return string |
| 55 |
*/ |
| 56 |
private function buildRedirectToDropdownHtml(string $pageTitle, string $pageIDAndType): string { |
| 57 |
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . |
| 58 |
"/html/addManualRedirectPageSearchDropdown.html"); |
| 59 |
$html = $this->f->str_replace('{redirect_to_label}', __('Redirect to', '404-solution'), $html); |
| 60 |
$html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_EMPTY}', |
| 61 |
__('(Type a page name or an external URL)', '404-solution'), $html); |
| 62 |
$html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_PAGE}', |
| 63 |
__('(A page has been selected.)', '404-solution'), $html); |
| 64 |
$html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_CUSTOM_STRING}', |
| 65 |
__('(A custom string has been entered.)', '404-solution'), $html); |
| 66 |
$html = $this->f->str_replace('{TOOLTIP_POPUP_EXPLANATION_URL}', |
| 67 |
__('(An external URL will be used.)', '404-solution'), $html); |
| 68 |
$html = $this->f->str_replace('{REDIRECT_TO_USER_FIELD_WARNING}', '', $html); |
| 69 |
$html = $this->f->str_replace('{redirectPageTitle}', esc_attr($pageTitle), $html); |
| 70 |
$html = $this->f->str_replace('{pageIDAndType}', esc_attr($pageIDAndType), $html); |
| 71 |
$html = $this->f->str_replace('{data-url}', |
| 72 |
"admin-ajax.php?action=echoRedirectToPages&includeDefault404Page=true&includeSpecial=true&nonce=" . wp_create_nonce('abj404_ajax'), $html); |
| 73 |
$html = $this->f->doNormalReplacements($html); |
| 74 |
return $html; |
| 75 |
} |
| 76 |
|
| 77 |
|
| 78 |
/** |
| 79 |
* Render hidden inputs and URL list for bulk redirect editing. |
| 80 |
* |
| 81 |
* @param array<int, int> $recnums_multiple |
| 82 |
* @return array{redirect: array<string, mixed>, redirects_multiple: array<int, array<string, mixed>>}|null Null on error (already echoed). |
| 83 |
*/ |
| 84 |
private function renderBulkRedirectFormFields(array $recnums_multiple): ?array { |
| 85 |
$redirects_multiple = $this->redirectsRepository->getRedirectsByIDs($recnums_multiple); |
| 86 |
if ($redirects_multiple == null) { |
| 87 |
echo "Error: Invalid ID Numbers! (ids: " . esc_html(implode(',', $recnums_multiple)) . ")"; |
| 88 |
$this->logger->debugMessage("Error: Invalid ID Numbers! (ids: " . |
| 89 |
esc_html(implode(',', $recnums_multiple)) . ")"); |
| 90 |
return null; |
| 91 |
} |
| 92 |
|
| 93 |
echo '<input type="hidden" name="ids_multiple" value="' . esc_attr(implode(',', $recnums_multiple)) . '">'; |
| 94 |
|
| 95 |
// Bulk URL list |
| 96 |
echo '<div class="abj404-form-group">'; |
| 97 |
echo '<label class="abj404-form-label">' . esc_html__('URLs to redirect', '404-solution') . ' (' . count($redirects_multiple) . ')</label>'; |
| 98 |
echo '<div class="abj404-url-list">'; |
| 99 |
echo '<ul>'; |
| 100 |
foreach ($redirects_multiple as $bulkRedirect) { |
| 101 |
/** @var array<string, mixed> $bulkRedirect */ |
| 102 |
$bulkUrl = is_string($bulkRedirect['url'] ?? '') ? (string)($bulkRedirect['url'] ?? '') : ''; |
| 103 |
echo '<li><code>' . esc_html($bulkUrl) . '</code></li>'; |
| 104 |
} |
| 105 |
echo '</ul>'; |
| 106 |
echo '</div>'; |
| 107 |
echo '</div>'; |
| 108 |
|
| 109 |
// here we set the variable to the first value returned because it's used to set default values |
| 110 |
// in the form data. |
| 111 |
$redirect = reset($redirects_multiple); |
| 112 |
|
| 113 |
return array( |
| 114 |
'redirect' => $redirect, |
| 115 |
'redirects_multiple' => $redirects_multiple, |
| 116 |
); |
| 117 |
} |
| 118 |
|
| 119 |
/** |
| 120 |
* Render the suggestion block for a captured URL's best match. |
| 121 |
* |
| 122 |
* @param array{title: string, score: int, id_and_type: string, type_label: string} $suggestion |
| 123 |
* @return void |
| 124 |
*/ |
| 125 |
private function renderSuggestionBlock(array $suggestion): void { |
| 126 |
echo '<div class="abj404-suggestion-block" id="abj404-suggestion-block">'; |
| 127 |
echo '<div class="abj404-suggestion-label">' . esc_html__('Suggested destination', '404-solution') . '</div>'; |
| 128 |
echo '<div class="abj404-suggestion-content">'; |
| 129 |
echo '<strong>' . esc_html($suggestion['title']) . '</strong>'; |
| 130 |
if (!empty($suggestion['type_label'])) { |
| 131 |
echo '<span class="abj404-suggestion-type">' . esc_html($suggestion['type_label']) . '</span>'; |
| 132 |
} |
| 133 |
echo '<span class="abj404-score-badge abj404-score-' . ($suggestion['score'] >= 75 ? 'high' : ($suggestion['score'] >= 50 ? 'medium' : 'low')) . '">' |
| 134 |
. esc_html($suggestion['score'] . '%') . ' ' . esc_html__('match', '404-solution') . '</span>'; |
| 135 |
echo '</div>'; |
| 136 |
echo '<div class="abj404-suggestion-actions">'; |
| 137 |
echo '<button type="button" class="button button-primary" onclick="abj404AcceptSuggestion(this)"' |
| 138 |
. ' data-page-title="' . esc_attr($suggestion['title']) . '"' |
| 139 |
. ' data-page-id-type="' . esc_attr($suggestion['id_and_type']) . '">' |
| 140 |
. esc_html__('Accept Suggestion', '404-solution') . '</button>'; |
| 141 |
echo '<button type="button" class="button" onclick="abj404ShowManualPicker()">' |
| 142 |
. esc_html__('Pick a Different Page', '404-solution') . '</button>'; |
| 143 |
echo '</div>'; |
| 144 |
echo '</div>'; |
| 145 |
} |
| 146 |
|
| 147 |
/** |
| 148 |
* Render hidden inputs, URL field, regex checkbox and date fields for a single redirect. |
| 149 |
* |
| 150 |
* @param int $recnum |
| 151 |
* @param bool $isSimpleMode |
| 152 |
* @return array{redirect: array<string, mixed>, redirects_multiple: array<int, array<string, mixed>>, redirectUrl: string, startDate: string, endDate: string}|null Null on error (already echoed). |
| 153 |
*/ |
| 154 |
private function renderSingleRedirectFormFields(int $recnum, bool $isSimpleMode): ?array { |
| 155 |
$recnumAsArray = array(); |
| 156 |
$recnumAsArray[] = $recnum; |
| 157 |
$redirects_multiple = $this->redirectsRepository->getRedirectsByIDs($recnumAsArray); |
| 158 |
|
| 159 |
if (empty($redirects_multiple)) { |
| 160 |
echo "Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")"; |
| 161 |
$this->logger->errorMessage("Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")"); |
| 162 |
return null; |
| 163 |
} |
| 164 |
|
| 165 |
/** @var array<string, mixed> $redirect */ |
| 166 |
$redirect = reset($redirects_multiple); |
| 167 |
$row = ABJ_404_Solution_RedirectRow::fromRaw($redirect); |
| 168 |
$isRegexChecked = ''; |
| 169 |
if ($row !== null && $row->isRegex()) { |
| 170 |
$isRegexChecked = ' checked '; |
| 171 |
} |
| 172 |
|
| 173 |
$redirectId = $row !== null ? (string)$row->getId() : ''; |
| 174 |
$redirectUrl = $row !== null ? $row->getUrl() : ''; |
| 175 |
echo '<input type="hidden" name="id" value="' . esc_attr($redirectId) . '">'; |
| 176 |
|
| 177 |
// URL field (with optional "Matched by" note for auto-created redirects) |
| 178 |
echo '<div class="abj404-form-group">'; |
| 179 |
echo '<label class="abj404-form-label" for="url">' . esc_html__('URL', '404-solution') . ' *</label>'; |
| 180 |
echo '<input type="text" id="url" name="url" class="abj404-form-input" value="' . esc_attr($redirectUrl) . '" required>'; |
| 181 |
$redirectEngine = $row !== null ? $row->getEngine() : ''; |
| 182 |
if ($redirectEngine !== '') { |
| 183 |
$humanEngine = $this->humanizeEngineName($redirectEngine); |
| 184 |
echo '<p class="abj404-form-help abj404-matched-by">' . esc_html__('Auto-matched by:', '404-solution') . ' ' . esc_html($humanEngine) . '</p>'; |
| 185 |
} |
| 186 |
echo '</div>'; |
| 187 |
|
| 188 |
// Regex checkbox (hidden in Simple mode) |
| 189 |
if (!$isSimpleMode) { |
| 190 |
echo '<div class="abj404-form-group">'; |
| 191 |
echo '<div class="abj404-checkbox-group">'; |
| 192 |
echo '<input type="checkbox" name="is_regex_url" id="is_regex_url" class="abj404-checkbox-input" value="1" ' . $isRegexChecked . '>'; |
| 193 |
echo '<label for="is_regex_url" class="abj404-checkbox-label">' . esc_html__('Treat this URL as a regular expression', '404-solution') . '</label>'; |
| 194 |
echo ' <a href="#" class="abj404-regex-toggle" onclick="abj404ToggleRegexInfo(event)">' . esc_html__('(Explain)', '404-solution') . '</a>'; |
| 195 |
echo '</div>'; |
| 196 |
echo '<div class="abj404-regex-info" style="display: none;">'; |
| 197 |
echo '<p>' . esc_html__('When checked, the text is treated as a regular expression. Note that including a bad regular expression or one that takes too long will break your website. So please use caution and test them elsewhere before trying them here. If you don\'t know what you\'re doing please don\'t use this option (as it\'s not necessary for the functioning of the plugin).', '404-solution') . '</p>'; |
| 198 |
echo '<p><strong>' . esc_html__('Example:', '404-solution') . '</strong> <code>/events/(.+)</code></p>'; |
| 199 |
echo '<p>' . esc_html__('/events/(.+) will match any URL that begins with /events/ and redirect to the specified page. Since a capture group is used, you can use a $1 replacement in the destination string of an external URL.', '404-solution') . '</p>'; |
| 200 |
echo '</div>'; |
| 201 |
echo '</div>'; |
| 202 |
} |
| 203 |
|
| 204 |
// Scheduled redirect dates (rendered inside Advanced Options in echoEditRedirect) |
| 205 |
$startTs = $row !== null ? $row->getStartTs() : 0; |
| 206 |
$endTs = $row !== null ? $row->getEndTs() : 0; |
| 207 |
$startDate = $startTs > 0 ? date('Y-m-d', $startTs) : ''; |
| 208 |
$endDate = $endTs > 0 ? date('Y-m-d', $endTs) : ''; |
| 209 |
|
| 210 |
return array( |
| 211 |
'redirect' => $redirect, |
| 212 |
'redirects_multiple' => $redirects_multiple, |
| 213 |
'redirectUrl' => $redirectUrl, |
| 214 |
'startDate' => $startDate, |
| 215 |
'endDate' => $endDate, |
| 216 |
); |
| 217 |
} |
| 218 |
|
| 219 |
/** @return void */ |
| 220 |
function echoAdminEditRedirectPage() { |
| 221 |
|
| 222 |
$options = $this->getOptionsWithDefaults(); |
| 223 |
|
| 224 |
// Compute source page early so we can use it in the back link |
| 225 |
$source_page = $this->viewGetPostOrGetSanitize('source_page'); |
| 226 |
if ($source_page === '') { |
| 227 |
$source_page = $this->viewGetPostOrGetSanitize('subpage'); |
| 228 |
} |
| 229 |
if ($source_page === '' || $source_page == 'abj404_edit') { |
| 230 |
$source_page = 'abj404_redirects'; |
| 231 |
} |
| 232 |
$backUrl = '?page=' . ABJ404_PP . '&subpage=' . esc_attr($source_page); |
| 233 |
|
| 234 |
// Modern page container |
| 235 |
echo '<div class="abj404-edit-page">'; |
| 236 |
echo '<div class="abj404-edit-container">'; |
| 237 |
|
| 238 |
// Header row: title + back link |
| 239 |
$isSimpleMode = $this->logic->getSettingsMode() === 'simple'; |
| 240 |
$isFromCaptured = ($source_page === 'abj404_captured'); |
| 241 |
echo '<div class="abj404-edit-page-header">'; |
| 242 |
if ($isSimpleMode && $isFromCaptured) { |
| 243 |
echo '<h2>' . esc_html__('Create Redirect', '404-solution') . '</h2>'; |
| 244 |
echo '<a href="' . esc_url($backUrl) . '" class="abj404-back-link">← ' . esc_html__('Back to Captured 404s', '404-solution') . '</a>'; |
| 245 |
} else { |
| 246 |
echo '<h2>' . esc_html__('Edit Redirect', '404-solution') . '</h2>'; |
| 247 |
echo '<a href="' . esc_url($backUrl) . '" class="abj404-back-link">← ' . esc_html__('Back to Redirects', '404-solution') . '</a>'; |
| 248 |
} |
| 249 |
echo '</div>'; |
| 250 |
|
| 251 |
$link = wp_nonce_url("?page=" . ABJ404_PP . "&subpage=abj404_edit", "abj404editRedirect"); |
| 252 |
|
| 253 |
echo '<form method="POST" name="admin-edit-redirect" action="' . esc_attr($link) . '" onsubmit="return validateAddManualRedirectForm(event);">'; |
| 254 |
echo "<input type=\"hidden\" name=\"action\" value=\"editRedirect\">"; |
| 255 |
|
| 256 |
// Preserve source page for return navigation |
| 257 |
echo "<input type=\"hidden\" name=\"source_page\" value=\"" . esc_attr($source_page) . "\">"; |
| 258 |
|
| 259 |
// Preserve table options so we can return to the exact same view |
| 260 |
$filter = $this->viewGetPostOrGetSanitize('filter'); |
| 261 |
if ($filter !== '') { |
| 262 |
echo "<input type=\"hidden\" name=\"source_filter\" value=\"" . esc_attr($filter) . "\">"; |
| 263 |
} |
| 264 |
$orderby = $this->viewGetPostOrGetSanitize('orderby'); |
| 265 |
if ($orderby !== '') { |
| 266 |
echo "<input type=\"hidden\" name=\"source_orderby\" value=\"" . esc_attr($orderby) . "\">"; |
| 267 |
} |
| 268 |
$order = $this->viewGetPostOrGetSanitize('order'); |
| 269 |
if ($order !== '') { |
| 270 |
echo "<input type=\"hidden\" name=\"source_order\" value=\"" . esc_attr($order) . "\">"; |
| 271 |
} |
| 272 |
$paged = $this->viewGetPostOrGetSanitize('paged'); |
| 273 |
if ($paged !== '') { |
| 274 |
echo "<input type=\"hidden\" name=\"source_paged\" value=\"" . esc_attr($paged) . "\">"; |
| 275 |
} |
| 276 |
|
| 277 |
$recnum = null; |
| 278 |
$recnums_multiple = null; |
| 279 |
$startDate = ''; |
| 280 |
$endDate = ''; |
| 281 |
if (isset($_GET['id']) && $this->f->regexMatch('[0-9]+', $_GET['id'])) { |
| 282 |
$this->logger->debugMessage("Edit redirect page. GET ID: " . |
| 283 |
wp_kses_post((string)json_encode($_GET['id']))); |
| 284 |
$recnum = absint($_GET['id']); |
| 285 |
|
| 286 |
} else if (isset($_POST['id']) && $this->f->regexMatch('[0-9]+', $_POST['id'])) { |
| 287 |
$this->logger->debugMessage("Edit redirect page. POST ID: " . |
| 288 |
wp_kses_post((string)json_encode($_POST['id']))); |
| 289 |
$recnum = absint($_POST['id']); |
| 290 |
|
| 291 |
} else if ($this->viewGetPostOrGetSanitize('idnum') !== '' || isset($_GET['idnum']) || isset($_POST['idnum'])) { |
| 292 |
$rawIdnum = isset($_GET['idnum']) ? $_GET['idnum'] : (isset($_POST['idnum']) ? $_POST['idnum'] : $this->viewGetPostOrGetSanitize('idnum')); |
| 293 |
$recnums_multiple = array_values(array_filter(array_map(function($v) { return absint($v); }, (array)$rawIdnum), function($v) { return $v > 0; })); |
| 294 |
$this->logger->debugMessage("Edit redirect page. ids_multiple: " . |
| 295 |
wp_kses_post((string)json_encode($recnums_multiple))); |
| 296 |
|
| 297 |
} else { |
| 298 |
echo __('Error: No ID(s) found for edit request.', '404-solution'); |
| 299 |
$this->logger->debugMessage("No ID(s) found in GET or POST data for edit request."); |
| 300 |
return; |
| 301 |
} |
| 302 |
|
| 303 |
// Decide whether we're editing one or multiple redirects. |
| 304 |
// If we're editing only one then set the ID to that one value. |
| 305 |
if ($recnum != null) { |
| 306 |
$recnumAsArray = array(); |
| 307 |
$recnumAsArray[] = $recnum; |
| 308 |
$redirects_multiple = $this->redirectsRepository->getRedirectsByIDs($recnumAsArray); |
| 309 |
|
| 310 |
if (empty($redirects_multiple)) { |
| 311 |
echo "Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")"; |
| 312 |
$this->logger->errorMessage("Error: Invalid ID Number! (id: " . esc_html((string)$recnum) . ")"); |
| 313 |
return; |
| 314 |
} |
| 315 |
|
| 316 |
/** @var array<string, mixed> $redirect */ |
| 317 |
$redirect = reset($redirects_multiple); |
| 318 |
$row = ABJ_404_Solution_RedirectRow::fromRaw($redirect); |
| 319 |
$isRegexChecked = ''; |
| 320 |
if ($row !== null && $row->isRegex()) { |
| 321 |
$isRegexChecked = ' checked '; |
| 322 |
} |
| 323 |
|
| 324 |
$redirectId = $row !== null ? (string)$row->getId() : ''; |
| 325 |
$redirectUrl = $row !== null ? $row->getUrl() : ''; |
| 326 |
echo '<input type="hidden" name="id" value="' . esc_attr($redirectId) . '">'; |
| 327 |
|
| 328 |
// URL field (with optional "Matched by" note for auto-created redirects) |
| 329 |
echo '<div class="abj404-form-group">'; |
| 330 |
echo '<label class="abj404-form-label" for="url">' . esc_html__('URL', '404-solution') . ' *</label>'; |
| 331 |
echo '<input type="text" id="url" name="url" class="abj404-form-input" value="' . esc_attr($redirectUrl) . '" required>'; |
| 332 |
$redirectEngine = $row !== null ? $row->getEngine() : ''; |
| 333 |
if ($redirectEngine !== '') { |
| 334 |
$humanEngine = $this->humanizeEngineName($redirectEngine); |
| 335 |
echo '<p class="abj404-form-help abj404-matched-by">' . esc_html__('Auto-matched by:', '404-solution') . ' ' . esc_html($humanEngine) . '</p>'; |
| 336 |
} |
| 337 |
echo '</div>'; |
| 338 |
|
| 339 |
// Regex checkbox (hidden in Simple mode) |
| 340 |
if (!$isSimpleMode) { |
| 341 |
echo '<div class="abj404-form-group">'; |
| 342 |
echo '<div class="abj404-checkbox-group">'; |
| 343 |
echo '<input type="checkbox" name="is_regex_url" id="is_regex_url" class="abj404-checkbox-input" value="1" ' . $isRegexChecked . '>'; |
| 344 |
echo '<label for="is_regex_url" class="abj404-checkbox-label">' . esc_html__('Treat this URL as a regular expression', '404-solution') . '</label>'; |
| 345 |
echo ' <a href="#" class="abj404-regex-toggle" onclick="abj404ToggleRegexInfo(event)">' . esc_html__('(Explain)', '404-solution') . '</a>'; |
| 346 |
echo '</div>'; |
| 347 |
echo '<div class="abj404-regex-info" style="display: none;">'; |
| 348 |
echo '<p>' . esc_html__('When checked, the text is treated as a regular expression. Note that including a bad regular expression or one that takes too long will break your website. So please use caution and test them elsewhere before trying them here. If you don\'t know what you\'re doing please don\'t use this option (as it\'s not necessary for the functioning of the plugin).', '404-solution') . '</p>'; |
| 349 |
echo '<p><strong>' . esc_html__('Example:', '404-solution') . '</strong> <code>/events/(.+)</code></p>'; |
| 350 |
echo '<p>' . esc_html__('/events/(.+) will match any URL that begins with /events/ and redirect to the specified page. Since a capture group is used, you can use a $1 replacement in the destination string of an external URL.', '404-solution') . '</p>'; |
| 351 |
echo '</div>'; |
| 352 |
echo '</div>'; |
| 353 |
} |
| 354 |
|
| 355 |
// Scheduled redirect dates (rendered inside Advanced Options in echoEditRedirect) |
| 356 |
$startTs = $row !== null ? $row->getStartTs() : 0; |
| 357 |
$endTs = $row !== null ? $row->getEndTs() : 0; |
| 358 |
$startDate = $startTs > 0 ? date('Y-m-d', $startTs) : ''; |
| 359 |
$endDate = $endTs > 0 ? date('Y-m-d', $endTs) : ''; |
| 360 |
|
| 361 |
} else if ($recnums_multiple != null) { |
| 362 |
$bulkResult = $this->renderBulkRedirectFormFields($recnums_multiple); |
| 363 |
if ($bulkResult === null) { |
| 364 |
return; |
| 365 |
} |
| 366 |
$redirect = $bulkResult['redirect']; |
| 367 |
$redirects_multiple = $bulkResult['redirects_multiple']; |
| 368 |
$redirectUrl = ''; |
| 369 |
|
| 370 |
} else { |
| 371 |
$idsText = isset($rawIdnum) && is_array($rawIdnum) ? implode(',', array_map(function($v) { return is_scalar($v) ? (string)$v : ''; }, $rawIdnum)) : ''; |
| 372 |
echo $errorText = ($recnum === 0 || $idsText !== '') ? "Error: Invalid ID Number(s) specified! (id: " . esc_html((string)$recnum) . ", ids: " . esc_html($idsText) . ")" : __('Error: No ID(s) found for edit request.', '404-solution'); |
| 373 |
$this->logger->debugMessage($errorText . " (id: " . esc_html((string)$recnum) . |
| 374 |
", ids: " . esc_html($idsText) . ")"); |
| 375 |
return; |
| 376 |
} |
| 377 |
|
| 378 |
$destInfo = $this->resolveRedirectDestinationInfo($redirect, $options); |
| 379 |
$final = $destInfo['final']; |
| 380 |
$pageIDAndType = $destInfo['pageIDAndType']; |
| 381 |
$codeSelected = $destInfo['codeSelected']; |
| 382 |
|
| 383 |
// Try to find a suggested destination for captured URLs. |
| 384 |
// Any captured URL should get a suggestion lookup — the plugin may have auto-assigned |
| 385 |
// a destination via the spell-checker, but the user hasn't chosen one yet. |
| 386 |
$suggestion = null; |
| 387 |
$isSimpleMode = $this->logic->getSettingsMode() === 'simple'; |
| 388 |
if ($isFromCaptured && !empty($redirectUrl)) { |
| 389 |
$suggestion = $this->getSuggestedDestination($redirectUrl, $options); |
| 390 |
} |
| 391 |
|
| 392 |
// Render suggested destination block (if available) |
| 393 |
if ($suggestion !== null) { |
| 394 |
$this->renderSuggestionBlock($suggestion); |
| 395 |
} |
| 396 |
|
| 397 |
// When creating from captured URLs, clear the redirect_to field so the |
| 398 |
// placeholder text is visible. The suggestion block (if shown) handles |
| 399 |
// presenting the best match separately. |
| 400 |
$redirectFinalDest = is_scalar($redirect['final_dest'] ?? 0) ? (string)($redirect['final_dest'] ?? '0') : '0'; |
| 401 |
if ($isFromCaptured) { |
| 402 |
$pageTitle = ''; |
| 403 |
$pageIDAndType = ''; |
| 404 |
} else { |
| 405 |
$pageTitle = $this->logic->getPageTitleFromIDAndType($pageIDAndType, $redirectFinalDest); |
| 406 |
} |
| 407 |
$html = $this->buildRedirectToDropdownHtml($pageTitle, $pageIDAndType); |
| 408 |
|
| 409 |
// In Simple mode with a suggestion, hide the manual picker initially |
| 410 |
$manualPickerHiddenClass = ($suggestion !== null && $isSimpleMode) ? ' abj404-hidden' : ''; |
| 411 |
echo '<div class="abj404-form-group abj404-autocomplete-wrapper' . $manualPickerHiddenClass . '" id="abj404-manual-picker">'; |
| 412 |
echo $html; |
| 413 |
echo '</div>'; |
| 414 |
|
| 415 |
$this->echoEditRedirect($final, $codeSelected, __('Update Redirect', '404-solution'), $source_page, $filter, $orderby, $order, $startDate, $endDate); |
| 416 |
|
| 417 |
echo '</form>'; |
| 418 |
echo '</div>'; // end abj404-edit-container |
| 419 |
echo '</div>'; // end abj404-edit-page |
| 420 |
} |
| 421 |
|
| 422 |
/** |
| 423 |
* @param string $dest |
| 424 |
* @param array<int, object> $rows |
| 425 |
* @return string |
| 426 |
*/ |
| 427 |
function echoRedirectDestinationOptionsOthers($dest, $rows) { |
| 428 |
$content = array(); |
| 429 |
|
| 430 |
$rowCounter = 0; |
| 431 |
$currentPostType = ''; |
| 432 |
|
| 433 |
foreach ($rows as $row) { |
| 434 |
$rowCounter++; |
| 435 |
/** @var object{id: int, post_type: string, depth?: int} $row */ |
| 436 |
$id = $row->id; |
| 437 |
$theTitle = get_the_title($id); |
| 438 |
$thisval = $id . "|" . ABJ404_TYPE_POST; |
| 439 |
|
| 440 |
$selected = ""; |
| 441 |
if ($thisval == $dest) { |
| 442 |
$selected = " selected"; |
| 443 |
} |
| 444 |
|
| 445 |
abj_service('request_context')->debug_info = 'Before row: ' . $rowCounter . ', Title: ' . $theTitle . |
| 446 |
', Post type: ' . $row->post_type; |
| 447 |
|
| 448 |
if ($row->post_type != $currentPostType) { |
| 449 |
if ($currentPostType != '') { |
| 450 |
$content[] = "\n" . '</optgroup>' . "\n"; |
| 451 |
} |
| 452 |
|
| 453 |
$content[] = "\n" . '<optgroup label="' . __(ucwords($row->post_type), '404-solution') . '">' . "\n"; |
| 454 |
$currentPostType = $row->post_type; |
| 455 |
} |
| 456 |
|
| 457 |
// this is split in this ridiculous way to help me figure out how to resolve a memory issue. |
| 458 |
// (https://wordpress.org/support/topic/options-tab-is-not-loading/) |
| 459 |
$content[] = "\n <option value=\""; |
| 460 |
$content[] = esc_attr($thisval); |
| 461 |
$content[] = "\""; |
| 462 |
$content[] = $selected; |
| 463 |
$content[] = ">"; |
| 464 |
|
| 465 |
// insert some spaces for child pages. |
| 466 |
$depth = property_exists($row, 'depth') ? intval($row->depth) : 0; |
| 467 |
for ($i = 0; $i < $depth; $i++) { |
| 468 |
$content[] = " "; |
| 469 |
} |
| 470 |
|
| 471 |
$content[] = __(ucwords($row->post_type), '404-solution'); |
| 472 |
$content[] = ": "; |
| 473 |
$content[] = esc_html($theTitle); |
| 474 |
$content[] = "</option>"; |
| 475 |
|
| 476 |
abj_service('request_context')->debug_info = 'After row: ' . $rowCounter . ', Title: ' . $theTitle . |
| 477 |
', Post type: ' . $row->post_type; |
| 478 |
} |
| 479 |
|
| 480 |
$content[] = "\n" . '</optgroup>' . "\n"; |
| 481 |
|
| 482 |
|
| 483 |
abj_service('request_context')->debug_info = 'Cleared after building redirect destination page list.'; |
| 484 |
|
| 485 |
return implode('', $content); |
| 486 |
} |
| 487 |
|
| 488 |
/** |
| 489 |
* @param string $dest |
| 490 |
* @return string |
| 491 |
*/ |
| 492 |
function echoRedirectDestinationOptionsCatsTags($dest) { |
| 493 |
$content = ""; |
| 494 |
$content .= "\n" . '<optgroup label="Categories">' . "\n"; |
| 495 |
|
| 496 |
$customTagsEtc = array(); |
| 497 |
|
| 498 |
// categories --------------------------------------------- |
| 499 |
$cats = $this->contentRepository->getPublishedCategories(); |
| 500 |
foreach ($cats as $cat) { |
| 501 |
/** @var \WP_Term $cat */ |
| 502 |
$taxonomy = $cat->taxonomy; |
| 503 |
if ($taxonomy != 'category') { |
| 504 |
continue; |
| 505 |
} |
| 506 |
|
| 507 |
$id = $cat->term_id; |
| 508 |
$theTitle = $cat->name; |
| 509 |
$thisval = $id . "|" . ABJ404_TYPE_CAT; |
| 510 |
|
| 511 |
$selected = ""; |
| 512 |
if ($thisval == $dest) { |
| 513 |
$selected = " selected"; |
| 514 |
} |
| 515 |
$content .= "\n<option value=\"" . esc_attr($thisval) . "\"" . $selected . ">" . __('Category', '404-solution') . ": " . $theTitle . "</option>"; |
| 516 |
} |
| 517 |
$content .= "\n" . '</optgroup>' . "\n"; |
| 518 |
/** @var array<int, object{taxonomy: string, name?: string}> $cats */ |
| 519 |
$customTagsEtc = $this->logic->getMapOfCustomCategories($cats); |
| 520 |
|
| 521 |
// tags --------------------------------------------- |
| 522 |
$content .= "\n" . '<optgroup label="Tags">' . "\n"; |
| 523 |
$tags = $this->contentRepository->getPublishedTags(); |
| 524 |
foreach ($tags as $tag) { |
| 525 |
/** @var \WP_Term $tag */ |
| 526 |
$id = $tag->term_id; |
| 527 |
$theTitle = $tag->name; |
| 528 |
$thisval = $id . "|" . ABJ404_TYPE_TAG; |
| 529 |
|
| 530 |
$selected = ""; |
| 531 |
if ($thisval == $dest) { |
| 532 |
$selected = " selected"; |
| 533 |
} |
| 534 |
$content .= "\n<option value=\"" . esc_attr($thisval) . "\"" . $selected . ">" . __('Tag', '404-solution') . ": " . $theTitle . "</option>"; |
| 535 |
} |
| 536 |
$content .= "\n" . '</optgroup>' . "\n"; |
| 537 |
|
| 538 |
// custom --------------------------------------------- |
| 539 |
foreach ($customTagsEtc as $taxonomy => $catRow) { |
| 540 |
$content .= "\n" . '<optgroup label="' . esc_html($taxonomy) . '">' . "\n"; |
| 541 |
|
| 542 |
foreach ($catRow as $cat) { |
| 543 |
/** @var \WP_Term $cat */ |
| 544 |
$id = $cat->term_id; |
| 545 |
$theTitle = $cat->name; |
| 546 |
$thisval = $id . "|" . ABJ404_TYPE_CAT; |
| 547 |
|
| 548 |
$selected = ""; |
| 549 |
if ($thisval == $dest) { |
| 550 |
$selected = " selected"; |
| 551 |
} |
| 552 |
$content .= "\n<option value=\"" . esc_attr($thisval) . "\"" . $selected . ">" . __('Custom', '404-solution') . ": " . $theTitle . "</option>"; |
| 553 |
} |
| 554 |
|
| 555 |
$content .= "\n" . '</optgroup>' . "\n"; |
| 556 |
} |
| 557 |
|
| 558 |
return $content; |
| 559 |
} |
| 560 |
|
| 561 |
/** |
| 562 |
* Convert a raw engine class name to a human-readable label. |
| 563 |
* |
| 564 |
* Examples: |
| 565 |
* TitleMatchingEngine → "Title Matching" |
| 566 |
* SpellingMatchingEngine → "Spelling Matching" |
| 567 |
* CategoryTagMatchingEngine → "Category/Tag Matching" |
| 568 |
* UrlFixEngine → "URL Fix" |
| 569 |
* ArchiveFallbackEngine → "Archive Fallback" |
| 570 |
* |
| 571 |
* @param string $rawName |
| 572 |
* @return string |
| 573 |
*/ |
| 574 |
private function humanizeEngineName(string $rawName): string { |
| 575 |
// Strip full namespace prefix if stored with it. |
| 576 |
$name = preg_replace('/^ABJ_404_Solution_/', '', $rawName); |
| 577 |
if (!is_string($name)) { |
| 578 |
$name = $rawName; |
| 579 |
} |
| 580 |
// Strip "MatchingEngine" or bare "Engine" suffix. |
| 581 |
$name = (string)preg_replace('/MatchingEngine$/', ' Matching', $name); |
| 582 |
$name = (string)preg_replace('/Engine$/', '', $name); |
| 583 |
// Insert a space before each upper-case letter that follows a lower-case letter |
| 584 |
// (e.g. CategoryTag → Category Tag). |
| 585 |
$name = (string)preg_replace('/(?<=[a-z])([A-Z])/', ' $1', $name); |
| 586 |
$name = trim($name); |
| 587 |
// Fix known abbreviations. |
| 588 |
$name = str_replace(array('Url ', 'Url'), array('URL ', 'URL'), $name); |
| 589 |
// Fix Category/Tag — appears as "Category Tag Matching", make the separator a slash. |
| 590 |
$name = str_replace('Category Tag', 'Category/Tag', $name); |
| 591 |
return $name !== '' ? $name : $rawName; |
| 592 |
} |
| 593 |
|
| 594 |
/** |
| 595 |
* Get the best suggested destination for a captured URL using the spell-checker. |
| 596 |
* |
| 597 |
* @param string $url The captured 404 URL. |
| 598 |
* @param array<string, mixed> $options Plugin options. |
| 599 |
* @return array{title: string, score: int, id_and_type: string, type_label: string}|null The best match, or null if none found. |
| 600 |
*/ |
| 601 |
private function getSuggestedDestination(string $url, array $options): ?array { |
| 602 |
try { |
| 603 |
$spellChecker = abj_service('spell_checker'); |
| 604 |
$permalinksPacket = $spellChecker->findMatchingPosts($url, '1', '1'); |
| 605 |
$permalinks = is_array($permalinksPacket[0] ?? null) ? $permalinksPacket[0] : array(); |
| 606 |
$rowType = is_string($permalinksPacket[1] ?? '') ? (string)($permalinksPacket[1] ?? '') : ''; |
| 607 |
|
| 608 |
if (empty($permalinks)) { |
| 609 |
return null; |
| 610 |
} |
| 611 |
|
| 612 |
// Take the top match |
| 613 |
$topIdAndType = array_key_first($permalinks); |
| 614 |
$topScore = intval($permalinks[$topIdAndType]); |
| 615 |
|
| 616 |
// Only suggest if score is at least 25% |
| 617 |
if ($topScore < 25) { |
| 618 |
return null; |
| 619 |
} |
| 620 |
|
| 621 |
$permalink = ABJ_404_Solution_Functions::permalinkInfoToArray( |
| 622 |
$topIdAndType, $topScore, $rowType, $options |
| 623 |
); |
| 624 |
|
| 625 |
$title = is_string($permalink['title'] ?? '') ? (string)($permalink['title'] ?? '') : ''; |
| 626 |
if ($title === '' || ($permalink['status'] ?? '') === 'trash') { |
| 627 |
return null; |
| 628 |
} |
| 629 |
|
| 630 |
// Determine a human-readable type label |
| 631 |
$typeParts = explode('|', is_string($topIdAndType) ? $topIdAndType : ''); |
| 632 |
$typeInt = isset($typeParts[1]) && is_numeric($typeParts[1]) ? (int)$typeParts[1] : -1; |
| 633 |
$typeLabel = ''; |
| 634 |
if ($typeInt === ABJ404_TYPE_POST) { |
| 635 |
$postType = get_post_type((int)$typeParts[0]); |
| 636 |
$typeLabel = ($postType === 'page') ? __('Page', '404-solution') : __('Post', '404-solution'); |
| 637 |
} elseif ($typeInt === ABJ404_TYPE_CAT) { |
| 638 |
$typeLabel = __('Category', '404-solution'); |
| 639 |
} elseif ($typeInt === ABJ404_TYPE_TAG) { |
| 640 |
$typeLabel = __('Tag', '404-solution'); |
| 641 |
} elseif ($typeInt === ABJ404_TYPE_HOME) { |
| 642 |
$typeLabel = __('Home', '404-solution'); |
| 643 |
} |
| 644 |
|
| 645 |
return array( |
| 646 |
'title' => $title, |
| 647 |
'score' => $topScore, |
| 648 |
'id_and_type' => is_string($topIdAndType) ? $topIdAndType : '', |
| 649 |
'type_label' => $typeLabel, |
| 650 |
); |
| 651 |
} catch (\Throwable $e) { // allow-silent-catch: spell-checker may fail on some URLs (encoding, length); null signals "no suggestion" which the caller already handles |
| 652 |
return null; |
| 653 |
} |
| 654 |
} |
| 655 |
|
| 656 |
} |
| 657 |
|