PluginProbe
404 Solution / 4.2.0
404 Solution v4.2.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 / PluginLogicAdminActions.php

PluginLogicAdminActions.php in 404 Solution 4.2.0, at includes/PluginLogicAdminActions.php

991 lines 43.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Admin action handlers: trash, delete, ignore, later, edit, bulk actions, empty trash.
10 * Standalone class extracted from PluginLogicTrait_AdminActions.
11 */
12 class ABJ_404_Solution_PluginLogicAdminActions {
13
14 /** @var ABJ_404_Solution_Functions */
15 private $f;
16
17 /** @var ABJ_404_Solution_Logging */
18 private $logger;
19
20 /** @var ABJ_404_Solution_RedirectsRepositoryInterface */
21 private $redirectsRepo;
22
23 /** @var ABJ_404_Solution_ViewBuildOrchestratorInterface */
24 private $viewBuild;
25
26 /** @var ABJ_404_Solution_ViewReadServiceInterface */
27 private $viewRead;
28
29 /** @var ABJ_404_Solution_ContentRepositoryInterface */
30 private $contentRepo;
31
32 /** @var ABJ_404_Solution_DatabaseCoreInterface */
33 private $dbCore;
34
35 /** @var ABJ_404_Solution_DataAccess */
36 private $dao;
37
38 /** @var ABJ_404_Solution_PluginLogicUrlNormalization */
39 private $urlNormalization;
40
41 /** @var ABJ_404_Solution_PluginLogic */
42 private $pluginLogic;
43
44 /**
45 * @param ABJ_404_Solution_Functions $f
46 * @param ABJ_404_Solution_Logging $logger
47 * @param ABJ_404_Solution_RedirectsRepositoryInterface $redirectsRepo
48 * @param ABJ_404_Solution_ViewBuildOrchestratorInterface $viewBuild
49 * @param ABJ_404_Solution_ViewReadServiceInterface $viewRead
50 * @param ABJ_404_Solution_ContentRepositoryInterface $contentRepo
51 * @param ABJ_404_Solution_DatabaseCoreInterface $dbCore
52 * @param ABJ_404_Solution_DataAccess $dao
53 * @param ABJ_404_Solution_PluginLogicUrlNormalization $urlNormalization
54 * @param ABJ_404_Solution_PluginLogic $pluginLogic
55 */
56 function __construct($f, $logger, $redirectsRepo, $viewBuild, $viewRead, $contentRepo, $dbCore, $dao, $urlNormalization, $pluginLogic) {
57 $this->f = $f;
58 $this->logger = $logger;
59 $this->redirectsRepo = $redirectsRepo;
60 $this->viewBuild = $viewBuild;
61 $this->viewRead = $viewRead;
62 $this->contentRepo = $contentRepo;
63 $this->dbCore = $dbCore;
64 $this->dao = $dao;
65 $this->urlNormalization = $urlNormalization;
66 $this->pluginLogic = $pluginLogic;
67 }
68
69 /**
70 * Verify a nonce for admin-link actions, without depending on the browser's Referer header.
71 *
72 * @param string $action Nonce action string used in wp_nonce_url()
73 * @param string $queryArg Nonce query arg name (default '_wpnonce')
74 * @return bool
75 */
76 private function verifyLinkNonce($action, $queryArg = '_wpnonce') {
77 if (function_exists('check_admin_referer')) {
78 $ok = check_admin_referer($action, $queryArg);
79 if ($ok) {
80 return true;
81 }
82 }
83
84 if (!function_exists('wp_verify_nonce')) {
85 return false;
86 }
87
88 if (!isset($_REQUEST[$queryArg])) {
89 return false;
90 }
91
92 $nonce = sanitize_text_field(wp_unslash($_REQUEST[$queryArg]));
93 if ($nonce === '') {
94 return false;
95 }
96
97 return wp_verify_nonce($nonce, $action) !== false;
98 }
99
100 /** Do the passed in action and return the associated message.
101 * @param string $action
102 * @param string $sub
103 * @return string
104 */
105 function handlePluginAction($action, &$sub) {
106 $message = "";
107 $message = array_key_exists('display-this-message', $_POST) ?
108 sanitize_text_field($_POST['display-this-message']) : '';
109
110 if ($action == "updateOptions") {
111 if (wp_verify_nonce($_POST['nonce'], 'abj404UpdateOptions') && is_admin()) {
112 if (array_key_exists('deleteDebugFile', $_POST) && $_POST['deleteDebugFile']) {
113 $filepath = $this->logger->getDebugFilePath();
114 if (!file_exists($filepath)) {
115 $message = sprintf(__("Debug file not found. (%s)", '404-solution'), $filepath);
116 } else if ($this->logger->deleteDebugFile()) {
117 $message = sprintf(__("Debug file(s) deleted. (%s)", '404-solution'), $filepath);
118 } else {
119 $message = sprintf(__("Issue deleting debug file. (%s)", '404-solution'), $filepath);
120 }
121 return $message;
122 }
123
124 $sub = "abj404_options";
125 } else {
126 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
127 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
128 }
129 } else if ($action == "addRedirect") {
130 if (check_admin_referer('abj404addRedirect') && is_admin()) {
131 $message = $this->addAdminRedirect();
132 if ($message == "") {
133 $message = __('New Redirect Added Successfully!', '404-solution');
134 } else {
135 $message .= __('Error: unable to add new redirect.', '404-solution');
136 }
137 } else {
138 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
139 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
140 }
141 } else if ($action == "emptyRedirectTrash") {
142 if (check_admin_referer('abj404_bulkProcess') && is_admin()) {
143 $this->doEmptyTrash('abj404_redirects');
144 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
145 $message = __('All trashed URLs have been deleted!', '404-solution');
146 } else {
147 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
148 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
149 }
150 } else if ($action == "emptyCapturedTrash") {
151 if (check_admin_referer('abj404_bulkProcess') && is_admin()) {
152 $this->doEmptyTrash('abj404_captured');
153 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
154 $message = __('All trashed URLs have been deleted!', '404-solution');
155 } else {
156 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
157 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
158 }
159 } else if ($action == "purgeRedirects") {
160 if (check_admin_referer('abj404_purgeRedirects') && is_admin()) {
161 $message = $this->redirectsRepo->deleteSpecifiedRedirects();
162 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
163 } else {
164 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
165 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
166 }
167 } else if ($action == "runMaintenance") {
168 if (check_admin_referer('abj404_runMaintenance') && is_admin()) {
169 $message = $this->redirectsRepo->deleteOldRedirectsCron();
170 } else {
171 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
172 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
173 }
174 } else if ($action == "rebuildNgramCache") {
175 if (check_admin_referer('abj404_rebuildNgramCache') && is_admin()) {
176 $userId = get_current_user_id();
177 $transientKey = 'abj404_ngram_rebuild_request_' . $userId;
178 $recentRequest = get_transient($transientKey);
179
180 if ($recentRequest) {
181 $message = __('N-gram cache rebuild is already scheduled or in progress. Please wait for it to complete.', '404-solution');
182 } else {
183 set_transient($transientKey, time(), 10);
184
185 $dbUpgrades = abj_service('database_upgrades');
186
187 $scheduled = $dbUpgrades->scheduleNGramCacheRebuild();
188
189 if ($scheduled) {
190 $message = __('N-gram cache rebuild has been scheduled and will run in the background. This may take several minutes on large sites. You can continue using the plugin normally.', '404-solution');
191 } else {
192 $nextScheduled = wp_next_scheduled('abj404_rebuild_ngram_cache_hook');
193 if ($nextScheduled) {
194 $message = __('N-gram cache rebuild is already scheduled or in progress. Please wait for it to complete.', '404-solution');
195 } else {
196 $message = __('Failed to schedule N-gram cache rebuild. Please try again or check your WordPress cron configuration.', '404-solution');
197 }
198 }
199 }
200 } else {
201 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
202 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
203 }
204 } else if ($action == "clearSpellingCache") {
205 if (check_admin_referer('abj404_clearSpellingCache') && is_admin()) {
206 $this->contentRepo->deleteSpellingCache();
207 $message = __('Spelling cache cleared successfully.', '404-solution');
208 } else {
209 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
210 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
211 }
212 } else if ($action == "saveGscSettings") {
213 if (check_admin_referer('abj404_gsc_save', '_wpnonce_gsc') && is_admin()) {
214 $logger = abj_service('logging');
215 $gsc = new ABJ_404_Solution_GoogleSearchConsole($logger);
216 $error = $gsc->saveSettings($_POST);
217 $message = ($error === '') ? __('Google Search Console credentials saved.', '404-solution') : $error;
218 } else {
219 $this->logger->debugMessage("saveGscSettings security check failed. is_admin: " . is_admin());
220 }
221 } else if ($action == "importFromPlugin") {
222 if (check_admin_referer('abj404_importFromPlugin') && is_admin()) {
223 $message = $this->handleActionImportFromPlugin();
224 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
225 } else {
226 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
227 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
228 }
229 } else if ($action == "undoRegexAutoPromote") {
230 if (check_admin_referer('abj404undoRegexAutoPromote') && is_admin()) {
231 $message = $this->handleActionUndoRegexAutoPromote();
232 } else {
233 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
234 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
235 }
236 } else if ($this->f->substr($action . '', 0, 4) == "bulk") {
237 if (check_admin_referer('abj404_bulkProcess') && is_admin()) {
238 if (!isset($_POST['idnum'])) {
239 $this->logger->debugMessage("No ID(s) specified for bulk action: " . esc_html($action));
240 echo sprintf(__("Error: No ID(s) specified for bulk action. (%s)", '404-solution'),
241 esc_html($action));
242 return '';
243 }
244 $message = $this->doBulkAction($action, array_map('absint', $_POST['idnum']));
245 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
246 } else {
247 $this->logger->debugMessage("Unexpected result. How did we get here? is_admin: " .
248 is_admin() . ", Action: " . $action . ", Sub: " . $sub);
249 }
250 }
251
252 return $message;
253 }
254
255 /** Move redirects to trash.
256 * @return string
257 */
258 function hanldeTrashAction() {
259
260 $message = "";
261 if (isset($_GET['trash'])) {
262 if (is_admin() && $this->verifyLinkNonce('abj404_trashRedirect')) {
263 $trash = "";
264 if ($_GET['trash'] == 0) {
265 $trash = 0;
266 } else if ($_GET['trash'] == 1) {
267 $trash = 1;
268 } else {
269 $this->logger->errorMessage("Unexpected trash operation: " .
270 esc_html($_GET['trash']));
271 $message = __('Error: Bad trash operation specified.', '404-solution');
272 return $message;
273 }
274
275 $id = absint($_GET['id']);
276 $message = $this->redirectsRepo->moveRedirectsToTrash($id, $trash);
277 if ($message == "") {
278 $subpage = isset($_GET['subpage']) ? sanitize_text_field(wp_unslash($_GET['subpage'])) : '';
279 $filter = isset($_GET['filter']) ? intval($_GET['filter']) : 0;
280 if ($trash == 0 && $subpage === 'abj404_captured' && $filter === ABJ404_TRASH_FILTER) {
281 $this->redirectsRepo->updateRedirectTypeStatus($id, (string)ABJ404_STATUS_CAPTURED);
282 }
283 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
284 if ($trash == 1) {
285 $message = __('Redirect moved to trash successfully!', '404-solution');
286 } else {
287 $message = __('Redirect restored from trash successfully!', '404-solution');
288 }
289 } else {
290 if ($trash == 1) {
291 $message = __('Error: Unable to move redirect to trash.', '404-solution');
292 } else {
293 $message = __('Error: Unable to move redirect from trash.', '404-solution');
294 }
295 }
296
297 }
298 }
299
300 return $message;
301 }
302
303 /** @return void */
304 function handleActionChangeItemsPerRow(): void {
305
306 if ($this->f->getPostOrGetSanitize('action') == 'changeItemsPerRow' && $this->pluginLogic->userIsPluginAdmin()) {
307 check_admin_referer('abj404_changeItemsPerRow');
308 $this->updatePerPageOption(absint($this->f->getPostOrGetSanitize('perpage')));
309 }
310 }
311
312 /** @return void */
313 function handleActionExport(): void {
314
315 if (($this->f->getPostOrGetSanitize('action') == 'exportRedirects') && $this->pluginLogic->userIsPluginAdmin()) {
316 check_admin_referer('abj404_exportRedirects');
317 $this->pluginLogic->doExport();
318 }
319 }
320
321 /** @return string|null */
322 function handleActionImportFile() {
323
324 if (($this->f->getPostOrGetSanitize('action') == 'importRedirectsFile') && $this->pluginLogic->userIsPluginAdmin()) {
325 check_admin_referer('abj404_importRedirectsFile');
326 $result = $this->pluginLogic->doImportFile();
327 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
328 return $result;
329 }
330
331 return null;
332 }
333
334 /** @return void */
335 function updatePerPageOption(int $rows): void {
336 $showRows = max($rows, ABJ404_OPTION_MIN_PERPAGE);
337 $showRows = min($showRows, ABJ404_OPTION_MAX_PERPAGE);
338
339 $options = $this->pluginLogic->getOptions();
340 $options['perpage'] = $showRows;
341 $this->pluginLogic->updateOptions($options);
342 }
343
344 /**
345 * @return string
346 */
347 function handleActionImportRedirects() {
348 $message = "";
349
350
351 if ($this->f->getPostOrGetSanitize('action') == 'importRedirects') {
352 if ($this->f->getPostOrGetSanitize('sanity_404redirected') != '1') {
353 $message = __("Error: You didn't check the I understand checkbox. No importing for you!", '404-solution');
354 return $message;
355 }
356
357 check_admin_referer('abj404_importRedirects');
358
359 try {
360 $result = $this->dao->importDataFromPluginRedirectioner();
361 if ($result['last_error'] != '') {
362 $lastErrorJson = json_encode($result['last_error']);
363 $message = sprintf(__("Error: No records were imported. SQL result: %s", '404-solution'),
364 wp_kses_post(is_string($lastErrorJson) ? $lastErrorJson : ''));
365 } else {
366 $rowsAffected = is_scalar($result['rows_affected']) ? (string)$result['rows_affected'] : '0';
367 $message = sprintf(__("Records imported: %s", '404-solution'), esc_html($rowsAffected));
368 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
369 }
370
371 } catch (Exception $e) {
372 $message = "Error: Importing failed. Message: " . $e->getMessage();
373 $this->logger->errorMessage('Error importing redirects.', $e);
374 }
375 }
376
377 return $message;
378 }
379
380 /** Delete redirects.
381 * @return string
382 */
383 function handleDeleteAction() {
384 $message = "";
385
386 if (array_key_exists('remove', $_GET) && @$_GET['remove'] == 1) {
387 if (is_admin() && $this->verifyLinkNonce('abj404_removeRedirect')) {
388 if ($this->f->regexMatch('[0-9]+', $_GET['id'])) {
389 $this->redirectsRepo->deleteRedirect(absint($_GET['id']));
390 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
391 $message = __('Redirect Removed Successfully!', '404-solution');
392 }
393 }
394 }
395
396 return $message;
397 }
398
399 /**
400 * @param string $paramName The $_GET parameter name
401 * @param string $nonceAction The nonce action name
402 * @param int $activeStatus The status constant to use when action=1
403 * @param string $errorActionName Action name for error messages
404 * @param string $successActionName Action name for success messages
405 * @return string
406 */
407 private function handleStatusUpdate($paramName, $nonceAction, $activeStatus, $errorActionName, $successActionName) {
408 $message = "";
409
410 if (isset($_GET[$paramName])) {
411 if (is_admin() && $this->verifyLinkNonce($nonceAction)) {
412 if ($_GET[$paramName] != 0 && $_GET[$paramName] != 1) {
413 $this->logger->debugMessage("Unexpected {$errorActionName} operation: " .
414 esc_html($_GET[$paramName]));
415 $message = sprintf(__('Error: Bad %s operation specified.', '404-solution'), $errorActionName);
416 return $message;
417 }
418
419 $id = $_GET['id'] ?? '';
420 if ($id !== '' && $this->f->regexMatch('[0-9]+', $id)) {
421 if ($_GET[$paramName] == 1) {
422 $newstatus = $activeStatus;
423 } else {
424 $newstatus = ABJ404_STATUS_CAPTURED;
425 }
426
427 $message = $this->redirectsRepo->updateRedirectTypeStatus(absint($id), (string)$newstatus);
428 if ($message == "") {
429 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
430 if ($newstatus == ABJ404_STATUS_CAPTURED) {
431 $message = sprintf(__('Removed 404 URL from %s list successfully!', '404-solution'), $successActionName);
432 } else {
433 $message = sprintf(__('404 URL marked as %s successfully!', '404-solution'), $successActionName);
434 }
435 } else {
436 if ($newstatus == ABJ404_STATUS_CAPTURED) {
437 $message = sprintf(__('Error: unable to remove URL from %s list', '404-solution'), $successActionName);
438 } else {
439 $message = sprintf(__('Error: unable to mark URL as %s', '404-solution'), $successActionName);
440 }
441 }
442 }
443 }
444 }
445
446 return $message;
447 }
448
449 /** @return string */
450 function handleIgnoreAction() {
451 return $this->handleStatusUpdate('ignore', 'abj404_ignore404', ABJ404_STATUS_IGNORED, 'ignore', 'ignored');
452 }
453
454 /** @return string */
455 function handleLaterAction() {
456 return $this->handleStatusUpdate('later', 'abj404_organizeLater', ABJ404_STATUS_LATER, 'organize later', 'organize later');
457 }
458
459 /** Edit redirect data.
460 * @param string $sub
461 * @param string $action
462 * @return string
463 */
464 function handleActionEdit(&$sub, &$action) {
465 $message = "";
466
467 if (array_key_exists('action', $_POST) && $_POST['action'] == "editRedirect") {
468 $id = $this->f->getPostOrGetSanitize('id');
469 $ids = $this->f->getPostOrGetSanitize('ids_multiple');
470 if (!($id === '' && $ids === '') && ($this->f->regexMatch('[0-9]+', '' . $id) || $this->f->regexMatch('[0-9]+', '' . $ids))) {
471 if (is_admin() && $this->verifyLinkNonce('abj404editRedirect')) {
472 $message = $this->pluginLogic->updateRedirectData();
473 if ($message == "") {
474 $source_page = $this->f->getPostOrGetSanitize('source_page');
475
476 $valid_tabs = array('abj404_redirects', 'abj404_captured', 'abj404_logs',
477 'abj404_stats', 'abj404_tools', 'abj404_options');
478 if ($source_page === '' || !in_array($source_page, $valid_tabs)) {
479 $source_page = 'abj404_redirects';
480 }
481
482 $redirect_url = "?page=" . ABJ404_PP . "&subpage=" . $source_page;
483 $redirect_url .= "&updated=1";
484
485 $source_filter = $this->f->getPostOrGetSanitize('source_filter', '');
486 if ($source_filter !== '' && $source_filter !== '0') {
487 $redirect_url .= "&filter=" . urlencode($source_filter);
488 }
489
490 $source_orderby = $this->f->getPostOrGetSanitize('source_orderby', '');
491 $source_order = $this->f->getPostOrGetSanitize('source_order', '');
492 if ($source_orderby !== '' && $source_order !== '') {
493 if (!($source_orderby === "url" && $source_order === "ASC")) {
494 $redirect_url .= "&orderby=" . urlencode($source_orderby);
495 $redirect_url .= "&order=" . urlencode($source_order);
496 }
497 }
498
499 $source_paged = $this->f->getPostOrGetSanitize('source_paged', '');
500 if ($source_paged !== '' && (int)$source_paged > 1) {
501 $redirect_url .= "&paged=" . urlencode($source_paged);
502 }
503
504 wp_safe_redirect(admin_url('admin.php' . $redirect_url));
505 return "";
506 } else {
507 $message .= __('Error: Unable to update redirect data.', '404-solution');
508 }
509 }
510 }
511 }
512
513 return $message;
514 }
515
516 /**
517 * @param string $action
518 * @param array<int, int> $ids
519 * @return string
520 */
521 function doBulkAction(string $action, array $ids): string {
522 $message = "";
523
524 $this->logger->debugMessage("In doBulkAction. Action: " .
525 esc_html($action == '' ? '(none)' : $action) . ", ids: " . wp_kses_post((string)json_encode($ids)));
526
527 if ($action == "bulkignore" || $action == "bulkcaptured" || $action == "bulklater" ||
528 $action == "bulk_trash_restore") {
529
530 $status = 0;
531 if ($action == "bulkignore") {
532 $status = ABJ404_STATUS_IGNORED;
533 } else if ($action == "bulkcaptured") {
534 $status = ABJ404_STATUS_CAPTURED;
535 } else if ($action == "bulklater") {
536 $status = ABJ404_STATUS_LATER;
537 }
538
539 $count = 0;
540 foreach ($ids as $id) {
541 $s = $this->redirectsRepo->moveRedirectsToTrash($id, 0);
542 if ($action != "bulk_trash_restore") {
543 $s = $this->redirectsRepo->updateRedirectTypeStatus($id, (string)$status);
544 }
545 if ($s == "") {
546 $count++;
547 }
548 }
549 if ($action == "bulkignore") {
550 $message = $count . " " . __('URL(s) marked as Ignored.', '404-solution');
551 } else if ($action == "bulkcaptured") {
552 $message = $count . " " . __('URL(s) marked as Captured.', '404-solution');
553 } else if ($action == "bulklater") {
554 $message = $count . " " . __('URL(s) marked as Later.', '404-solution');
555 } else {
556 $message = $count . " " . __('URL(s) restored.', '404-solution');
557 }
558
559 } else if ($action == "bulk_trash_delete_permanently") {
560 $count = 0;
561 foreach ($ids as $id) {
562 $this->redirectsRepo->deleteRedirect(absint($id));
563 $count ++;
564 }
565 $message = $count . " " . __('URL(s) deleted', '404-solution');
566
567 } else if ($action == "bulktrash") {
568 $count = 0;
569 foreach ($ids as $id) {
570 $s = $this->redirectsRepo->moveRedirectsToTrash($id, 1);
571 if ($s == "") {
572 $count ++;
573 }
574 }
575 $message = $count . " " . __('URL(s) moved to trash', '404-solution');
576
577 } else {
578 $this->logger->errorMessage("Unrecognized bulk action: " . esc_html($action));
579 echo sprintf(__("Error: Unrecognized bulk action. (%s)", '404-solution'), esc_html($action));
580 }
581 return $message;
582 }
583
584 /**
585 * @param string $sub
586 * @return void
587 */
588 function doEmptyTrash(string $sub): void {
589 global $wpdb;
590 global $abj404_redirect_types;
591 global $abj404_captured_types;
592
593 $query = "";
594 if ($sub == "abj404_captured") {
595 $query = "delete FROM {wp_abj404_redirects} \n" .
596 "where disabled = 1 \n" .
597 " and status in (" . implode(", ", $abj404_captured_types) . ")";
598
599 } else if ($sub == "abj404_redirects") {
600 $query = "delete FROM {wp_abj404_redirects} \n" .
601 "where disabled = 1 \n" .
602 " and status in (" . implode(", ", $abj404_redirect_types) . ")";
603
604 } else {
605 $this->logger->errorMessage("Unrecognized type in doEmptyTrash(" . $sub . ")");
606 return;
607 }
608
609 $result = $this->dbCore->queryAndGetResults($query);
610 $this->logger->debugMessage("doEmptyTrash deleted " . $result['rows_affected'] . " rows total. (" . $sub . ")");
611
612 $this->viewRead->invalidateStatusCountsCache();
613
614 $this->dbCore->queryAndGetResults("optimize table {wp_abj404_redirects}");
615 }
616
617 /**
618 * @return string
619 */
620 function updateRedirectData() {
621 $message = "";
622 $fromURL = "";
623 $ids_multiple = "";
624
625 if (
626 (!array_key_exists('url', $_POST) || $_POST['url'] == "") &&
627 (array_key_exists('ids_multiple', $_POST) && $_POST['ids_multiple'] != "")) {
628 $ids_multiple = array_map('absint', explode(',', $_POST['ids_multiple']));
629
630 } else if (array_key_exists('url', $_POST) && $_POST['url'] != "" &&
631 (!array_key_exists('ids_multiple', $_POST) || $_POST['ids_multiple'] == "")) {
632
633 $fromURL = stripslashes($_POST['url']);
634 } else {
635 $message .= __('Error: URL is a required field.', '404-solution') . "<BR/>";
636 }
637
638 if ($fromURL != "" && $this->f->substr($_POST['url'], 0, 1) != "/") {
639 $message .= __('Error: URL must start with /', '404-solution') . "<BR/>";
640 }
641
642 $typeAndDest = $this->getRedirectTypeAndDest();
643
644 $typeAndDestMessage = is_string($typeAndDest['message']) ? $typeAndDest['message'] : '';
645 if ($typeAndDestMessage != "") {
646 return $typeAndDestMessage;
647 }
648
649 $tdTypeRaw = is_scalar($typeAndDest['type']) ? (string)$typeAndDest['type'] : '';
650 $tdType = ($tdTypeRaw !== '') ? (int)$tdTypeRaw : -1;
651 $tdDest = is_scalar($typeAndDest['dest']) ? (string)$typeAndDest['dest'] : '';
652 $postedCodeForCheck = isset($_POST['code']) && is_scalar($_POST['code']) ? (string)$_POST['code'] : '';
653 $isCode410 = $postedCodeForCheck === '410' || $postedCodeForCheck === '451';
654 if ($tdTypeRaw !== '' && ($tdDest !== "" || $isCode410)) {
655 $statusType = ABJ404_STATUS_MANUAL;
656 if (isset($_POST['is_regex_url']) &&
657 $_POST['is_regex_url'] != '0') {
658
659 $statusType = ABJ404_STATUS_REGEX;
660 }
661
662 $startDateRaw = isset($_POST['redirect_start_date']) && is_string($_POST['redirect_start_date']) ? trim($_POST['redirect_start_date']) : '';
663 $endDateRaw = isset($_POST['redirect_end_date']) && is_string($_POST['redirect_end_date']) ? trim($_POST['redirect_end_date']) : '';
664 $startTs = ($startDateRaw !== '') ? strtotime($startDateRaw . ' 00:00:00') : null;
665 $endTs = ($endDateRaw !== '') ? strtotime($endDateRaw . ' 23:59:59') : null;
666 if ($startTs === false) { $startTs = null; }
667 if ($endTs === false) { $endTs = null; }
668
669 $rawConditions = (isset($_POST['conditions']) && is_array($_POST['conditions']))
670 ? $_POST['conditions'] : [];
671 $sanitizedConditions = [];
672 $allowedConditionTypes = [
673 'login_status', 'user_role', 'referrer',
674 'user_agent', 'ip_range', 'http_header',
675 ];
676 $allowedOperators = [
677 'equals', 'not_equals', 'contains',
678 'not_contains', 'regex', 'cidr',
679 ];
680 foreach ($rawConditions as $rawCond) {
681 if (!is_array($rawCond)) {
682 continue;
683 }
684 $condType = isset($rawCond['condition_type']) && is_string($rawCond['condition_type'])
685 ? sanitize_text_field($rawCond['condition_type']) : '';
686 if (!in_array($condType, $allowedConditionTypes, true)) {
687 continue;
688 }
689 $condLogic = (isset($rawCond['logic']) && strtoupper((string)$rawCond['logic']) === 'OR') ? 'OR' : 'AND';
690 $condOperator = isset($rawCond['operator']) && is_string($rawCond['operator'])
691 ? sanitize_text_field($rawCond['operator']) : 'equals';
692 if (!in_array($condOperator, $allowedOperators, true)) {
693 $condOperator = 'equals';
694 }
695 $condValue = isset($rawCond['value']) && is_string($rawCond['value'])
696 ? sanitize_text_field(wp_unslash($rawCond['value'])) : '';
697 $condSortOrder = isset($rawCond['sort_order']) ? absint($rawCond['sort_order']) : 0;
698
699 $sanitizedConditions[] = [
700 'logic' => $condLogic,
701 'condition_type' => $condType,
702 'operator' => $condOperator,
703 'value' => $condValue,
704 'sort_order' => $condSortOrder,
705 ];
706 }
707
708 if ($fromURL != "") {
709 $id = isset($_POST['id']) && is_scalar($_POST['id']) ? (int)$_POST['id'] : 0;
710 $code = isset($_POST['code']) && is_string($_POST['code']) ? $_POST['code'] : '';
711 $originalFromURL = $fromURL;
712 $autoPromote = $this->maybeAutoPromoteRegex($statusType, $fromURL);
713 $statusType = $autoPromote['statusType'];
714 $fromURL = $autoPromote['url'];
715 $this->redirectsRepo->updateRedirect($tdType, $tdDest,
716 $fromURL, $id, $code, (string)$statusType, $startTs, $endTs);
717 if ($autoPromote['autoPromoted']) {
718 $this->saveRegexAutoPromoteNotice($id, $originalFromURL, $fromURL, $autoPromote['urlRewritten']);
719 }
720
721 if ($id > 0) {
722 $this->redirectsRepo->saveRedirectConditions($id, $sanitizedConditions);
723 }
724 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
725
726 } else if ($ids_multiple != "") {
727 $redirects_multiple = $this->redirectsRepo->getRedirectsByIDs($ids_multiple);
728 $code = isset($_POST['code']) && is_string($_POST['code']) ? $_POST['code'] : '';
729 foreach ($redirects_multiple as $redirect) {
730 $redirectUrl = is_string($redirect['url']) ? $redirect['url'] : '';
731 $redirectId = is_scalar($redirect['id']) ? (int)$redirect['id'] : 0;
732 $this->redirectsRepo->updateRedirect($tdType, $tdDest,
733 $redirectUrl, $redirectId, $code, (string)$statusType);
734 }
735 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
736
737 } else {
738 $this->logger->errorMessage("Issue determining which redirect(s) to update. " .
739 "fromURL: " . $fromURL . ", ids_multiple: " . $ids_multiple);
740 }
741
742 } else {
743 $message .= __('Error: Data not formatted properly.', '404-solution') . "<BR/>";
744 $this->logger->errorMessage("Update redirect data issue. Type: " . esc_html((string)$tdType) .
745 ", dest: " . esc_html($tdDest));
746 }
747
748 return $message;
749 }
750
751 /**
752 * @return array<string, mixed>
753 */
754 function getRedirectTypeAndDest(): array {
755
756 $response = array();
757 $response['type'] = "";
758 $response['dest'] = "";
759 $response['message'] = "";
760 $userEnteredURL = '';
761
762 $postedCode = isset($_POST['code']) && is_scalar($_POST['code']) ? (string)$_POST['code'] : '';
763 if ($postedCode === '410' || $postedCode === '451') {
764 $response['type'] = (string)ABJ404_TYPE_HOME;
765 $response['dest'] = '';
766 return $response;
767 }
768
769 if (!isset($_POST['redirect_to_data_field_id']) || $_POST['redirect_to_data_field_id'] === '') {
770 $response['message'] = __('Error: Redirect destination is required.', '404-solution') . "<BR/>";
771 return $response;
772 }
773
774 if ($_POST['redirect_to_data_field_id'] == ABJ404_TYPE_EXTERNAL . '|' . ABJ404_TYPE_EXTERNAL) {
775 $rawEnteredURLResult = $this->f->getPostOrGetSanitizeUrl('redirect_to_user_field');
776 $rawEnteredURL = is_string($rawEnteredURLResult) ? $rawEnteredURLResult : null;
777 $userEnteredURL = $this->urlNormalization->normalizeExternalDestinationUrl($rawEnteredURL);
778 $userEnteredURL = esc_url($userEnteredURL, array('http', 'https'));
779 if ($userEnteredURL == "") {
780 $response['message'] = __('Error: You selected external URL but did not enter a URL.', '404-solution') . "<BR/>";
781
782 } else if ($this->f->strlen($userEnteredURL) < 8) {
783 $response['message'] = __('Error: External URL is too short.', '404-solution') . "<BR/>";
784
785 } else if ($this->f->strpos($userEnteredURL, "://") === false) {
786 $response['message'] = __("Error: External URL doesn't contain ://", '404-solution') . "<BR/>";
787
788 } else {
789 $parsed_url = parse_url($userEnteredURL);
790 if (!is_array($parsed_url) || !isset($parsed_url['scheme']) || !in_array(strtolower($parsed_url['scheme']), array('http', 'https'))) {
791 $response['message'] = __('Error: External URL must use http:// or https:// protocol only.', '404-solution') . "<BR/>";
792 }
793
794 $validated_url = apply_filters('abj404_validate_external_redirect', $userEnteredURL);
795 if ($validated_url === false) {
796 $response['message'] = __('Error: External redirect URL failed validation.', '404-solution') . "<BR/>";
797 } else {
798 $userEnteredURL = $validated_url;
799 }
800 }
801 }
802
803 if ($response['message'] != "") {
804 return $response;
805 }
806 $info = explode("|", sanitize_text_field($_POST['redirect_to_data_field_id']));
807
808 if ($_POST['redirect_to_data_field_id'] == ABJ404_TYPE_EXTERNAL . '|' . ABJ404_TYPE_EXTERNAL) {
809 $response['type'] = ABJ404_TYPE_EXTERNAL;
810 $response['dest'] = $userEnteredURL;
811 } else {
812 if (count($info) == 2) {
813 $response['dest'] = absint($info[0]);
814 $response['type'] = $info[1];
815 } else {
816 $infoJson = json_encode($info);
817 $this->logger->errorMessage("Unexpected info while updating redirect: " .
818 wp_kses_post(is_string($infoJson) ? $infoJson : ''));
819 }
820 }
821
822 return $response;
823 }
824
825 /**
826 * @return string
827 */
828 function addAdminRedirect() {
829 $message = "";
830
831 if (!isset($_POST['manual_redirect_url']) || $_POST['manual_redirect_url'] == "") {
832 $message .= __('Error: URL is a required field.', '404-solution') . "<BR/>";
833 return $message;
834 }
835
836 $manualURL = isset($_POST['manual_redirect_url']) ? wp_unslash($_POST['manual_redirect_url']) : '';
837 $manualURL = $this->urlNormalization->normalizeUserProvidedPath($manualURL);
838 if ($this->f->substr($manualURL, 0, 1) != "/") {
839 $message .= __('Error: URL must start with /', '404-solution') . "<BR/>";
840 return $message;
841 }
842
843 $typeAndDest = $this->getRedirectTypeAndDest();
844
845 $tdMsg = is_string($typeAndDest['message']) ? $typeAndDest['message'] : '';
846 if ($tdMsg != "") {
847 return $tdMsg;
848 }
849
850 $tdType2 = is_scalar($typeAndDest['type']) ? (string)$typeAndDest['type'] : '';
851 $tdDest2 = is_scalar($typeAndDest['dest']) ? (string)$typeAndDest['dest'] : '';
852 $postedCodeForCheck2 = isset($_POST['code']) && is_scalar($_POST['code']) ? (string)$_POST['code'] : '';
853 $code410 = $postedCodeForCheck2 === '410' || $postedCodeForCheck2 === '451';
854 if ($tdType2 != "" && ($tdDest2 !== "" || $code410)) {
855 $statusType = ABJ404_STATUS_MANUAL;
856 if (isset($_POST['is_regex_url']) &&
857 $_POST['is_regex_url'] != '0') {
858
859 $statusType = ABJ404_STATUS_REGEX;
860 }
861
862 $code = isset($_POST['code']) && is_scalar($_POST['code']) && (string)$_POST['code'] !== '' ? (string)$_POST['code'] : '301';
863
864 $originalManualURL = $manualURL;
865 $autoPromoteAdd = $this->maybeAutoPromoteRegex($statusType, $manualURL);
866 $statusType = $autoPromoteAdd['statusType'];
867 $manualURL = $autoPromoteAdd['url'];
868
869 $newRedirectId = $this->redirectsRepo->setupRedirect($manualURL, (string)$statusType,
870 $tdType2, $tdDest2,
871 sanitize_text_field($code), 0);
872 if ($autoPromoteAdd['autoPromoted']) {
873 $this->saveRegexAutoPromoteNotice((int)$newRedirectId, $originalManualURL, $manualURL, $autoPromoteAdd['urlRewritten']);
874 }
875 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
876
877 } else {
878 $message .= __('Error: Data not formatted properly.', '404-solution') . "<BR/>";
879 $this->logger->errorMessage("Add redirect data issue. Type: " . esc_html($tdType2) . ", dest: " .
880 esc_html($tdDest2));
881 }
882
883 return $message;
884 }
885
886 /**
887 * @param int $statusTypeIn
888 * @param string $fromURL
889 * @return array{statusType: int, url: string, autoPromoted: bool, urlRewritten: bool}
890 */
891 private function maybeAutoPromoteRegex($statusTypeIn, $fromURL) {
892 $result = array(
893 'statusType' => (int)$statusTypeIn,
894 'url' => is_string($fromURL) ? $fromURL : '',
895 'autoPromoted' => false,
896 'urlRewritten' => false,
897 );
898
899 if ((int)$statusTypeIn === ABJ404_STATUS_REGEX) {
900 return $result;
901 }
902 if (!ABJ_404_Solution_RegexAutoPromote::looksLikeUnambiguousRegex($result['url'])) {
903 return $result;
904 }
905
906 $result['statusType'] = ABJ404_STATUS_REGEX;
907 $result['autoPromoted'] = true;
908 $glob = ABJ_404_Solution_RegexAutoPromote::applyGlobFixup($result['url']);
909 $result['url'] = $glob['url'];
910 $result['urlRewritten'] = $glob['changed'];
911
912 return $result;
913 }
914
915 /**
916 * @param int $redirectId
917 * @param string $originalURL
918 * @param string $newURL
919 * @param bool $urlRewritten
920 * @return void
921 */
922 private function saveRegexAutoPromoteNotice($redirectId, $originalURL, $newURL, $urlRewritten) {
923 ABJ_404_Solution_RegexAutoPromote::saveNotice($redirectId, $originalURL, $newURL, $urlRewritten);
924 }
925
926 /**
927 * @return string Human-readable result message.
928 */
929 function handleActionUndoRegexAutoPromote() {
930 $notice = ABJ_404_Solution_RegexAutoPromote::readNotice();
931 if ($notice === null || $notice['redirect_id'] <= 0) {
932 return __('Error: No regex auto-promotion to undo.', '404-solution');
933 }
934 $redirectsTable = $this->dbCore->doTableNameReplacements('{wp_abj404_redirects}');
935 $sql = "UPDATE `" . $redirectsTable . "` SET `url` = %s, `status` = %d WHERE `id` = %d";
936 $this->dbCore->queryAndGetResults($sql, array('query_params' => array(
937 $notice['original_url'],
938 (int)ABJ404_STATUS_MANUAL,
939 (int)$notice['redirect_id'],
940 )));
941 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
942 ABJ_404_Solution_RegexAutoPromote::clearNotice();
943 return sprintf(
944 /* translators: %s = the original from_url string that was restored */
945 __('Regex auto-promotion undone. Restored "%s" with status Manual.', '404-solution'),
946 $notice['original_url']
947 );
948 }
949
950 /**
951 * @return string Human-readable result message.
952 */
953 private function handleActionImportFromPlugin(): string {
954 $source = isset($_POST['import_source']) && is_string($_POST['import_source'])
955 ? sanitize_text_field($_POST['import_source'])
956 : '';
957
958 if ($source === '') {
959 return __('Error: No source plugin specified.', '404-solution');
960 }
961
962 $allowedSources = array('rankmath', 'yoast', 'aioseo', 'safe-redirect-manager', 'redirection');
963 if (!in_array($source, $allowedSources, true)) {
964 return sprintf(
965 /* translators: %s = unknown source identifier */
966 __('Error: Unknown source plugin "%s".', '404-solution'),
967 esc_html($source)
968 );
969 }
970
971 $importer = new ABJ_404_Solution_CrossPluginImporter($this->dao, $this->logger);
972 $count = $importer->importFrom($source);
973
974 if ($count > 0) {
975 $this->viewBuild->markViewDoneInvalidatedByAdminMutation();
976 }
977
978 return sprintf(
979 /* translators: %d = number of redirects imported */
980 _n(
981 '%d redirect imported successfully.',
982 '%d redirects imported successfully.',
983 $count,
984 '404-solution'
985 ),
986 $count
987 );
988 }
989
990 }
991