";
}
/** Do an action like trash/delete/ignore/edit and display a page like stats/logs/redirects/options.
* @return void
*/
static function handleMainAdminPageActionAndDisplay() {
global $abj404view;
$instance = self::getInstance();
try {
$action = $instance->dao->getPostOrGetSanitize('action');
if (!is_admin() || !$instance->logic->userIsPluginAdmin()) {
$instance->logger->logUserCapabilities("handleMainAdminPageActionAndDisplay (" .
esc_html($action == '' ? '(none)' : $action) . ")");
echo '
';
echo '
' . esc_html(PLUGIN_NAME) . '
';
echo '
';
echo '' . esc_html__('Permission denied.', '404-solution') . ' ';
echo esc_html__('Your user account does not have permission to access this page.', '404-solution');
echo '
';
echo esc_html__('Please verify that your WordPress role has the', '404-solution') . ' ';
echo 'manage_options ' . esc_html__('capability.', '404-solution') . ' ';
echo esc_html__('If you have a security plugin installed, it may be restricting access to this page.', '404-solution');
echo '
';
return;
}
$sub = "";
// --------------------------------------------------------------------
// Handle Post Actions
$instance->logger->debugMessage("Processing request for action: " .
esc_html($action == '' ? '(none)' : $action));
// this should really not pass things by reference so it can be more object oriented (encapsulation etc).
$message = "";
$message .= $instance->logic->handlePluginAction($action, $sub);
$message .= $instance->logic->hanldeTrashAction();
$message .= $instance->logic->handleDeleteAction();
$message .= $instance->logic->handleIgnoreAction();
$message .= $instance->logic->handleLaterAction();
$message .= $instance->logic->handleActionEdit($sub, $action);
$message .= $instance->logic->handleActionImportRedirects();
$instance->logic->handleActionChangeItemsPerRow();
$message .= $instance->logic->handleActionImportFile();
// --------------------------------------------------------------------
// Output the correct page.
$abj404view->echoChosenAdminTab($action, $sub, $message);
} catch (\Throwable $e) {
$encodedEx = json_encode($e);
$instance->logger->errorMessage("Caught exception: " . stripcslashes(wp_kses_post(is_string($encodedEx) ? $encodedEx : '')));
echo '
';
echo '
';
echo '
404 Solution: An error occurred while rendering this page.
';
}
}
/** Display the chosen admin page.
* @param string $action
* @param string $sub
* @param string $message
* @return void
*/
function echoChosenAdminTab($action, $sub, $message) {
global $abj404view;
// If globals are not set, use sensible defaults
if ($abj404view === null) {
$abj404view = $this;
}
// Deal With Page Tabs
if ($sub == "") {
$sub = $this->f->strtolower($this->dao->getPostOrGetSanitize('subpage'));
}
if ($sub == "") {
$sub = 'abj404_redirects';
$this->logger->debugMessage('No tab selected. Displaying the "redirects" tab.');
}
// Check if we're returning from a successful redirect update
$updated = $this->dao->getPostOrGetSanitize('updated');
if ($updated == '1') {
$message .= __('Redirect Information Updated Successfully!', '404-solution');
}
$this->logger->debugMessage("Displaying sub page: " . esc_html($sub == '' ? '(none)' : $sub));
$abj404view->outputAdminHeaderTabs($sub, $message);
$abj404action = $this->dao->getPostOrGetSanitize('abj404action');
if (($action == 'editRedirect') || ($abj404action == 'editRedirect') || ($sub == 'abj404_edit')) {
$abj404view->echoAdminEditRedirectPage();
} else if ($sub == 'abj404_redirects') {
$abj404view->echoAdminRedirectsPage();
} else if ($sub == 'abj404_captured') {
$abj404view->echoAdminCapturedURLsPage();
} else if ($sub == "abj404_options") {
$abj404view->echoAdminOptionsPage();
} else if ($sub == 'abj404_logs') {
$abj404view->echoAdminLogsPage();
} else if ($sub == 'abj404_stats') {
$abj404view->outputAdminStatsPage();
} else if ($sub == 'abj404_tools') {
$abj404view->echoAdminToolsPage();
} else if ($sub == 'abj404_debugfile') {
$abj404view->echoAdminDebugFile();
} else {
$this->logger->debugMessage('No tab selected. Displaying the "redirects" tab.');
$abj404view->echoAdminRedirectsPage();
}
$abj404view->echoAdminFooter();
}
/**
* Echo the text that appears at the bottom of each admin page.
* @return void
*/
function echoAdminFooter() {
// read the html content.
$html = ABJ_404_Solution_Functions::readFileContents(__DIR__ . "/html/adminFooter.html");
$html = $this->f->str_replace('{JAPANESE_FLASHCARDS_URL}', ABJ404_FC_URL, $html);
$html = $this->f->str_replace(
'{ABJ404_VIEW_FRESHNESS}',
$this->renderViewFreshnessLabel(),
$html
);
// constants and translations.
$html = $this->f->doNormalReplacements($html);
echo $html;
}
/**
* Render a short, human-readable "how long ago was the view_done cache
* last successfully built" label for the admin footer. Returns one of:
* - "n/a" when never built or freshness option cleared (post-invalidate);
* - "Xs" when 0 to 59 seconds old;
* - "Xm" when 1 to 59 minutes old;
* - "Xh" when 1 to 23 hours old;
* - "Xd" when a day or more.
*
* The view_done snapshot is what the Redirects / Captured / Logs tables
* read from; if this label drifts upward into hours when the freshness
* TTL is 120s, the auto-rebuild path is failing or being blocked.
*
* @return string
*/
private function renderViewFreshnessLabel(): string {
if (!is_object($this->dao) || !method_exists($this->dao, 'getViewDoneBuiltAtTimestamp')) {
return 'n/a';
}
// Defensive: a unit-test DAO may be a Mockery mock that throws
// BadMethodCallException when called without an expectation; any
// future DAO implementation could also throw on a transient read
// failure. The freshness label is a footer cosmetic. Treat any
// failure as n/a so it never blocks the page render. Without this
// try/catch, dozens of pre-existing unit tests that mock the DAO
// without explicitly stubbing this method threw on every footer
// render.
try {
$builtAt = (int)$this->dao->getViewDoneBuiltAtTimestamp();
} catch (\Throwable $e) {
return 'n/a';
}
if ($builtAt <= 0) {
return 'n/a';
}
$age = time() - $builtAt;
if ($age < 0) {
return 'n/a'; // clock skew; do not surface a negative age
}
if ($age < 60) { return $age . 's'; }
if ($age < 3600) { return intval($age / 60) . 'm'; }
if ($age < 86400) { return intval($age / 3600) . 'h'; }
return intval($age / 86400) . 'd';
}
/** Output the tabs at the top of the plugin page.
* @param string $sub
* @param string $message
* @return void
*/
function outputAdminHeaderTabs($sub = 'list', $message = '') {
ABJ_404_Solution_WPNotices::echoAdminNotices();
echo "
';
echo '' . esc_html__("You're all set!", '404-solution') . ' ';
if ($auto && $notify) {
echo esc_html__('404 Solution is now monitoring your site. When visitors hit broken links, they will be automatically redirected. We will email you if something needs attention.', '404-solution');
} elseif ($auto) {
echo esc_html__('404 Solution is now monitoring your site. When visitors hit broken links, they will be automatically redirected.', '404-solution');
} elseif ($notify) {
echo esc_html__('404 Solution is now monitoring your site. We will email you if captured 404 URLs need attention.', '404-solution');
} else {
echo esc_html__('404 Solution is now monitoring your site. You can create manual redirects anytime from the Page Redirects tab.', '404-solution');
}
echo '
\n";
}
$isSimpleMode = $this->logic->getSettingsMode() === 'simple';
echo '';
}
/** This outputs a box with a title and some content in it.
* It's used on the Stats, Options and Tools page (for example).
* @param int|string $id
* @param string $title
* @param string $content
* @return void
*/
function echoPostBox($id, $title, $content) {
echo "
";
echo "
" . esc_html($title) . "
";
echo "
" . $content /* Can't escape here, as contains forms */ . "
";
echo "
";
}
/**
* Echo an accordion section using card-based layout
* @param string $sectionId The section identifier
* @param string $postboxId The ID for the postbox
* @param string $title The title of the section
* @param string $content The content to display
* @param bool $initiallyVisible Whether the card starts expanded (default: false)
* @param string $icon The SVG icon for the card header (optional)
* @param string $badge Optional info badge text
* @return void
*/
function echoOptionsSection($sectionId, $postboxId, $title, $content, $initiallyVisible = false, $icon = '', $badge = '') {
$expandedClass = $initiallyVisible ? ' expanded' : '';
echo "
";
echo "
";
echo "
";
if ($icon) {
echo $icon; // Icon is pre-sanitized SVG
}
echo esc_html($title);
if ($badge) {
echo "" . esc_html($badge) . "";
}
echo "
";
echo "";
echo "
";
echo "
";
echo $content; /* Can't escape here, as contains forms */
echo "
";
echo "
";
}
/**
* Get SVG icon for a section
* @param string $iconName The icon name
* @return string The SVG icon markup
*/
function getCardIcon($iconName) {
$icons = array(
'lightning' => '',
'gear' => '',
'filter' => '',
'document' => '',
'sliders' => '',
'lightbulb' => '',
// Stats page icons
'chart' => '',
'warning' => '',
'clock' => '',
// Tools page icons
'download' => '',
'upload' => '',
'trash' => '',
'database' => '',
'cog' => ''
);
return isset($icons[$iconName]) ? $icons[$iconName] : '';
}
/**
* Echo the sticky save bar
* @return void
*/
function echoStickySaveBar() {
$version = ABJ404_VERSION;
echo '
';
echo '
';
/* translators: %s: plugin version number */
echo esc_html(sprintf(__('Plugin v%s', '404-solution'), $version));
echo '
';
echo '
';
echo '';
echo '
';
echo '
';
}
/**
* Echo the success toast notification container
* @return void
*/
function echoToastNotification() {
echo '
';
}
/**
* Echo the expand/collapse all button and save button
* @param bool $showSuggestions Not used (kept for compatibility)
* @return void
*/
function echoExpandCollapseButton($showSuggestions = true) {
?>