• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

equalizedigital / accessibility-checker / 24683125413

20 Apr 2026 06:22PM UTC coverage: 57.497% (-3.0%) from 60.51%
24683125413

push

github

web-flow
Merge pull request #1315 from equalizedigital/william/pro-286-update-the-free-plugin-to-be-able-to-handle-licence

Add support for registering sites with myDot and exchanging a jwt token that can be used to share site stats

440 of 1210 new or added lines in 9 files covered. (36.36%)

2 existing lines in 2 files now uncovered.

5426 of 9437 relevant lines covered (57.5%)

4.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.71
/admin/class-scans-stats.php
1
<?php
2
/**
3
 * Class file for scans stats
4
 *
5
 * @package Accessibility_Checker
6
 */
7

8
namespace EDAC\Admin;
9

10
use EqualizeDigital\AccessibilityCheckerPro\Admin\Scans;
11

12
/**
13
 * Class that handles calculating scans stats
14
 */
15
class Scans_Stats {
16

17
        /**
18
         * Number of seconds to return results from cache.
19
         *
20
         * @var integer
21
         */
22
        private $cache_time = 0;
23

24
        /**
25
         * Max number of issues to consider for results.
26
         *
27
         * @var integer
28
         */
29
        private $record_limit = 100000;
30

31
        /**
32
         * Total number of rules that are run on each page during a scan
33
         *
34
         * @var integer
35
         */
36
        private $rule_count;
37

38
        /**
39
         * Prefix used cache name
40
         *
41
         * @var string
42
         */
43
        private $cache_name_prefix;
44

45
        /**
46
         * Constructor
47
         *
48
         * @param integer $cache_time number of seconds to return the results from cache.
49
         */
50
        public function __construct( $cache_time = 60 * 60 * 24 ) {
51

52
                $this->cache_time        = $cache_time;
10✔
53
                $this->cache_name_prefix = 'edac_scans_stats_' . EDAC_VERSION . '_' . $this->record_limit;
10✔
54
                $this->rule_count        = count( edac_register_rules() );
10✔
55
        }
56

57
        /**
58
         * Load all stats into the cache. Should be called by a background scheduled.
59
         *
60
         * @return void
61
         */
62
        public function load_cache() {
63

64
                // Cache the summary.
65
                $this->summary();
×
66

67
                // Cache the post_types.
68
                $scannable_post_types = Settings::get_scannable_post_types();
×
69

70
                $post_types = get_post_types(
×
71
                        [
×
72
                                'public' => true,
×
73
                        ]
×
74
                );
×
75

76
                unset( $post_types['attachment'] );
×
77

78
                foreach ( $post_types as $post_type ) {
×
79
                        if ( in_array( $post_type, $scannable_post_types, true ) ) {
×
80
                                $this->issues_summary_by_post_type( $post_type );
×
81
                        }
82
                }
83
        }
84

85
        /**
86
         * Clear the summary and post type scans stats that have been cached
87
         *
88
         * @return void
89
         */
90
        public function clear_cache() {
91

92
                // Delete the cached summary stats.
93
                $transient_name = $this->cache_name_prefix . '_summary';
10✔
94
                delete_transient( $transient_name );
10✔
95

96
                // Delete the cached post_type stats.
97
                $post_types = get_post_types(
10✔
98
                        [
10✔
99
                                'public' => true,
10✔
100
                        ]
10✔
101
                );
10✔
102
                unset( $post_types['attachment'] );
10✔
103
                foreach ( $post_types as $post_type ) {
10✔
104
                        $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type;
10✔
105
                        delete_transient( $transient_name );
10✔
106
                }
107
        }
108

109
        /**
110
         * Gets summary information about all scans
111
         *
112
         * @param  boolean $skip_cache whether to skip the cache.
113
         * @return array
114
         */
115
        public function summary( $skip_cache = false ) {
116

117
                global $wpdb;
10✔
118

119
                $transient_name = $this->cache_name_prefix . '_summary';
10✔
120

121
                $cache = get_transient( $transient_name );
10✔
122

123
                if ( ! $skip_cache && ( $this->cache_time && $cache ) ) {
10✔
124

125
                        if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time()
×
126
                        ) {
127

128
                                $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' );
×
129
                                if ( $full_scan_completed_at <= $cache['cached_at'] ) {
×
130
                                        // There hasn't been a full scan completed since we've cached, so return these results.
131
                                        $cache['cache_hit'] = true;
×
132
                                        return $cache;
×
133
                                }
134
                        }
135
                }
136

137
                $data = [];
10✔
138

139
                $scannable_posts_count = Settings::get_scannable_posts_count();
10✔
140
                $tests_count           = $scannable_posts_count * $this->rule_count;
10✔
141
                $siteid                = get_current_blog_id();
10✔
142

143
                $data['scannable_posts_count']      = (int) $scannable_posts_count;
10✔
144
                $data['rule_count']                 = (int) $this->rule_count;
10✔
145
                $data['tests_count']                = (int) $tests_count;
10✔
146
                $data['scannable_post_types_count'] = (int) count( Settings::get_scannable_post_types( true ) );
10✔
147

148
                $post_types = get_post_types(
10✔
149
                        [
10✔
150
                                'public' => true,
10✔
151
                        ]
10✔
152
                );
10✔
153
                unset( $post_types['attachment'] );
10✔
154

155
                $data['public_post_types_count'] = (int) count( $post_types );
10✔
156

157
                $issues_query = new Issues_Query( [], $this->record_limit, Issues_Query::FLAG_INCLUDE_ALL_POST_TYPES );
10✔
158

159
                // Count total unique meta values from postmeta table where the meta_key is _edac_issue_density.
160
                // This will give us the total number of posts that have been scanned.
161
                $data['posts_scanned'] = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
10✔
162
                        $wpdb->prepare(
10✔
163
                                'SELECT COUNT(DISTINCT %i) FROM %i WHERE meta_key = %s',
10✔
164
                                'post_id',
10✔
165
                                $wpdb->postmeta,
10✔
166
                                '_edac_issue_density'
10✔
167
                        )
10✔
168
                );
