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

equalizedigital / accessibility-checker / 20276292312

16 Dec 2025 05:05PM UTC coverage: 57.469% (+0.005%) from 57.464%
20276292312

push

github

web-flow
Merge pull request #1291 from equalizedigital/steve/no-issue/passed-test-percentage-logic

Updated: passed test percentage logic to include scannable post types and handle cases with no posts scanned.

5 of 5 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

4174 of 7263 relevant lines covered (57.47%)

3.43 hits per line

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

60.08
/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✔
UNCOV
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
                $data['cache_id']   = $transient_name;
10✔
348
                $data['cached_at']  = time();
10✔
349
                $data['expires_at'] = time() + $this->cache_time;
10✔
350
                $data['cache_hit']  = false;
10✔
351

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

367
                foreach ( $data as $key => $value ) {
10✔
368
                        if ( ! in_array( $key, $formatting_exceptions, true ) ) {
10✔
369
                                $formatting[ $key . '_formatted' ] = Helpers::format_number( $value );
10✔
370
                        }
371
                }
372

373
                // Handle exceptions.
374
                $formatting['fullscan_completed_at_formatted'] = 'N/A';
10✔
375
                if ( $data['fullscan_completed_at'] > 0 ) {
10✔
376
                        $formatting['fullscan_completed_at_formatted'] = Helpers::format_date( $data['fullscan_completed_at'], true );
×
377
                }
378
                $formatting['passed_percentage_formatted'] = Helpers::format_percentage( $data['passed_percentage'] );
10✔
379

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

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

385
                $data = array_merge( $data, $formatting );
10✔
386

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

394
                return $data;
10✔
395
        }
396

397
        /**
398
         * Gets issues summary information about a post type
399
         *
400
         * @param  string $post_type post type.
401
         * @return array .
402
         */
403
        public function issues_summary_by_post_type( $post_type ) {
404

405
                $transient_name = $this->cache_name_prefix . '_issues_summary_by_post_type_' . $post_type;
×
406

407
                $cache = get_transient( $transient_name );
×
408

409
                if ( $this->cache_time && $cache ) {
×
410

411
                        if ( $cache['expires_at'] >= time() && $cache['cached_at'] + $this->cache_time >= time()
×
412
                        ) {
413

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

423
                $data = [];
×
424

425
                $error_issues_query      = new Issues_Query(
×
426
                        [
×
427
                                'rule_types' => [ Issues_Query::RULETYPE_ERROR ],
×
428
                                'post_types' => [ $post_type ],
×
429
                        ],
×
430
                        $this->record_limit
×
431
                );
×
432
                $data['errors']          = $error_issues_query->count();
×
433
                $data['distinct_errors'] = $error_issues_query->distinct_count();
×
434

435
                $warning_issues_query      = new Issues_Query(
×
436
                        [
×
437
                                'rule_types' => [ Issues_Query::RULETYPE_WARNING ],
×
438
                                'post_types' => [ $post_type ],
×
439
                        ],
×
440
                        $this->record_limit
×
441
                );
×
442
                $data['warnings']          = $warning_issues_query->count();
×
443
                $data['distinct_warnings'] = $warning_issues_query->distinct_count();
×
444

445
                $color_contrast_issues_query      = new Issues_Query(
×
446
                        [
×
447
                                'rule_types' => [ Issues_Query::RULETYPE_COLOR_CONTRAST ],
×
448
                                'post_types' => [ $post_type ],
×
449
                        ],
×
450
                        $this->record_limit
×
451
                );
×
452
                $data['contrast_errors']          = $color_contrast_issues_query->count();
×
453
                $data['distinct_contrast_errors'] = $color_contrast_issues_query->distinct_count();
×
454

455
                $data['errors_without_contrast']          = $data['errors'] - $data['contrast_errors'];
×
456
                $data['distinct_errors_without_contrast'] = $data['distinct_errors'] - $data['distinct_contrast_errors'];
×
457

458
                foreach ( $data as $key => $val ) {
×
459
                        $data[ $key . '_formatted' ] = Helpers::format_number( $val );
×
460
                }
461

462
                $data['cache_id']   = $transient_name;
×
463
                $data['cached_at']  = time();
×
464
                $data['expires_at'] = time() + $this->cache_time;
×
465
                $data['cache_hit']  = false;
×
466

467
                set_transient( $transient_name, $data, $this->cache_time );
×
468

469
                return $data;
×
470
        }
471
}
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