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