10✔
169

170

171
                $data['is_truncated']      = $issues_query->has_truncated_results();
10✔
172
                $data['posts_with_issues'] = (int) $issues_query->distinct_posts_count();
10✔
173
                $data['rules_failed']      = 0;
10✔
174

175
                // Get a count of rules that are not in the issues table.
176
                $rule_slugs = array_map(
10✔
177
                        function ( $item ) {
10✔
178
                                return $item['slug'];
10✔
179
                        },
10✔
180
                        edac_register_rules()
10✔
181
                );
10✔
182

183
                foreach ( $rule_slugs as $rule_slug ) {
10✔
184
                        $rule_query = new Issues_Query(
10✔
185
                                [
10✔
186
                                        'rule_slugs' => [ $rule_slug ],
10✔
187
                                        'post_types' => Settings::get_scannable_post_types(),
10✔
188
                                ],
10✔
189
                                1
10✔
190
                        );
10✔
191

192
                        if ( $rule_query->count() ) {
10✔
193
                                        ++$data['rules_failed'];
2✔
194
                        }
195
                }
196
                $data['rules_passed'] = $this->rule_count - $data['rules_failed'];
10✔
197

198
                $data['passed_percentage'] = 'N/A';
10✔
199
                $scannable_post_types      = Settings::get_scannable_post_types();
10✔
200
                if ( ! empty( $scannable_post_types ) && $data['posts_scanned'] > 0 && $tests_count > 0 ) {
10✔
201
                        $data['passed_percentage'] = round( ( $data['rules_passed'] / $this->rule_count ) * 100, 2 );
×
202
                }
203

