PluginProbe
404 Solution / 4.3.3
404 Solution v4.3.3
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 / view / View_AdminChrome.php

View_AdminChrome.php in 404 Solution 4.3.3, at includes/view/View_AdminChrome.php

297 lines 18.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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' => '&#10003;',
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