PluginProbe
Accessibility by AllAccessible / trunk
Accessibility by AllAccessible vtrunk
2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.6 trunk 1.0 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2 1.2.1 1.2.10 1.2.2 1.2.3 1.2.4 1.2.5 All 43 releases
allaccessible / inc / ScanTriggerPanel.php

ScanTriggerPanel.php in Accessibility by AllAccessible trunk, at inc/ScanTriggerPanel.php

405 lines 24.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Reusable "Run your first scan" panel.
4 *
5 * @package AllAccessible
6 * @since 2.1.0
7 */
8
9 if (!defined('ABSPATH')) {
10 exit;
11 }
12
13 final class AllAccessible_ScanTriggerPanel {
14
15 /**
16 * Print the panel markup. Caller decides the context (wizard / settings /
17 * fixes-page) which influences copy + redirect-after-success behavior.
18 *
19 * @param array $opts {
20 * @type string $context 'wizard' | 'settings' | 'fixes' — for analytics + copy
21 * @type string $heading Override panel heading
22 * @type string $description Override description line
23 * @type string $success_redirect URL to send the user after scan finishes (empty = stay)
24 * @type bool $compact Tight layout for sidebar / wizard contexts
25 * }
26 */
27 public static function render(array $opts = array()) {
28 $context = isset($opts['context']) ? sanitize_key($opts['context']) : 'settings';
29 $heading = isset($opts['heading']) ? (string) $opts['heading'] : __('Run your first scan', 'allaccessible');
30 $description = isset($opts['description']) ? (string) $opts['description'] : __('AllAccessible automatically finds every published page on your site — no sitemap needed. Start your first scan below; future scans then run automatically based on your plan.', 'allaccessible');
31 $success_redirect = isset($opts['success_redirect']) ? esc_url((string) $opts['success_redirect']) : '';
32 $compact = !empty($opts['compact']);
33 $nonce = wp_create_nonce(AACB_SCAN_NONCE);
34
35 $client = class_exists('AllAccessible_ApiClient') ? AllAccessible_ApiClient::get_instance() : null;
36 $last_meta = $client ? $client->get_last_scan_meta() : array('triggeredAt' => null, 'sitemapUrl' => '');
37 $schedule = $client ? $client->get_scan_schedule() : null;
38 $sched_ok = is_array($schedule);
39 $already_scanned = ($sched_ok && !empty($schedule['lastScanAt'])) || !empty($last_meta['triggeredAt']);
40 if ($context === 'wizard') {
41 $already_scanned = false;
42 }
43 $btn_label_first = __('Scan now', 'allaccessible');
44 $btn_label_again = __('Run scan again', 'allaccessible');
45 $primary_button = $already_scanned ? $btn_label_again : $btn_label_first;
46 $page_count = 0;
47 if (function_exists('wp_count_posts')) {
48 $page_counts = wp_count_posts('page');
49 $post_counts = wp_count_posts('post');
50 $page_count = (int) ($page_counts->publish ?? 0) + (int) ($post_counts->publish ?? 0);
51 }
52
53 $body_padding = $compact ? 'var(--aacx-space-4)' : 'var(--aacx-space-6)';
54 ?>
55 <div class="aacb-scan-trigger aacx-v2 allaccessible-admin"
56 data-context="<?php echo esc_attr($context); ?>"
57 data-nonce="<?php echo esc_attr($nonce); ?>"
58 data-success-redirect="<?php echo esc_attr($success_redirect); ?>">
59
60 <div class="aacx-v2__card aacx-v2__card--elevated">
61 <div class="aacx-v2__card-body" style="padding: <?php echo esc_attr($body_padding); ?>;">
62
63 <?php if ($heading !== '') : ?>
64 <h3 style="margin: 0 0 var(--aacx-space-2);"><?php echo esc_html($heading); ?></h3>
65 <?php endif; ?>
66 <p style="font-size: var(--aacx-text-sm); color: var(--aacx-text-muted); margin-bottom: var(--aacx-space-4);">
67 <?php echo esc_html($description); ?>
68 </p>
69
70 <?php if ($sched_ok) :
71 $freq = (string) ($schedule['scanFrequency'] ?? '');
72 $freq_label = (string) ($schedule['scanFrequencyLabel'] ?? '');
73 $last_at = $schedule['lastScanAt'] ?? null;
74 $next_at = $schedule['nextScanAt'] ?? null;
75 $is_free = ($freq === 'never');
76 $banner_variant = $is_free ? 'warn' : 'ok';
77 ?>
78 <div class="aacx-v2__banner aacx-v2__banner--<?php echo esc_attr($banner_variant); ?>" style="margin-bottom: var(--aacx-space-4);">
79 <div>
80 <strong style="font-size: var(--aacx-text-sm);"><?php echo esc_html($freq_label); ?></strong>
81 <p style="font-size: var(--aacx-text-xs); margin-top: var(--aacx-space-1);">
82 <?php if ($last_at) : ?>
83 <?php
84 printf(
85 /* translators: %s: human-readable time diff */
86 esc_html__('Last scan %s ago', 'allaccessible'),
87 esc_html(human_time_diff(strtotime($last_at), current_time('timestamp')))
88 );
89 ?>
90 <?php else : ?>
91 <?php esc_html_e('No scans yet', 'allaccessible'); ?>
92 <?php endif; ?>
93 <?php if ($next_at && !$is_free) : ?>
94 <span style="margin: 0 var(--aacx-space-2);">·</span>
95 <?php
96 $next_ts = strtotime($next_at);
97 $now_ts = current_time('timestamp');
98 if ($next_ts > $now_ts) {
99 printf(
100 /* translators: %s: human-readable time until */
101 esc_html__('Next scan in %s', 'allaccessible'),
102 esc_html(human_time_diff($now_ts, $next_ts))
103 );
104 } else {
105 esc_html_e('Next scan due now', 'allaccessible');
106 }
107 ?>
108 <?php endif; ?>
109 </p>
110 </div>
111 </div>
112 <?php elseif (!empty($last_meta['triggeredAt'])) : ?>
113 <p class="aacx-v2__help aacb-scan-last-triggered" style="margin-bottom: var(--aacx-space-4);">
114 <?php
115 printf(
116 /* translators: %s: human-readable time difference */
117 esc_html__('Last scan triggered %s ago', 'allaccessible'),
118 esc_html(human_time_diff((int) $last_meta['triggeredAt'], current_time('timestamp')))
119 );
120 ?>
121 </p>
122 <?php endif; ?>
123
124 <?php if ($page_count > 0) : ?>
125 <p style="font-size: var(--aacx-text-sm); color: var(--aacx-text-muted); margin: 0 0 var(--aacx-space-3); display: flex; align-items: center; gap: var(--aacx-space-2);">
126 <span class="dashicons dashicons-admin-page" aria-hidden="true" style="color: var(--aacx-primary-600);"></span>
127 <?php
128 printf(
129 /* translators: %d: number of published posts + pages */
130 esc_html(_n(
131 '%d published page will be scanned. This can take a minute or two.',
132 '%d published pages and posts will be scanned. This can take a minute or two.',
133 $page_count,
134 'allaccessible'
135 )),
136 (int) $page_count
137 );
138 ?>
139 </p>
140 <?php endif; ?>
141 <div style="display: flex; align-items: center; gap: var(--aacx-space-3);">
142 <button type="button" class="aacb-scan-start aacx-v2__btn aacx-v2__btn--primary aacx-v2__btn--lg"
143 data-first-label="<?php echo esc_attr($btn_label_first); ?>"
144 data-again-label="<?php echo esc_attr($btn_label_again); ?>">
145 <?php echo esc_html($primary_button); ?> →
146 </button>
147 <span class="aacb-scan-status aacx-hidden" style="font-size: var(--aacx-text-sm); color: var(--aacx-text-muted);"></span>
148 </div>
149
150 <div class="aacb-scan-progress aacx-hidden" style="margin-top: var(--aacx-space-4);">
151 <div role="progressbar" aria-valuemin="0" aria-valuemax="100" style="height: 8px; background: var(--aacx-slate-200); border-radius: var(--aacx-radius-pill); overflow: hidden;">
152 <div class="aacb-scan-bar" style="height: 100%; width: 0%; background: var(--aacx-primary-600); transition: width 200ms ease;"></div>
153 </div>
154 <p style="margin-top: var(--aacx-space-2); font-size: var(--aacx-text-sm); color: var(--aacx-text-muted);">
155 <span class="aacb-scan-done">0</span> /
156 <span class="aacb-scan-total">—</span>
157 <?php esc_html_e('pages scanned', 'allaccessible'); ?>
158 <span class="aacb-scan-eta" style="color: var(--aacx-text-muted);"></span>
159 </p>
160 </div>
161
162 <div class="aacb-scan-done-state aacb-scan-trigger-banner-ok aacx-v2__banner aacx-v2__banner--ok aacx-hidden" style="margin-top: var(--aacx-space-4);">
163 <div>
164 <span aria-hidden="true">âś“</span>
165 <strong class="aacb-scan-pages-final">0</strong>
166 <?php esc_html_e('pages scanned. Accessibility scores appear in your Pages list within a minute or two as the AI finishes reviewing.', 'allaccessible'); ?>
167 <a href="<?php echo esc_url(admin_url('edit.php?post_type=page')); ?>" style="margin-left: var(--aacx-space-2);">
168 <?php esc_html_e('Open Pages list', 'allaccessible'); ?> →
169 </a>
170 </div>
171 </div>
172
173 <div class="aacb-scan-error-state aacx-v2__banner aacx-v2__banner--danger aacx-hidden" style="margin-top: var(--aacx-space-4);">
174 <div class="aacb-scan-error-msg"></div>
175 </div>
176
177 <?php
178 $rg = 'aacb-sitemap-choice-' . esc_attr($context);
179 ?>
180 <details class="aacb-advanced-sitemap" style="margin-top: var(--aacx-space-5);">
181 <summary style="cursor:pointer;font-size:var(--aacx-text-sm);color:var(--aacx-text-muted);">
182 <?php esc_html_e('Scan a specific sitemap instead (advanced)', 'allaccessible'); ?>
183 </summary>
184 <div class="aacb-advanced-sitemap-body" data-loaded="0" data-radio-group="<?php echo esc_attr($rg); ?>" style="margin-top: var(--aacx-space-3);">
185 <p class="aacx-v2__help" style="margin-bottom: var(--aacx-space-3);">
186 <?php esc_html_e('Leave this closed to let AllAccessible auto-discover your pages (recommended). Open only to point the scan at a specific sitemap.', 'allaccessible'); ?>
187 </p>
188 <div class="aacb-sitemap-candidates">
189 <span class="aacx-v2__help aacb-sitemap-loading aacx-hidden"><?php esc_html_e('Looking for sitemaps…', 'allaccessible'); ?></span>
190 </div>
191 </div>
192 </details>
193
194 </div>
195 </div>
196 </div>
197 <?php
198 }
199
200 /**
201 * Enqueue the JS that drives every instance of the panel on the page.
202 */
203 public static function enqueue_inline_script() {
204 static $printed = false;
205 if ($printed) return;
206 $printed = true;
207 ?>
208 <script>
209 (function($) {
210 const POLL_MS = 5000;
211
212 function start(panel) {
213 const $p = $(panel);
214 const nonce = $p.data('nonce');
215 const redirect = $p.data('success-redirect') || '';
216 const $input = $p.find('.aacb-scan-sitemap-input');
217 const choice = $p.find('.aacb-sitemap-radio:checked').val();
218 let sitemap = '';
219 if (choice === '__custom__') {
220 sitemap = ($input.val() || '').trim();
221 if (!sitemap) { showError($p, '<?php echo esc_js(__('Enter your sitemap URL, or close Advanced to auto-discover.', 'allaccessible')); ?>'); return; }
222 } else if (choice && choice !== '__skip__') {
223 sitemap = choice; // a verified candidate URL
224 }
225 // choice empty OR '__skip__' → sitemap stays '' (auto-discover)
226
227 const $btn = $p.find('.aacb-scan-start');
228 const $progress = $p.find('.aacb-scan-progress');
229 const $done = $p.find('.aacb-scan-done-state');
230 const $err = $p.find('.aacb-scan-error-state');
231
232 $btn.prop('disabled', true).text('<?php echo esc_js(__('Starting…', 'allaccessible')); ?>');
233 $err.addClass('aacx-hidden');
234 $done.addClass('aacx-hidden');
235 $progress.removeClass('aacx-hidden');
236
237 $.post(ajaxurl, {
238 action: 'aacb_start_scan',
239 _wpnonce: nonce,
240 sitemap_url: sitemap,
241 viewport: 'both',
242 }).done(function(resp) {
243 if (!resp || !resp.success) {
244 showError($p, (resp && resp.data) || '<?php echo esc_js(__('Scan failed to start.', 'allaccessible')); ?>');
245 $btn.prop('disabled', false).text(($btn.data('first-label') || '<?php echo esc_js(__('Start first scan', 'allaccessible')); ?>') + ' →');
246 return;
247 }
248 // Dispatch the scan; results appear on the Agentic Fixes
249 // page shortly.
250 if (resp.data && resp.data.queued) {
251 $p.removeData('scan-retried');
252 $p.find('.aacb-scan-progress').addClass('aacx-hidden');
253 $p.find('.aacb-scan-pages-final').text('—');
254 $p.find('.aacb-scan-done-state').removeClass('aacx-hidden')
255 .find('p').text('<?php echo esc_js(__('Scan started. Results appear on the Agentic Fixes page in 2-5 minutes.', 'allaccessible')); ?>');
256 $p.find('.aacb-scan-last-triggered').text('<?php echo esc_js(__('Scan triggered just now', 'allaccessible')); ?>');
257 $btn.prop('disabled', false).text(($btn.data('again-label') || '<?php echo esc_js(__('Run scan again', 'allaccessible')); ?>'));
258 if (redirect) setTimeout(function() { window.location.href = redirect; }, 2500);
259 return;
260 }
261 // Fallback path for older responses.
262 const jobId = resp.data && (resp.data.jobId || resp.data.id);
263 if (!jobId) {
264 showError($p, '<?php echo esc_js(__('Server did not return a job ID.', 'allaccessible')); ?>');
265 $btn.prop('disabled', false).text(($btn.data('first-label') || '<?php echo esc_js(__('Start first scan', 'allaccessible')); ?>') + ' →');
266 return;
267 }
268 poll($p, nonce, jobId, redirect);
269 }).fail(function(xhr) {
270 // Transient unavailability while the server finishes
271 // setup: retry once after a short delay.
272 if (xhr.status === 503 && !$p.data('scan-retried')) {
273 $p.data('scan-retried', 1);
274 $btn.text('<?php echo esc_js(__('Finishing setup…', 'allaccessible')); ?>');
275 setTimeout(function() { start($p); }, 4000);
276 return;
277 }
278 $p.removeData('scan-retried');
279 let msg = '<?php echo esc_js(__('Network error', 'allaccessible')); ?>';
280 if (xhr.responseJSON && xhr.responseJSON.data) {
281 msg = (typeof xhr.responseJSON.data === 'string')
282 ? xhr.responseJSON.data
283 : (xhr.responseJSON.data.reason || JSON.stringify(xhr.responseJSON.data));
284 } else if (xhr.status === 502 || xhr.status === 504) {
285 msg = '<?php echo esc_js(__('Server timeout while queuing scan. Try again.', 'allaccessible')); ?>';
286 } else if (xhr.status) {
287 msg = '<?php echo esc_js(__('Request failed (HTTP %s)', 'allaccessible')); ?>'.replace('%s', xhr.status);
288 }
289 showError($p, msg);
290 $btn.prop('disabled', false).text(($btn.data('first-label') || '<?php echo esc_js(__('Start first scan', 'allaccessible')); ?>') + ' →');
291 });
292 }
293
294 function poll($p, nonce, jobId, redirect) {
295 $.post(ajaxurl, { action: 'aacb_scan_progress', _wpnonce: nonce, job_id: jobId })
296 .done(function(resp) {
297 if (!resp || !resp.success) { setTimeout(function() { poll($p, nonce, jobId, redirect); }, POLL_MS); return; }
298 const d = resp.data || {};
299 const done = Number(d.pages_done || d.pagesDone || 0);
300 const total = Number(d.total_pages || d.totalPages || 0);
301 const pct = total > 0 ? Math.min(100, Math.round((done / total) * 100)) : 5;
302 $p.find('.aacb-scan-done').text(done);
303 $p.find('.aacb-scan-total').text(total || '—');
304 $p.find('.aacb-scan-bar').css('width', pct + '%');
305 const status = (d.status || '').toLowerCase();
306 if (status === 'done' || status === 'complete' || status === 'partial') {
307 $p.find('.aacb-scan-pages-final').text(done);
308 $p.find('.aacb-scan-done-state').removeClass('aacx-hidden');
309 $p.find('.aacb-scan-progress').addClass('aacx-hidden');
310 const $b = $p.find('.aacb-scan-start');
311 $b.prop('disabled', false).text(($b.data('again-label') || '<?php echo esc_js(__('Run scan again', 'allaccessible')); ?>'));
312 if (redirect) setTimeout(function() { window.location.href = redirect; }, 1500);
313 return;
314 }
315 if (status === 'failed') {
316 showError($p, '<?php echo esc_js(__('Scan failed. Try again.', 'allaccessible')); ?>');
317 $p.find('.aacb-scan-progress').addClass('aacx-hidden');
318 $p.find('.aacb-scan-start').prop('disabled', false).text('<?php echo esc_js(__('Retry scan', 'allaccessible')); ?>');
319 return;
320 }
321 setTimeout(function() { poll($p, nonce, jobId, redirect); }, POLL_MS);
322 })
323 .fail(function() { setTimeout(function() { poll($p, nonce, jobId, redirect); }, POLL_MS); });
324 }
325
326 function showError($p, msg) {
327 $p.find('.aacb-scan-error-msg').text(typeof msg === 'string' ? msg : JSON.stringify(msg));
328 $p.find('.aacb-scan-error-state').removeClass('aacx-hidden');
329 $p.find('.aacb-scan-progress').addClass('aacx-hidden');
330 }
331
332 const SRC_LABELS = {
333 yoast: 'Yoast SEO', rankmath: 'Rank Math', aioseo: 'All in One SEO',
334 seopress: 'SEOPress', 'wp-core': 'WordPress', 'http-probe': '<?php echo esc_js(__('Detected', 'allaccessible')); ?>'
335 };
336 function esc(s) { return $('<div>').text(s == null ? '' : String(s)).html(); }
337
338 // Load detected sitemap candidates when Advanced opens.
339 function loadCandidates($p) {
340 const $body = $p.find('.aacb-advanced-sitemap-body');
341 if ($body.attr('data-loaded') === '1') return;
342 $body.attr('data-loaded', '1');
343 const rg = $body.attr('data-radio-group') || 'aacb-sitemap-choice';
344 const $box = $p.find('.aacb-sitemap-candidates');
345 $box.html('<span class="aacx-v2__help"><?php echo esc_js(__('Looking for sitemaps…', 'allaccessible')); ?></span>');
346
347 $.post(ajaxurl, { action: 'aacb_detect_sitemap', _wpnonce: $p.data('nonce') })
348 .done(function(resp) {
349 const list = (resp && resp.success && resp.data && resp.data.candidates) || [];
350 let html = '';
351 list.forEach(function(c) {
352 const url = c && c.url ? String(c.url) : '';
353 if (!url) return;
354 const label = SRC_LABELS[c.source] || '<?php echo esc_js(__('Detected', 'allaccessible')); ?>';
355 html += '<label class="aacb-sitemap-option" style="display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--aacx-border);border-radius:6px;margin-bottom:8px;cursor:pointer;">'
356 + '<input type="radio" name="' + esc(rg) + '" class="aacb-sitemap-radio" value="' + esc(url) + '">'
357 + '<span style="display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;">'
358 + '<span style="font-size:var(--aacx-text-sm);color:var(--aacx-text-strong);">' + esc(label) + '</span>'
359 + '<span style="font-family:var(--aacx-font-mono);font-size:var(--aacx-text-xs);color:var(--aacx-text-muted);word-break:break-all;">' + esc(url) + '</span>'
360 + '</span>'
361 + '<a href="' + esc(url) + '" target="_blank" rel="noopener" class="aacx-v2__btn aacx-v2__btn--ghost aacx-v2__btn--sm" style="flex-shrink:0;" onclick="event.stopPropagation();"><?php echo esc_js(__('Open & verify', 'allaccessible')); ?> ↗</a>'
362 + '</label>';
363 });
364 if (!list.length) {
365 html += '<p class="aacx-v2__help" style="margin-bottom:8px;"><?php echo esc_js(__('No reachable sitemap found. Enter one below, or close Advanced to auto-discover.', 'allaccessible')); ?></p>';
366 }
367 // Always offer a manual URL entry.
368 html += '<label class="aacb-sitemap-option" style="display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--aacx-border);border-radius:6px;margin-bottom:8px;cursor:pointer;">'
369 + '<input type="radio" name="' + esc(rg) + '" class="aacb-sitemap-radio" value="__custom__">'
370 + '<span style="font-size:var(--aacx-text-sm);"><?php echo esc_js(__('Enter my own sitemap URL', 'allaccessible')); ?></span>'
371 + '</label>'
372 + '<input type="url" class="aacb-scan-sitemap-input aacx-v2__input" placeholder="https://your-site.com/sitemap.xml" disabled>';
373 $box.html(html);
374 })
375 .fail(function() {
376 $box.html('<p class="aacx-v2__help"><?php echo esc_js(__('Could not check for sitemaps. Close Advanced to auto-discover instead.', 'allaccessible')); ?></p>');
377 $body.attr('data-loaded', '0'); // allow retry on next open
378 });
379 }
380
381 // `toggle` does not bubble, so bind directly (panel markup is
382 // already in the DOM by the time this footer script runs).
383 $(function() {
384 $('.aacb-advanced-sitemap').on('toggle', function() {
385 if (this.open) loadCandidates($(this).closest('.aacb-scan-trigger'));
386 });
387 });
388
389 $(document).on('click', '.aacb-scan-start', function(e) {
390 e.preventDefault();
391 start($(this).closest('.aacb-scan-trigger'));
392 });
393 $(document).on('change', '.aacb-sitemap-radio', function() {
394 const $p = $(this).closest('.aacb-scan-trigger');
395 const isCustom = $(this).val() === '__custom__';
396 const $input = $p.find('.aacb-scan-sitemap-input');
397 $input.prop('disabled', !isCustom);
398 if (isCustom) { $input.focus(); } else { $input.val(''); }
399 });
400 })(jQuery);
401 </script>
402 <?php
403 }
404 }
405