204
                $warning_issues_query      = new Issues_Query(
10✔
205
                        [
10✔
206
                                'post_types' => Settings::get_scannable_post_types(),
10✔
207
                                'rule_types' => [ Issues_Query::RULETYPE_WARNING ],
10✔
208
                        ],
10✔
209
                        $this->record_limit
10✔
210
                );
10✔
211
                $data['warnings']          = (int) $warning_issues_query->count();
10✔
212
                $data['distinct_warnings'] = (int) $warning_issues_query->distinct_count();
10✔
213

214
                $contrast_issues_query = new Issues_Query(
10✔
215
                        [
10✔
216
                                'post_types' => Settings::get_scannable_post_types(),
10✔
217
                                'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ],
10✔
218
                        ],
10✔
219
                        $this->record_limit
10✔
220
                );
10✔
221

222
                $data['contrast_errors']          = (int) $contrast_issues_query->count();
10✔
223
                $data['distinct_contrast_errors'] = (int) $contrast_issues_query->distinct_count();
10✔
224

225
                $error_issues_query      = new Issues_Query(
10✔
226
                        [
10✔
227
                                'post_types' => Settings::get_scannable_post_types(),
10✔
228
                                'rule_types' => [ Issues_Query::RULETYPE_ERROR ],
10✔
229
                        ],
10✔
230
                        $this->record_limit
10✔
231
                );
10✔
232
                $data['errors']          = (int) $error_issues_query->count();
10✔
233
                $data['distinct_errors'] = (int) $error_issues_query->distinct_count();
10✔
234

235
                $data['errors_without_contrast']          = $data['errors'] - $data['contrast_errors'];
10✔
236
                $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors'];
10✔
237

238
                $ignored_issues_query         = new Issues_Query(
10✔
239
                        [
10✔
240
                                'post_types' => Settings::get_scannable_post_types(),
10✔
241
                        ],
10✔
242
                        $this->record_limit,
10✔
243
                        Issues_Query::FLAG_ONLY_IGNORED
10✔
244
                );
10✔
245
                $data['ignored']              = (int) $ignored_issues_query->count();
10✔
246
                $data['distinct_ignored']     = (int) $ignored_issues_query->distinct_count();
10✔
247
                $data['posts_without_issues'] = 0;
10✔
248
                $data['avg_issues_per_post']  = 0;
10✔
249

