| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Renders reusable admin-page chrome for plugin screens. |
| 9 |
* |
| 10 |
* Owns header tabs, footer freshness, native notices, postboxes, option |
| 11 |
* sections, card icons, save controls, restore modal, toast container, and |
| 12 |
* settings mode toggles. |
| 13 |
*/ |
| 14 |
class ABJ_404_Solution_View_AdminChrome extends ABJ_404_Solution_ViewComponent { |
| 15 |
|
| 16 |
private function tpl(string $name): string { |
| 17 |
$raw = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/' . $name, false); |
| 18 |
return rtrim((string)$raw, "\n"); |
| 19 |
} |
| 20 |
|
| 21 |
/** @param array<string,string> $vars */ |
| 22 |
private function fillTpl(string $name, array $vars): string { |
| 23 |
$tpl = $this->tpl($name); |
| 24 |
$search = array(); |
| 25 |
$replace = array(); |
| 26 |
foreach ($vars as $k => $v) { |
| 27 |
$search[] = '{' . $k . '}'; |
| 28 |
$replace[] = $v; |
| 29 |
} |
| 30 |
if (is_object($this->f)) { |
| 31 |
return (string)$this->f->str_replace($search, $replace, $tpl); |
| 32 |
} |
| 33 |
return (string)str_replace($search, $replace, $tpl); |
| 34 |
} |
| 35 |
|
| 36 |
/** @return void */ |
| 37 |
function echoAdminFooter(): void { |
| 38 |
$html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/adminFooter.html"); |
| 39 |
$html = $this->f->str_replace('{JAPANESE_FLASHCARDS_URL}', ABJ404_FC_URL, $html); |
| 40 |
|
| 41 |
$html = $this->f->doNormalReplacements($html); |
| 42 |
echo $html; |
| 43 |
} |
| 44 |
|
| 45 |
function outputAdminHeaderTabs(string $sub = 'list', string $message = ''): void { |
| 46 |
ABJ_404_Solution_WPNotices::echoAdminNotices(); |
| 47 |
|
| 48 |
echo $this->tpl('adminHeaderWrapOpen.html'); |
| 49 |
|
| 50 |
if (isset($_GET['setup_complete']) && $_GET['setup_complete'] === '1') { |
| 51 |
$options = abj_service('options_repository')->getOptions(); |
| 52 |
$auto = !empty($options['auto_redirects']) && $options['auto_redirects'] !== '0'; |
| 53 |
$notify = !empty($options['admin_notification']) && $options['admin_notification'] !== '0'; |
| 54 |
|
| 55 |
if ($auto && $notify) { |
| 56 |
$body = 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'); |
| 57 |
} elseif ($auto) { |
| 58 |
$body = esc_html__('404 Solution is now monitoring your site. When visitors hit broken links, they will be automatically redirected.', '404-solution'); |
| 59 |
} elseif ($notify) { |
| 60 |
$body = esc_html__('404 Solution is now monitoring your site. We will email you if captured 404 URLs need attention.', '404-solution'); |
| 61 |
} else { |
| 62 |
$body = esc_html__('404 Solution is now monitoring your site. You can create manual redirects anytime from the Page Redirects tab.', '404-solution'); |
| 63 |
} |
| 64 |
echo $this->fillTpl('setupCompleteWelcomeBanner.html', array( |
| 65 |
'heading' => esc_html__("You're all set!", '404-solution'), |
| 66 |
'body' => $body, |
| 67 |
)) . "\n"; |
| 68 |
} |
| 69 |
|
| 70 |
if ($message != "") { |
| 71 |
$allowed_tags = array( |
| 72 |
'br' => array(), |
| 73 |
'em' => array(), |
| 74 |
'strong' => array(), |
| 75 |
); |
| 76 |
|
| 77 |
if (($this->f->strlen($message) >= 6) && ($this->f->substr($this->f->strtolower($message), 0, 6) == 'error:')) { |
| 78 |
$cssClasses = 'notice notice-error'; |
| 79 |
} else { |
| 80 |
$cssClasses = 'notice notice-success'; |
| 81 |
} |
| 82 |
|
| 83 |
echo $this->fillTpl('adminHeaderMessageNotice.html', array( |
| 84 |
'cssClasses' => $cssClasses, |
| 85 |
'message' => (string)wp_kses($message, $allowed_tags), |
| 86 |
)) . "\n"; |
| 87 |
} |
| 88 |
|
| 89 |
if (class_exists('ABJ_404_Solution_RegexAutoPromote')) { |
| 90 |
$regexNotice = ABJ_404_Solution_RegexAutoPromote::readNotice(); |
| 91 |
if (is_array($regexNotice) && $regexNotice['redirect_id'] > 0) { |
| 92 |
$this->renderRegexAutoPromoteNotice($regexNotice); |
| 93 |
} |
| 94 |
} |
| 95 |
|
| 96 |
$isSimpleMode = abj_service('settings_mode_preference')->getMode() === 'simple'; |
| 97 |
$tabs = array( |
| 98 |
array('abj404_redirects', __('Page Redirects', '404-solution')), |
| 99 |
array('abj404_captured', __('Captured 404s', '404-solution')), |
| 100 |
); |
| 101 |
if (!$isSimpleMode) { |
| 102 |
$tabs[] = array('abj404_logs', __('Logs', '404-solution')); |
| 103 |
$tabs[] = array('abj404_stats', __('Stats', '404-solution')); |
| 104 |
} |
| 105 |
$tabs[] = array('abj404_tools', __('Tools', '404-solution')); |
| 106 |
$tabs[] = array('abj404_options', __('Options', '404-solution')); |
| 107 |
|
| 108 |
$itemTpl = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminHeaderTab.html'); |
| 109 |
$tabsHtml = ''; |
| 110 |
foreach ($tabs as $pair) { |
| 111 |
list($subKey, $label) = $pair; |
| 112 |
$isActive = ($sub == $subKey); |
| 113 |
$row = $itemTpl; |
| 114 |
$row = str_replace('{url}', esc_url('?page=' . ABJ404_PP . '&subpage=' . $subKey), $row); |
| 115 |
$row = str_replace('{activeClass}', $isActive ? ' nav-tab-active active' : '', $row); |
| 116 |
$row = str_replace('{ariaSelected}', $isActive ? ' aria-selected="true"' : '', $row); |
| 117 |
$row = str_replace('{label}', esc_html($label), $row); |
| 118 |
$tabsHtml .= $row; |
| 119 |
} |
| 120 |
|
| 121 |
$outer = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . '/html/adminHeaderTabs.html'); |
| 122 |
echo str_replace('{tabs}', $tabsHtml, $outer); |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* @param array{redirect_id: int, original_url: string, new_url: string, url_rewritten: bool, created_at: int} $notice |
| 127 |
* @return void |
| 128 |
*/ |
| 129 |
public function renderRegexAutoPromoteNotice(array $notice): void { |
| 130 |
$redirectId = (int)$notice['redirect_id']; |
| 131 |
$originalUrl = (string)$notice['original_url']; |
| 132 |
$newUrl = (string)$notice['new_url']; |
| 133 |
$urlRewritten = !empty($notice['url_rewritten']); |
| 134 |
|
| 135 |
$editUrl = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_edit&id=' . $redirectId); |
| 136 |
$undoBase = admin_url('admin.php?page=' . ABJ404_PP . '&subpage=abj404_redirects&action=undoRegexAutoPromote'); |
| 137 |
$undoUrl = wp_nonce_url($undoBase, 'abj404undoRegexAutoPromote'); |
| 138 |
|
| 139 |
if ($urlRewritten) { |
| 140 |
$message = sprintf( |
| 141 |
__('Detected as a regex pattern. Stored "%1$s" as "%2$s".', '404-solution'), |
| 142 |
$originalUrl, |
| 143 |
$newUrl |
| 144 |
); |
| 145 |
} else { |
| 146 |
$message = sprintf( |
| 147 |
__('Detected as a regex pattern. Stored "%s" with Regex status.', '404-solution'), |
| 148 |
$originalUrl |
| 149 |
); |
| 150 |
} |
| 151 |
|
| 152 |
echo $this->fillTpl('regexAutoPromoteNotice.html', array( |
| 153 |
'label' => esc_html__('404 Solution:', '404-solution'), |
| 154 |
'message' => esc_html($message), |
| 155 |
'editUrl' => esc_url($editUrl), |
| 156 |
'editLabel' => esc_html__('Edit', '404-solution'), |
| 157 |
'undoUrl' => esc_url($undoUrl), |
| 158 |
'confirm' => esc_js(__('Undo regex auto-promotion and restore Manual status?', '404-solution')), |
| 159 |
'undoLabel' => esc_html__('Undo', '404-solution'), |
| 160 |
)) . "\n"; |
| 161 |
} |
| 162 |
|
| 163 |
/** |
| 164 |
* @param string|int $id |
| 165 |
* @param mixed $content |
| 166 |
*/ |
| 167 |
function echoPostBox($id, string $title, $content): void { |
| 168 |
echo $this->fillTpl('postBox.html', array( |
| 169 |
'id' => esc_attr((string)$id), |
| 170 |
'title' => esc_html($title), |
| 171 |
'content' => (string)$content, |
| 172 |
)); |
| 173 |
} |
| 174 |
|
| 175 |
function echoOptionsSection(ABJ_404_Solution_OptionsSectionView $section): void { |
| 176 |
$expandedClass = $section->initiallyVisible ? ' expanded' : ''; |
| 177 |
$badgeHtml = ''; |
| 178 |
if ($section->badge) { |
| 179 |
$badgeHtml = $this->fillTpl('optionsSectionBadge.html', array( |
| 180 |
'badge' => esc_html($section->badge), |
| 181 |
)); |
| 182 |
} |
| 183 |
|
| 184 |
echo $this->fillTpl('optionsSection.html', array( |
| 185 |
'expandedClass' => esc_attr($expandedClass), |
| 186 |
'sectionId' => esc_attr($section->sectionId), |
| 187 |
'postboxId' => esc_attr($section->postboxId), |
| 188 |
'ariaExpanded' => $section->initiallyVisible ? 'true' : 'false', |
| 189 |
'icon' => (string)$section->icon, |
| 190 |
'title' => esc_html($section->title), |
| 191 |
'badge' => $badgeHtml, |
| 192 |
'content' => (string)$section->content, |
| 193 |
)); |
| 194 |
} |
| 195 |
|
| 196 |
function getCardIcon(string $iconName): string { |
| 197 |
$icons = array( |
| 198 |
'lightning' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>', |
| 199 |
'gear' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>', |
| 200 |
'filter' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path></svg>', |
| 201 |
'document' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>', |
| 202 |
'sliders' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path></svg>', |
| 203 |
'lightbulb' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path></svg>', |
| 204 |
'chart' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>', |
| 205 |
'warning' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>', |
| 206 |
'clock' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>', |
| 207 |
'download' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>', |
| 208 |
'upload' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>', |
| 209 |
'trash' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>', |
| 210 |
'database' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>', |
| 211 |
'cog' => '<svg class="abj404-card-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>' |
| 212 |
); |
| 213 |
return isset($icons[$iconName]) ? $icons[$iconName] : ''; |
| 214 |
} |
| 215 |
|
| 216 |
function echoStickySaveBar(): void { |
| 217 |
$version = ABJ404_VERSION; |
| 218 |
$restoreNonce = wp_create_nonce('abj404_restore_defaults'); |
| 219 |
$versionLabel = esc_html(sprintf(__('Plugin v%s', '404-solution'), $version)); |
| 220 |
|
| 221 |
echo $this->fillTpl('stickySaveBar.html', array( |
| 222 |
'versionLabel' => $versionLabel, |
| 223 |
'restoreNonce' => esc_attr($restoreNonce), |
| 224 |
'restoreLabel' => esc_html__('Restore Defaults', '404-solution'), |
| 225 |
'saveLabel' => esc_attr__('Save Settings', '404-solution'), |
| 226 |
)); |
| 227 |
$this->echoRestoreDefaultsModal(); |
| 228 |
} |
| 229 |
|
| 230 |
function echoRestoreDefaultsModal(): void { |
| 231 |
echo "\n" . $this->fillTpl('restoreDefaultsModal.html', array( |
| 232 |
'title' => esc_html__('Restore Default Settings?', '404-solution'), |
| 233 |
'closeLabel' => esc_attr__('Close', '404-solution'), |
| 234 |
'body' => esc_html__('This will reset all plugin settings to their default values. This cannot be undone. Your redirect rules and 404 logs will not be affected.', '404-solution'), |
| 235 |
'cancelLabel' => esc_html__('Cancel', '404-solution'), |
| 236 |
'confirmLabel' => esc_html__('Restore Defaults', '404-solution'), |
| 237 |
)) . "\n "; |
| 238 |
} |
| 239 |
|
| 240 |
function echoToastNotification(): void { |
| 241 |
echo $this->fillTpl('toastNotification.html', array( |
| 242 |
'icon' => '✓', |
| 243 |
'message' => esc_html__('Settings saved successfully!', '404-solution'), |
| 244 |
)); |
| 245 |
} |
| 246 |
|
| 247 |
function echoExpandCollapseButton(bool $showSuggestions = true): void { |
| 248 |
echo "\n" . $this->fillTpl('expandCollapseButton.html', array( |
| 249 |
'expandAllLabel' => esc_html__('Expand All', '404-solution'), |
| 250 |
'saveLabel' => esc_attr__('Save Settings', '404-solution'), |
| 251 |
)) . "\n "; |
| 252 |
} |
| 253 |
|
| 254 |
function echoSettingsModeToggle(string $currentMode): void { |
| 255 |
$simpleActive = ($currentMode === 'simple') ? 'active' : ''; |
| 256 |
$advancedActive = ($currentMode === 'advanced') ? 'active' : ''; |
| 257 |
$simplePressedState = ($currentMode === 'simple') ? 'true' : 'false'; |
| 258 |
$advancedPressedState = ($currentMode === 'advanced') ? 'true' : 'false'; |
| 259 |
|
| 260 |
if ($currentMode === 'simple') { |
| 261 |
$modeDescription = __('Simple Mode shows essential options only. Switch to Advanced Mode for full configuration.', '404-solution'); |
| 262 |
} else { |
| 263 |
$modeDescription = __('Advanced Mode shows all options. Switch to Simple Mode for a streamlined view.', '404-solution'); |
| 264 |
} |
| 265 |
|
| 266 |
$html = ABJ_404_Solution_FileSystemService::readFileContents(dirname(__DIR__) . "/html/settingsModeToggle.html"); |
| 267 |
$html = $this->f->str_replace('{nonce}', wp_create_nonce('abj404_mode_toggle'), $html); |
| 268 |
$html = $this->f->str_replace('{simpleActive}', $simpleActive, $html); |
| 269 |
$html = $this->f->str_replace('{advancedActive}', $advancedActive, $html); |
| 270 |
$html = $this->f->str_replace('{simplePressedState}', $simplePressedState, $html); |
| 271 |
$html = $this->f->str_replace('{advancedPressedState}', $advancedPressedState, $html); |
| 272 |
$html = $this->f->str_replace('{Simple Mode}', __('Simple Mode', '404-solution'), $html); |
| 273 |
$html = $this->f->str_replace('{Advanced Mode}', __('Advanced Mode', '404-solution'), $html); |
| 274 |
$html = $this->f->str_replace('{mode_description}', $modeDescription, $html); |
| 275 |
|
| 276 |
echo $html; |
| 277 |
} |
| 278 |
|
| 279 |
function echoInlineModeToggle(): void { |
| 280 |
$currentMode = abj_service('settings_mode_preference')->getMode(); |
| 281 |
$simpleActive = ($currentMode === 'simple') ? 'active' : ''; |
| 282 |
$advancedActive = ($currentMode === 'advanced') ? 'active' : ''; |
| 283 |
$simplePressedState = ($currentMode === 'simple') ? 'true' : 'false'; |
| 284 |
$advancedPressedState = ($currentMode === 'advanced') ? 'true' : 'false'; |
| 285 |
|
| 286 |
echo $this->fillTpl('inlineModeToggle.html', array( |
| 287 |
'nonce' => esc_attr(wp_create_nonce('abj404_mode_toggle')), |
| 288 |
'simpleActive' => esc_attr($simpleActive), |
| 289 |
'advancedActive' => esc_attr($advancedActive), |
| 290 |
'simplePressedState' => esc_attr($simplePressedState), |
| 291 |
'advancedPressedState' => esc_attr($advancedPressedState), |
| 292 |
'simpleLabel' => esc_html__('Simple', '404-solution'), |
| 293 |
'advancedLabel' => esc_html__('Advanced', '404-solution'), |
| 294 |
)); |
| 295 |
} |
| 296 |
} |
| 297 |
|