250
                if ( $data['posts_scanned'] > 0
10✔
251
                        && ! empty( Settings::get_scannable_post_types() )
10✔
252
                        && ! empty( Settings::get_scannable_post_statuses() )
10✔
253
                ) {
254
                        $ac_table_name = $wpdb->prefix . 'accessibility_checker';
×
255

256
                        $posts_without_issues = "
×
257
                                SELECT COUNT({$wpdb->posts}.ID) FROM {$wpdb->posts}
×
258
                                LEFT JOIN " . $wpdb->prefix . "accessibility_checker ON {$wpdb->posts}.ID = " .
×
259
                                $wpdb->prefix . 'accessibility_checker.postid WHERE ' .
×
260
                                $wpdb->prefix . 'accessibility_checker.postid IS NULL
×
261
                                AND post_type IN(' .
×
262
                                        Helpers::array_to_sql_safe_list(
×
263
                                                Settings::get_scannable_post_types()
×
264
                                        ) . ')
×
265
                                AND post_status IN(' .
×
266
                                        Helpers::array_to_sql_safe_list(
×
267
                                                Settings::get_scannable_post_statuses()
×
268
                                        ) . ')';
×
269

270
                        // give me sql that will find all post ids in the accessibility_checker table
271
                        // where ALL issues with that ID are eiter ignored or globally ignored.
272
                        $posts_with_just_ignored_issues = $wpdb->prepare(
×
273
                                'SELECT COUNT( DISTINCT postid )
×
274
                                FROM %i
275
                                WHERE siteid = %d
276
                                AND postid NOT IN (
277
                                          SELECT postid
278
                                          FROM %i
279
                                          WHERE ignre=0 AND ignre_global=0
280
                                )',
×
281
                                [ $ac_table_name, $siteid, $ac_table_name ]
×
282
                        );
×
283

284
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
285
                        $data['posts_without_issues'] = $wpdb->get_var( $posts_without_issues ) + $wpdb->get_var( $posts_with_just_ignored_issues );
×
286
                        $data['avg_issues_per_post']  = round( ( $data['warnings'] + $data['errors'] ) / $data['posts_scanned'], 2 );
×
287

288
                        // Show "< 1" only when there are issues but the average is less than 1 (excluding exactly 0).
289
                        if ( $data['avg_issues_per_post'] < 1 && $data['avg_issues_per_post'] > 0 ) {
×
290
                                $data['avg_issues_per_post'] = '< 1';
×
291
                        }
292
                }
293

294
                // Average issue density percentage is the sum of all post densities divided by the number of posts scanned.
295
                $data['avg_issue_density_percentage'] =
10✔
296
                        $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
10✔
297
                                $wpdb->prepare(
10✔
298
                                        'SELECT AVG(meta_value)
10✔
299
                                        FROM ' . $wpdb->postmeta . '
10✔
300
                                        JOIN (
301
                                                SELECT DISTINCT postid
302
                                                FROM ' . $wpdb->prefix . 'accessibility_checker
10✔
303
                                                WHERE ignre=%d AND ignre_global=%d
304
                                        ) AS distinct_posts ON ' . $wpdb->postmeta . '.post_id = distinct_posts.postid
10✔
305
                                        WHERE meta_key = %s',
10✔
306
                                        [ 0, 0, '_edac_issue_density' ]
10✔
307
                                )
10✔
308
                        );
10✔
309

310
                if ( null === $data['avg_issue_density_percentage'] ) {
10✔
311
                        $data['avg_issue_density_percentage'] = 'N/A';
10✔
312
                } else {
313
                        $data['avg_issue_density_percentage'] = round( $data['avg_issue_density_percentage'], 2 );
×
314
                }
315

316
                $data['fullscan_running']      = false;
10✔
317
                $data['fullscan_state']        = '';
10✔
318
                $data['fullscan_completed_at'] = 0;
10✔
319

320
                // For back compat reasons the old class_exists is kept and moved to an else block.
321
                // After a few releases the else should be removed.
322
                if ( class_exists( '\EqualizeDigital\AccessibilityCheckerPro\Admin\Scans' ) ) {
10✔
323
                        $scans      = new Scans();
×
324
                        $scan_state = $scans->scan_state();
×
325

326
                        $data['fullscan_state'] = $scan_state;
×
327
                        if ( Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state
×
328
                                || Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state
×
329
                        ) {
330
                                $data['fullscan_running'] = true;
×
331
                        }
332
                        $data['fullscan_completed_at'] = $scans->scan_date( 'php' );
×
333
                } elseif ( class_exists( '\EDACP\Scans' ) ) {
10✔
334
                        $scans      = new \EDACP\Scans();
×
335
                        $scan_state = $scans->scan_state();
×
336

337
                        $data['fullscan_state'] = $scan_state;
×
338
                        if ( \EDACP\Scans::SCAN_STATE_PHP_SCAN_RUNNING === $scan_state
×
339
                                || \EDACP\Scans::SCAN_STATE_JS_SCAN_RUNNING === $scan_state
×
340
                        ) {
341
                                $data['fullscan_running'] = true;
×
342
                        }
343
                        $data['fullscan_completed_at'] = $scans->scan_date( 'php' );
×
344

345
                }
346

347
                // Get top 5 posts with issues.
348
                $data['top_pages_with_issues']    = $this->get_top_pages_with_issues( 5 );
10✔
349
                $data['top_issues_found_on_site'] = $this->get_top_issues_found_on_site( 10 );
10✔
350

351
                $data['cache_id']   = $transient_name;
10✔
352
                $data['cached_at']  = time();
10✔
353
                $data['expires_at'] = time() + $this->cache_time;
10✔
354
                $data['cache_hit']  = false;
10✔
355

356
                // Handle formatting. Assumes all are numbers except for those listed in exceptions.
357
                $formatting            = [];
10✔
358
                $formatting_exceptions = [
10✔
359
                        'is_truncated',
10✔
360
                        'passed_percentage',
10✔
361
                        'avg_issue_density_percentage',
10✔
362
                        'fullscan_running',
10✔
363
                        'fullscan_state',
10✔
364
                        'fullscan_completed_at',
10✔
365
                        'cache_id',
10✔
366
                        'cached_at',
10✔
367
                        'expires_at',
10✔
368
                        'cache_hit',
10✔
369
                        'top_pages_with_issues',
10✔
370
                ];
10✔
371

372
                foreach ( $data as $key => $value ) {
10✔
373
                        if ( ! in_array( $key, $formatting_exceptions, true ) ) {
10✔
374
                                $formatting[ $key . '_formatted' ] = Helpers::format_number( $value );
10✔
375
                        }
376
                }
377

378
                // Handle exceptions.
379
                $formatting['fullscan_completed_at_formatted'] = 'N/A';
10✔
380
                if ( $data['fullscan_completed_at'] > 0 ) {
10✔
381
                        $formatting['fullscan_completed_at_formatted'] = Helpers::format_date( $data['fullscan_completed_at'], true );
×
382
                }
383
                $formatting['passed_percentage_formatted'] = Helpers::format_percentage( $data['passed_percentage'] );
10✔
384

385
                // The density should already a percentage value, just add the sign. If not an integer, just return the value which will be 'N/A'.
386
                $formatting['avg_issue_density_percentage_formatted'] = is_int( $data['avg_issue_density_percentage'] ) ? $data['avg_issue_density_percentage'] . '%' : $data['avg_issue_density_percentage'];
10✔
387

388
                $formatting['cached_at_formatted'] = Helpers::format_date( $data['cached_at'], true );
10✔
389

390
                $data = array_merge( $data, $formatting );
10✔
391

392
                if ( $data['posts_scanned'] > 0 ) {
10✔
393
                        set_transient( $transient_name, $data, $this->cache_time );
×
394
                } else {
395
                        // no posts have been scanned, so clear any previously cache results.
396
                        $this->clear_cache();
10✔
397
                }
398

399
                return $data;
10✔
400
        }
401

402
        /**
403
         * Gets issues summary information about a post type
404
         *
405
         * @param  string $post_type post type.
406
         * @return array .
407
         */
408
        public function issues_summary_by_post_type( $post_type ) {
409

410
                $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type;
×
411

412
                $cache = get_transient( $transient_name );
×
413

414
                if ( $this->cache_time && $cache ) {
×
415

416
                        if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time()
×
417
                        ) {
418

419
                                $full_scan_completed_at = (int) get_option( 'edacp_fullscan_completed_at' );
×
420
                                if ( $full_scan_completed_at <= $cache['cached_at'] ) {
×
421
                                        // There hasn't been a full scan completed since we've cached, so return these results.
422
                                        $cache['cache_hit'] = true;
×
423
                                        return $cache;
×
424
                                }
425
                        }
426
                }
427

428
                $data = [];
×
429

430
                $error_issues_query      = new Issues_Query(
×
431
                        [
×
432
                                'rule_types' => [ Issues_Query::RULETYPE_ERROR ],
×
433
                                'post_types' => [ $post_type ],
×
434
                        ],
×
435
                        $this->record_limit
×
436
                );
×
437
                $data['errors']          = $error_issues_query->count();
×
438
                $data['distinct_errors'] = $error_issues_query->distinct_count();
×
439

440
                $warning_issues_query      = new Issues_Query(
×
441
                        [
×
442
                                'rule_types' => [ Issues_Query::RULETYPE_WARNING ],
×
443
                                'post_types' => [ $post_type ],
×
444
                        ],
×
445
                        $this->record_limit
×
446
                );
×
447
                $data['warnings']          = $warning_issues_query->count();
×
448
                $data['distinct_warnings'] = $warning_issues_query->distinct_count();
×
449

450
                $color_contrast_issues_query      = new Issues_Query(
×
451
                        [
×
452
                                'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ],
×
453
                                'post_types' => [ $post_type ],
×
454
                        ],
×
455
                        $this->record_limit
×
456
                );
×
457
                $data['contrast_errors']          = $color_contrast_issues_query->count();
×
458
                $data['distinct_contrast_errors'] = $color_contrast_issues_query->distinct_count();
×
459

460
                $data['errors_without_contrast']          = $data['errors'] - $data['contrast_errors'];
×
461
                $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors'];
×
462

463
                foreach ( $data as $key => $val ) {
×
464
                        $data[ $key . '_formatted' ] = Helpers::format_number( $val );
×
465
                }
466

467
                $data['cache_id']   = $transient_name;
×
468
                $data['cached_at']  = time();
×
469
                $data['expires_at'] = time() + $this->cache_time;
×
470
                $data['cache_hit']  = false;
×
471

472
                set_transient( $transient_name, $data, $this->cache_time );
×
473

474
                return $data;
×
475
        }
476

477
        /**
478
         * Get top N posts with the most issues.
479
         *
480
         * @param int $limit Number of posts to return (default 5).
481
         * @return array Array of arrays with post_title, post_url, and issue_count.
482
         */
483
        private function get_top_pages_with_issues( int $limit = 5 ) {
484
                global $wpdb;
10✔
485

486
                $limit = max( 0, $limit );
10✔
487
                if ( 0 === $limit ) {
10✔
NEW
488
                        return [];
×
489
                }
490

491
                $ac_table_name = $wpdb->prefix . 'accessibility_checker';
10✔
492
                $siteid        = get_current_blog_id();
10✔
493

494
                // Get scannable post types and statuses to match site scope.
495
                $post_types    = Settings::get_scannable_post_types();
10✔
496
                $post_statuses = Settings::get_scannable_post_statuses();
10✔
497

498
                // Return empty array if no scannable content is configured.
499
                if ( empty( $post_types ) || empty( $post_statuses ) ) {
10✔
NEW
500
                        return [];
×
501
                }
502

503
                // Build SQL-safe lists for IN clauses (these are sanitized by the helper).
504
                $post_types_list    = Helpers::array_to_sql_safe_list( $post_types );
10✔
505
                $post_statuses_list = Helpers::array_to_sql_safe_list( $post_statuses );
10✔
506

507
                // Build the SQL query with siteid and post filters.
508
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- post_types_list and post_statuses_list are sanitized by Helpers::array_to_sql_safe_list().
509
                $sql = $wpdb->prepare(
10✔
510
                        "SELECT %i.ID, %i.post_title, COUNT(%i.id) as issue_count
10✔
511
                        FROM %i
512
                        INNER JOIN %i ON %i.ID = %i.postid
513
                        WHERE %i.siteid = %d
514
                        AND %i.ignre = 0
515
                        AND %i.ignre_global = 0
516
                        AND %i.post_type IN({$post_types_list})
10✔
517
                        AND %i.post_status IN({$post_statuses_list})
10✔
518
                        GROUP BY %i.ID
519
                        ORDER BY issue_count DESC
520
                        LIMIT %d",
10✔
521
                        $wpdb->posts,
10✔
522
                        $wpdb->posts,
10✔
523
                        $ac_table_name,
10✔
524
                        $wpdb->posts,
10✔
525
                        $ac_table_name,
10✔
526
                        $wpdb->posts,
10✔
527
                        $ac_table_name,
10✔
528
                        $ac_table_name,
10✔
529
                        $siteid,
10✔
530
                        $ac_table_name,
10✔
531
                        $ac_table_name,
10✔
532
                        $wpdb->posts,
10✔
533
                        $wpdb->posts,
10✔
534
                        $wpdb->posts,
10✔
535
                        $limit
10✔
536
                );
10✔
537
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
538

539
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Direct query for stats calculation, SQL is prepared above.
540
                $posts = $wpdb->get_results( $sql );
10✔
541

542
                $result = [];
10✔
543
                if ( $posts ) {
10✔
544
                        foreach ( $posts as $post ) {
2✔
545
                                $result[] = [
2✔
546
                                        'post_title'  => esc_html( $post->post_title ),
2✔
547
                                        'post_url'    => esc_url( get_permalink( $post->ID ) ),
2✔
548
                                        'issue_count' => (int) $post->issue_count,
2✔
549
                                ];
2✔
550
                        }
551
                }
552

553
                return $result;
10✔
554
        }
555

556
        /**
557
         * Get top N issues found on the site.
558
         *
559
         * @param int $limit Number of issues to return (default 10).
560
         * @return array Array of arrays with rule_slug and issue_count.
561
         */
562
        private function get_top_issues_found_on_site( int $limit = 10 ) {
563
                global $wpdb;
10✔
564

565
                $limit = max( 0, $limit );
10✔
566
                if ( 0 === $limit ) {
10✔
NEW
567
                        return [];
×
568
                }
569

570
                $ac_table_name = $wpdb->prefix . 'accessibility_checker';
10✔
571
                $siteid        = get_current_blog_id();
10✔
572

573
                $rules_raw     = edac_register_rules();
10✔
574
                $rules_parsed  = [];
10✔
575
                $severity_case = '0';
10✔
576

577
                foreach ( $rules_raw as $rule ) {
10✔
578
                        if ( ! isset( $rule['slug'] ) ) {
10✔
NEW
579
                                continue;
×
580
                        }
581

582
                        $slug                  = (string) $rule['slug'];
10✔
583
                        $rules_parsed[ $slug ] = $rule;
10✔
584
                        $severity              = isset( $rule['severity'] ) ? (int) $rule['severity'] : 0;
10✔
585
                        $severity_case        .= $wpdb->prepare( ' + (CASE WHEN rule = %s THEN %d ELSE 0 END)', $slug, $severity );
10✔
586
                }
587

588
                // Build the SQL query to get top issues by severity, then count.
589
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- No user input, and table name is properly escaped, severity cases is prepared above.
590
                $sql = $wpdb->prepare(
10✔
591
                        'SELECT rule, COUNT(id) AS issue_count, (' . $severity_case . ') AS severity
10✔
592
                        FROM %i
593
                        WHERE siteid = %d
594
                        AND ignre = 0
595
                        AND ignre_global = 0
596
                        GROUP BY rule
597
                        ORDER BY severity DESC, issue_count DESC, rule ASC
598
                        LIMIT %d',
10✔
599
                        $ac_table_name,
10✔
600
                        $siteid,
10✔
601
                        $limit
10✔
602
                );
10✔
603
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
604

605
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Direct query for stats calculation, SQL is prepared above.
606
                $issues = $wpdb->get_results( $sql );
10✔
607

608
                if ( ! $issues ) {
10✔
609
                        return [];
10✔
610
                }
611

612
                return array_map(
2✔
613
                        function ( $issue ) use ( $rules_parsed ) {
2✔
614
                                $rule = $rules_parsed[ $issue->rule ] ?? [];
2✔
615

616
                                return [
2✔
617
                                        'rule_nicename' => sanitize_text_field( $rule['title'] ?? $issue->rule ),
2✔
618
                                        'rule_slug'     => sanitize_text_field( $rule['slug'] ?? $issue->rule ),
2✔
619
                                        'issue_count'   => (int) $issue->issue_count,
2✔
620
                                        'severity'      => isset( $rule['severity'] ) ? (int) $rule['severity'] : (int) $issue->severity,
2✔
621
                                ];
2✔
622
                        },
2✔
623
                        $issues
2✔
624
                );
2✔
625
        }
626
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc