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

equalizedigital / accessibility-checker / 22634711490

03 Mar 2026 05:22PM UTC coverage: 59.97% (-0.7%) from 60.629%
22634711490

push

github

web-flow
Merge pull request #1332 from equalizedigital/william/pro-526-setup-integration-branch-for-ac-sidebar

Integration branch: Sidebar Metabox

385 of 814 new or added lines in 10 files covered. (47.3%)

10 existing lines in 3 files now uncovered.

4773 of 7959 relevant lines covered (59.97%)

4.7 hits per line

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

59.52
/includes/classes/class-rest-api.php
1
<?php
2
/**
3
 * Class file for REST api
4
 *
5
 * @package Accessibility_Checker
6
 */
7

8
namespace EDAC\Inc;
9

10
use EDAC\Admin\Insert_Rule_Data;
11
use EDAC\Admin\Scans_Stats;
12
use EDAC\Admin\Settings;
13
use EDAC\Admin\Purge_Post_Data;
14

15
if ( ! defined( 'ABSPATH' ) ) {
16
        exit;
17
}
18

19
/**
20
 * Class that initializes and handles the REST api
21
 */
22
class REST_Api {
23

24

25
        /**
26
         * Constructor
27
         */
28
        public function __construct() {
29
        }
6✔
30

31

32
        /**
33
         * Add the class the hooks.
34
         */
35
        public function init_hooks() {
36
                add_action( 'init', [ $this, 'init_rest_routes' ] );
×
37
                add_filter( 'edac_filter_js_violation_html', [ $this, 'filter_js_validation_html' ], 10, 3 );
×
38
        }
39

40

41
        /**
42
         * Add the rest routes.
43
         *
44
         * @return void
45
         */
46
        public function init_rest_routes() {
47

48
                $ns      = 'accessibility-checker/';
20✔
49
                $version = 'v1';
20✔
50

51
                add_action(
20✔
52
                        'rest_api_init',
20✔
53
                        function () use ( $ns, $version ) {
20✔
54
                                register_rest_route(
20✔
55
                                        $ns . $version,
20✔
56
                                        '/test',
20✔
57
                                        [
20✔
58
                                                'methods'             => [ 'GET', 'POST' ],
20✔
59
                                                'callback'            => function () {
20✔
60
                                                        $messages          = [];
×
61
                                                        $messages['time']  = time();
×
62
                                                        $messages['perms'] = current_user_can( 'edit_posts' );
×
63

64
                                                        return new \WP_REST_Response( [ 'messages' => $messages ], 200 );
×
65
                                                },
20✔
66
                                                'permission_callback' => function () {
20✔
67
                                                        return current_user_can( 'edit_posts' );
×
68
                                                },
20✔
69
                                        ]
20✔
70
                                );
20✔
71
                        }
20✔
72
                );
20✔
73

74
                add_action(
20✔
75
                        'rest_api_init',
20✔
76
                        function () use ( $ns, $version ) {
20✔
77
                                register_rest_route(
20✔
78
                                        $ns . $version,
20✔
79
                                        '/post-scan-results/(?P<id>\d+)',
20✔
80
                                        [
20✔
81
                                                'methods'             => 'POST',
20✔
82
                                                'callback'            => [ $this, 'set_post_scan_results' ],
20✔
83
                                                'args'                => [
20✔
84
                                                        'id' => [
20✔
85
                                                                'required'          => true,
20✔
86
                                                                'validate_callback' => function ( $param ) {
20✔
87
                                                                        return is_numeric( $param );
4✔
88
                                                                },
20✔
89
                                                                'sanitize_callback' => 'absint',
20✔
90
                                                        ],
20✔
91
                                                ],
20✔
92
                                                'permission_callback' => function ( $request ) {
20✔
93
                                                        $post_id = (int) $request['id'];
4✔
94
                                                        return current_user_can( 'edit_post', $post_id ); // able to edit the post.
4✔
95
                                                },
20✔
96
                                        ]
20✔
97
                                );
20✔
98
                        }
20✔
99
                );
20✔
100

101
                add_action(
20✔
102
                        'rest_api_init',
20✔
103
                        function () use ( $ns, $version ) {
20✔
104
                                register_rest_route(
20✔
105
                                        $ns . $version,
20✔
106
                                        '/scans-stats',
20✔
107
                                        [
20✔
108
                                                'methods'             => 'GET',
20✔
109
                                                'callback'            => [ $this, 'get_scans_stats' ],
20✔
110
                                                'permission_callback' => function () {
20✔
111
                                                        return current_user_can( 'edit_posts' );
×
112
                                                },
20✔
113
                                        ]
20✔
114
                                );
20✔
115
                        }
20✔
116
                );
20✔
117

118
                add_action(
20✔
119
                        'rest_api_init',
20✔
120
                        function () use ( $ns, $version ) {
20✔
121
                                register_rest_route(
20✔
122
                                        $ns . $version,
20✔
123
                                        '/clear-cached-scans-stats',
20✔
124
                                        [
20✔
125
                                                'methods'             => 'POST',
20✔
126
                                                'callback'            => [ $this, 'clear_cached_scans_stats' ],
20✔
127
                                                'permission_callback' => function () {
20✔
128
                                                        return current_user_can( 'publish_posts' );
×
129
                                                },
20✔
130
                                        ]
20✔
131
                                );
20✔
132
                        }
20✔
133
                );
20✔
134

135
                add_action(
20✔
136
                        'rest_api_init',
20✔
137
                        function () use ( $ns, $version ) {
20✔
138
                                register_rest_route(
20✔
139
                                        $ns . $version,
20✔
140
                                        '/scans-stats-by-post-type/(?P<slug>[a-zA-Z0-9_-]+)',
20✔
141
                                        [
20✔
142
                                                'methods'             => 'GET',
20✔
143
                                                'callback'            => [ $this, 'get_scans_stats_by_post_type' ],
20✔
144
                                                'permission_callback' => function () {
20✔
145
                                                        return current_user_can( 'edit_posts' );
×
146
                                                },
20✔
147
                                        ]
20✔
148
                                );
20✔
149
                        }
20✔
150
                );
20✔
151

152
                add_action(
20✔
153
                        'rest_api_init',
20✔
154
                        function () use ( $ns, $version ) {
20✔
155
                                register_rest_route(
20✔
156
                                        $ns . $version,
20✔
157
                                        '/scans-stats-by-post-types',
20✔
158
                                        [
20✔
159
                                                'methods'             => 'GET',
20✔
160
                                                'callback'            => [ $this, 'get_scans_stats_by_post_types' ],
20✔
161
                                                'permission_callback' => function () {
20✔
162
                                                        return current_user_can( 'edit_posts' );
×
163
                                                },
20✔
164
                                        ]
20✔
165
                                );
20✔
166
                        }
20✔
167
                );
20✔
168

169
                add_action(
20✔
170
                        'rest_api_init',
20✔
171
                        function () use ( $ns, $version ) {
20✔
172
                                register_rest_route(
20✔
173
                                        $ns . $version,
20✔
174
                                        '/clear-issues/(?P<id>\d+)',
20✔
175
                                        [
20✔
176
                                                'methods'             => 'POST',
20✔
177
                                                'callback'            => [ $this, 'clear_issues_for_post' ],
20✔
178
                                                'args'                => [
20✔
179
                                                        'id' => [
20✔
180
                                                                'required'          => true,
20✔
181
                                                                'validate_callback' => function ( $param ) {
20✔
182
                                                                        return is_numeric( $param );
4✔
183
                                                                },
20✔
184
                                                                'sanitize_callback' => 'absint',
20✔
185
                                                        ],
20✔
186
                                                ],
20✔
187
                                                'permission_callback' => function ( $request ) {
20✔
188
                                                        $post_id = (int) $request['id'];
4✔
189
                                                        return current_user_can( 'edit_post', $post_id ); // able to edit the post.
4✔
190
                                                },
20✔
191
                                        ]
20✔
192
                                );
20✔
193
                        }
20✔
194
                );
20✔
195

196
                // Exposes the scan summary data.
197
                add_action(
20✔
198
                        'rest_api_init',
20✔
199
                        function () use ( $ns, $version ) {
20✔
200
                                register_rest_route(
20✔
201
                                        $ns . $version,
20✔
202
                                        '/site-summary',
20✔
203
                                        [
20✔
204
                                                'methods'             => 'GET',
20✔
205
                                                'callback'            => [ $this, 'get_site_summary' ],
20✔
206
                                                'permission_callback' => function () {
20✔
207
                                                        return current_user_can( 'edit_posts' );
×
208
                                                },
20✔
209
                                        ]
20✔
210
                                );
20✔
211
                        }
20✔
212
                );
20✔
213

214
                // Sidebar data endpoint - returns all metabox data in one call.
215
                add_action(
20✔
216
                        'rest_api_init',
20✔
217
                        function () use ( $ns, $version ) {
20✔
218
                                register_rest_route(
20✔
219
                                        $ns . $version,
20✔
220
                                        '/sidebar-data/(?P<id>\d+)',
20✔
221
                                        [
20✔
222
                                                'methods'             => 'GET',
20✔
223
                                                'callback'            => [ $this, 'get_sidebar_data' ],
20✔
224
                                                'args'                => [
20✔
225
                                                        'id' => [
20✔
226
                                                                'required'          => true,
20✔
227
                                                                'validate_callback' => function ( $param ) {
20✔
228
                                                                        return is_numeric( $param );
8✔
229
                                                                },
20✔
230
                                                                'sanitize_callback' => 'absint',
20✔
231
                                                        ],
20✔
232
                                                ],
20✔
233
                                                'permission_callback' => function ( $request ) {
20✔
234
                                                        $post_id = (int) $request['id'];
8✔
235
                                                        return current_user_can( 'edit_post', $post_id );
8✔
236
                                                },
20✔
237
                                        ]
20✔
238
                                );
20✔
239
                        }
20✔
240
                );
20✔
241

242
                // Simplified summary endpoint - saves the simplified summary text.
243
                add_action(
20✔
244
                        'rest_api_init',
20✔
245
                        function () use ( $ns, $version ) {
20✔
246
                                register_rest_route(
20✔
247
                                        $ns . $version,
20✔
248
                                        '/simplified-summary/(?P<id>\d+)',
20✔
249
                                        [
20✔
250
                                                'methods'             => 'POST',
20✔
251
                                                'callback'            => [ $this, 'save_simplified_summary' ],
20✔
252
                                                'args'                => [
20✔
253
                                                        'id'      => [
20✔
254
                                                                'required'          => true,
20✔
255
                                                                'validate_callback' => function ( $param ) {
20✔
NEW
256
                                                                        return is_numeric( $param );
×
257
                                                                },
20✔
258
                                                                'sanitize_callback' => 'absint',
20✔
259
                                                        ],
20✔
260
                                                        'summary' => [
20✔
261
                                                                'required'          => true,
20✔
262
                                                                'sanitize_callback' => 'sanitize_textarea_field',
20✔
263
                                                                'validate_callback' => function ( $param ) {
20✔
NEW
264
                                                                        return is_string( $param );
×
265
                                                                },
20✔
266
                                                        ],
20✔
267
                                                ],
20✔
268
                                                'permission_callback' => function ( $request ) {
20✔
NEW
269
                                                        $post_id = (int) $request['id'];
×
NEW
270
                                                        return current_user_can( 'edit_post', $post_id );
×
271
                                                },
20✔
272
                                        ]
20✔
273
                                );
20✔
274
                        }
20✔
275
                );
20✔
276

277
                // Dismiss/restore issue endpoint.
278
                add_action(
20✔
279
                        'rest_api_init',
20✔
280
                        function () use ( $ns, $version ) {
20✔
281
                                register_rest_route(
20✔
282
                                        $ns . $version,
20✔
283
                                        '/dismiss-issue/(?P<issue_id>\d+)',
20✔
284
                                        [
20✔
285
                                                'methods'             => 'POST',
20✔
286
                                                'callback'            => [ $this, 'dismiss_issue' ],
20✔
287
                                                'args'                => [
20✔
288
                                                        'issue_id'      => [
20✔
289
                                                                'required'          => true,
20✔
290
                                                                'validate_callback' => function ( $param ) {
20✔
NEW
291
                                                                        return is_numeric( $param );
×
292
                                                                },
20✔
293
                                                                'sanitize_callback' => 'absint',
20✔
294
                                                        ],
20✔
295
                                                        'action'        => [
20✔
296
                                                                'required'          => true,
20✔
297
                                                                'validate_callback' => function ( $param ) {
20✔
NEW
298
                                                                        return in_array( $param, [ 'enable', 'disable', 'dismiss', 'undismiss', 'ignore', 'unignore' ], true );
×
299
                                                                },
20✔
300
                                                                'sanitize_callback' => 'sanitize_text_field',
20✔
301
                                                        ],
20✔
302
                                                        'reason'        => [
20✔
303
                                                                'required'          => false,
20✔
304
                                                                'sanitize_callback' => 'sanitize_text_field',
20✔
305
                                                        ],
20✔
306
                                                        'comment'       => [
20✔
307
                                                                'required'          => false,
20✔
308
                                                                'sanitize_callback' => function ( $param ) {
20✔
309
                                                                        // Allow basic tags, then store as HTML entities.
NEW
310
                                                                        $allowed_html = [
×
NEW
311
                                                                                'strong' => [],
×
NEW
312
                                                                                'b'      => [],
×
NEW
313
                                                                                'em'     => [],
×
NEW
314
                                                                                'i'      => [],
×
NEW
315
                                                                                'a'      => [
×
NEW
316
                                                                                        'href'   => true,
×
NEW
317
                                                                                        'target' => true,
×
NEW
318
                                                                                        'rel'    => true,
×
NEW
319
                                                                                ],
×
NEW
320
                                                                        ];
×
NEW
321
                                                                        return esc_html( wp_kses( $param, $allowed_html ) );
×
322
                                                                },
20✔
323
                                                        ],
20✔
324
                                                        'ignore_global' => [
20✔
325
                                                                'required'          => false,
20✔
326
                                                                'default'           => 0,
20✔
327
                                                                'sanitize_callback' => 'absint',
20✔
328
                                                        ],
20✔
329
                                                        'largeBatch'    => [
20✔
330
                                                                'required'          => false,
20✔
331
                                                                'default'           => false,
20✔
332
                                                                'sanitize_callback' => function ( $param ) {
20✔
NEW
333
                                                                        return filter_var( $param, FILTER_VALIDATE_BOOLEAN );
×
334
                                                                },
20✔
335
                                                        ],
20✔
336
                                                ],
20✔
337
                                                'permission_callback' => function ( $request ) {
20✔
NEW
338
                                                        global $wpdb;
×
NEW
339
                                                        $issue_id = isset( $request['issue_id'] ) ? (int) $request['issue_id'] : 0;
×
NEW
340
                                                        if ( $issue_id <= 0 ) {
×
NEW
341
                                                                return false;
×
342
                                                        }
343

NEW
344
                                                        $table_name = edac_get_valid_table_name( $wpdb->prefix . 'accessibility_checker' );
×
NEW
345
                                                        if ( ! $table_name ) {
×
NEW
346
                                                                return false;
×
347
                                                        }
348

349
                                                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup.
NEW
350
                                                        $post_id = (int) $wpdb->get_var(
×
NEW
351
                                                                $wpdb->prepare( 'SELECT postid FROM %i WHERE id = %d', $table_name, $issue_id )
×
NEW
352
                                                        );
×
353

NEW
354
                                                        return $post_id > 0 ? current_user_can( 'edit_post', $post_id ) : false;
×
355
                                                },
20✔
356
                                        ]
20✔
357
                                );
20✔
358
                        }
20✔
359
                );
20✔
360
        }
361

362
        /**
363
         * REST handler to clear issues results for a given post ID.
364
         *
365
         * @param \WP_REST_Request $request  The request passed from the REST call.
366
         *
367
         * @return \WP_REST_Response
368
         */
369
        public function clear_issues_for_post( $request ) {
370

371
                if ( ! isset( $request['id'] ) ) {
4✔
372
                        return new \WP_REST_Response( [ 'message' => 'The ID is required to be passed.' ], 400 );
×
373
                }
374

375
                $json    = $request->get_json_params();
4✔
376
                $post_id = (int) $request['id'];
4✔
377
                if ( ! isset( $json['skip_post_exists_check'] ) ) {
4✔
378
                        $post = get_post( $post_id );
4✔
379
                        if ( ! is_object( $post ) ) {
4✔
380
                                return new \WP_REST_Response( [ 'message' => 'The post is not valid.' ], 400 );
×
381
                        }
382

383
                        $post_type  = get_post_type( $post );
4✔
384
                        $post_types = Settings::get_scannable_post_types();
4✔
385
                        if ( empty( $post_types ) || ! in_array( $post_type, $post_types, true ) ) {
4✔
386
                                return new \WP_REST_Response( [ 'message' => 'The post type is not set to be scanned.' ], 400 );
×
387
                        }
388
                }
389

390
                // if flush is set then clear the issues for that ID.
391
                if ( isset( $json['flush'] ) ) {
4✔
392
                        // purge the issues for this post.
393
                        Purge_Post_Data::delete_post( $post_id );
4✔
394
                }
395

396
                return new \WP_REST_Response(
4✔
397
                        [
4✔
398
                                'success' => true,
4✔
399
                                'flushed' => isset( $json['flush'] ),
4✔
400
                                'id'      => $post_id,
4✔
401
                        ]
4✔
402
                );
4✔
403
        }
404

405

406
        /**
407
         * Filter the html of the js validation violation.
408
         *
409
         * This can be used to store additional data in the html of the violation.
410
         *
411
         * @since 1.13.0
412
         * @param string $html      The html of the violation.
413
         * @param string $rule_id   The id of the rule.
414
         * @param array  $violation The violation data.
415
         *
416
         * @return string
417
         */
418
        public function filter_js_validation_html( string $html, string $rule_id, array $violation ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- the variable was used previously and will be used in future most likely.
419
                // Use just the opening <html> and closing </html> tag, prevents storing entire page as the affected code.
420
                if ( 'html-has-lang' === $rule_id || 'document-title' === $rule_id ) {
×
421
                        $html = preg_replace( '/^.*(<html.*?>).*(<\/html>).*$/s', '$1...$2', $html );
×
422

423
                }
424
                return $html;
×
425
        }
426

427
        /**
428
         * REST handler that saves to the DB a list of js rule violations for a post.
429
         *
430
         * @param WP_REST_Request $request  The request passed from the REST call.
431
         *
432
         * @return \WP_REST_Response
433
         */
434
        public function set_post_scan_results( $request ) {
435

436
                if ( ! isset( $request['violations'] ) ) {
4✔
437
                        return new \WP_REST_Response( [ 'message' => 'A required parameter is missing.' ], 400 );
×
438
                }
439

440
                $post_id = (int) $request['id'];
4✔
441
                $post    = get_post( $post_id );
4✔
442
                if ( ! is_object( $post ) ) {
4✔
443

444
                        return new \WP_REST_Response( [ 'message' => 'The post is not valid.' ], 400 );
×
445
                }
446

447
                $post_type  = get_post_type( $post );
4✔
448
                $post_types = Settings::get_scannable_post_types();
4✔
449
                if ( empty( $post_types ) || ! in_array( $post_type, $post_types, true ) ) {
4✔
450

451
                        return new \WP_REST_Response( [ 'message' => 'The post type is not set to be scanned.' ], 400 );
×
452

453
                }
454

455
                //phpcs:ignore Generic.Commenting.Todo.TaskFound
456
                // TODO: setup a rules class for loading/filtering rules.
457
                $rules             = edac_register_rules();
4✔
458
                $js_rule_ids       = [];
4✔
459
                $combined_rule_ids = [];
4✔
460
                foreach ( $rules as $rule ) {
4✔
461
                        if ( array_key_exists( 'ruleset', $rule ) && 'js' === $rule['ruleset'] ) {
4✔
462
                                $js_rule_ids[] = $rule['slug'];
4✔
463

464
                                // Some rules can be a grouping of other checks with different ids. This tracks those combined check IDs for later mapping.
465
                                if ( array_key_exists( 'combines', $rule ) && ! empty( $rule['combines'] ) ) {
4✔
466
                                        foreach ( $rule['combines'] as $combine_rule_id ) {
4✔
467
                                                $combined_rule_ids[ $combine_rule_id ] = $rule['slug'];
4✔
468
                                        }
469
                                }
470
                        }
471
                }
472

473
                try {
474

475
                        /**
476
                         * Fires before the validation process starts.
477
                         *
478
                         * This is only running in the JS check context.
479
                         *
480
                         * @since 1.5.0
481
                         *
482
                         * @param int    $post_id The post ID.
483
                         * @param string $type    The type of validation which is always 'js' in this path.
484
                         */
485
                        do_action( 'edac_before_validate', $post_id, 'js' );
4✔
486

487
                        $violations = $request['violations'];
4✔
488

489
                        // set record check flag on previous error records.
490
                        edac_remove_corrected_posts( $post_id, $post->post_type, $pre = 1, 'js' );
4✔
491

492
                        if ( is_array( $violations ) && count( $violations ) > 0 ) {
4✔
493

494
                                foreach ( $violations as $violation ) {
4✔
495
                                        $rule_id = $violation['ruleId'];
4✔
496

497
                                        // If this rule is a combined rule then map it to the actual reporting rule ID.
498
                                        $actual_rule_id = array_key_exists( $rule_id, $combined_rule_ids ) ? $combined_rule_ids[ $rule_id ] : $rule_id;
4✔
499

500
                                        if ( in_array( $actual_rule_id, $js_rule_ids, true ) ) {
4✔
501

502
                                                // This rule is one that we've included in our js ruleset.
503

504
                                                $html   = apply_filters( 'edac_filter_js_violation_html', $violation['html'], $rule_id, $violation );
×
505
                                                $impact = $violation['impact']; // by default, use the impact setting from the js rule.
×
506

507
                                                //phpcs:ignore Generic.Commenting.Todo.TaskFound
508
                                                // TODO: setup a rules class for loading/filtering rules.
509
                                                foreach ( $rules as $rule ) {
×
510
                                                        if ( $rule['slug'] === $actual_rule_id ) {
×
511
                                                                $impact = $rule['rule_type']; // if we are defining the rule_type in php rules config, use that instead of the js rule's impact setting.
×
512
                                                        }
513
                                                }
514

515
                                                //phpcs:ignore Generic.Commenting.Todo.TaskFound, Squiz.PHP.CommentedOutCode.Found
516
                                                // TODO: add support storing $violation['selector'], $violation['tags'].
517

518
                                                /**
519
                                                 * Fires before a rule is run against the content.
520
                                                 *
521
                                                 * This is only running in the JS check context.
522
                                                 *
523
                                                 * @since 1.5.0
524
                                                 *
525
                                                 * @param int    $post_id The post ID.
526
                                                 * @param string $rule_id The rule ID.
527
                                                 * @param string $type    The type of validation which is always 'js' in this path.
528
                                                 */
529
                                                do_action( 'edac_before_rule', $post_id, $actual_rule_id, 'js' );
×
530

531
                                                $landmark          = $violation['landmark'] ?? null;
×
532
                                                $landmark_selector = $violation['landmarkSelector'] ?? null;
×
533

534
                                                $selectors = [
×
535
                                                        'selector' => $violation['selector'] ?? [],
×
536
                                                        'ancestry' => $violation['ancestry'] ?? [],
×
537
                                                        'xpath'    => $violation['xpath'] ?? [],
×
538
                                                ];
×
539
                                                ( new Insert_Rule_Data() )->insert( $post, $actual_rule_id, $impact, $html, $landmark, $landmark_selector, $selectors );
×
540

541
                                                /**
542
                                                 * Fires after a rule is run against the content.
543
                                                 *
544
                                                 * This is only running in the JS check context.
545
                                                 *
546
                                                 * @since 1.5.0
547
                                                 *
548
                                                 * @param int    $post_id The post ID.
549
                                                 * @param string $rule_id The rule ID.
550
                                                 * @param string $type    The type of validation which is always 'js' in this path.
551
                                                 */
552
                                                do_action( 'edac_after_rule', $post_id, $actual_rule_id, 'js' );
×
553

554
                                        }
555
                                }
556
                        }
557

558
                        /**
559
                         * Fires after the validation process is complete.
560
                         *
561
                         * This is only running in the JS check context.
562
                         *
563
                         * @since 1.5.0
564
                         *
565
                         * @param int    $post_id The post ID.
566
                         * @param string $type    The type of validation which is always 'js' in this path.
567
                         */
568
                        do_action( 'edac_after_validate', $post_id, 'js' );
4✔
569

570
                        // remove corrected records.
571
                        edac_remove_corrected_posts( $post_id, $post->post_type, $pre = 2, 'js' );
4✔
572

573
                        // Save the density metrics before the summary is generated.
574
                        $metrics = $request['densityMetrics'] ?? [ 0, 0 ];
4✔
575
                        if ( is_array( $metrics ) && count( $metrics ) > 0 ) {
4✔
576
                                update_post_meta(
4✔
577
                                        $post_id,
4✔
578
                                        '_edac_density_data',
4✔
579
                                        [
4✔
580
                                                $metrics['elementCount'] ?? 0,
4✔
581
                                                $metrics['contentLength'] ?? 0,
4✔
582
                                        ]
4✔
583
                                );
4✔
584
                        }
585

586
                        // Update the summary info that is stored in meta this post.
587
                        ( new Summary_Generator( $post_id ) )->generate_summary();
4✔
588

589
                        // store a record of this scan in the post's meta.
590
                        update_post_meta( $post_id, '_edac_post_checked_js', time() );
4✔
591

592
                        /**
593
                         * Fires before sending the REST response ending the validation process.
594
                         *
595
                         * @since 1.14.0
596
                         *
597
                         * @param int             $post_id The post ID.
598
                         * @param string          $type    The type of validation which is always 'js' in this path.
599
                         * @param WP_REST_Request $request The request passed from the REST call.
600
                         */
601
                        do_action( 'edac_validate_before_sending_rest_response', $post_id, 'js', $request );
4✔
602

603
                        return new \WP_REST_Response(
4✔
604
                                [
4✔
605
                                        'success'   => true,
4✔
606
                                        'id'        => $post_id,
4✔
607
                                        'timestamp' => time(),
4✔
608
                                ]
4✔
609
                        );
4✔
610

611
                } catch ( \Exception $ex ) {
×
612

613
                        return new \WP_REST_Response(
×
614
                                [
×
615
                                        'message' => $ex->getMessage(),
×
616
                                ],
×
617
                                500
×
618
                        );
×
619

620
                }
621
        }
622

623

624
        /**
625
         * REST handler that clears the cached stats about the scans
626
         *
627
         * @return \WP_REST_Response
628
         */
629
        public function clear_cached_scans_stats() {
630

631
                try {
632

633
                        // Clear the cache.
634
                        $scans_stats = new Scans_Stats();
×
635
                        $scans_stats->clear_cache();
×
636

637
                        // Prime the cache.
638
                        $scans_stats = new Scans_Stats();
×
639

640
                        return new \WP_REST_Response(
×
641
                                [
×
642
                                        'success' => true,
×
643
                                ]
×
644
                        );
×
645

646
                } catch ( \Exception $ex ) {
×
647

648
                        return new \WP_REST_Response(
×
649
                                [
×
650
                                        'message' => $ex->getMessage(),
×
651
                                ],
×
652
                                500
×
653
                        );
×
654

655
                }
656
        }
657

658
        /**
659
         * REST handler that gets stats about the scans
660
         *
661
         * @return \WP_REST_Response
662
         */
663
        public function get_scans_stats() {
664

665
                try {
666

667
                        $scans_stats = new Scans_Stats( 60 * 5 );
×
668
                        $stats       = $scans_stats->summary();
×
669

670
                        return new \WP_REST_Response(
×
671
                                [
×
672
                                        'success' => true,
×
673
                                        'stats'   => $stats,
×
674
                                ]
×
675
                        );
×
676

677
                } catch ( \Exception $ex ) {
×
678

679
                        return new \WP_REST_Response(
×
680
                                [
×
681
                                        'message' => $ex->getMessage(),
×
682
                                ],
×
683
                                500
×
684
                        );
×
685

686
                }
687
        }
688

689

690
        /**
691
         * REST handler that gets stats about the scans by post type
692
         *
693
         * @param WP_REST_Request $request The request passed from the REST call.
694
         *
695
         * @return \WP_REST_Response
696
         */
697
        public function get_scans_stats_by_post_type( $request ) {
698

699
                if ( ! isset( $request['slug'] ) ) {
×
700
                        return new \WP_REST_Response( [ 'message' => 'A required parameter is missing.' ], 400 );
×
701
                }
702

703
                try {
704

705
                        $post_type            = strval( $request['slug'] );
×
706
                        $scannable_post_types = Settings::get_scannable_post_types();
×
707

708
                        if ( in_array( $post_type, $scannable_post_types, true ) ) {
×
709

710
                                $scans_stats = new Scans_Stats( 60 * 5 );
×
711
                                $by_type     = $scans_stats->issues_summary_by_post_type( $post_type );
×
712

713
                                return new \WP_REST_Response(
×
714
                                        [
×
715
                                                'success' => true,
×
716
                                                'stats'   => $by_type,
×
717
                                        ]
×
718
                                );
×
719
                        }
720
                        return new \WP_REST_Response( [ 'message' => 'The post type is not set to be scanned.' ], 400 );
×
721
                } catch ( \Exception $ex ) {
×
722
                        return new \WP_REST_Response(
×
723
                                [
×
724
                                        'message' => $ex->getMessage(),
×
725
                                ],
×
726
                                500
×
727
                        );
×
728
                }
729
        }
730

731
        /**
732
         * REST handler that gets stats about the scans by post types
733
         *
734
         * @param WP_REST_Request $request The request passed from the REST call.
735
         *
736
         * @return \WP_REST_Response
737
         */
738
        public function get_scans_stats_by_post_types( $request ) { //phpcs:ignore
739

740
                try {
741

742
                        $scans_stats = new Scans_Stats( 60 * 5 );
×
743

744
                        $scannable_post_types = Settings::get_scannable_post_types();
×
745

746
                        $post_types = get_post_types(
×
747
                                [
×
748
                                        'public' => true,
×
749
                                ]
×
750
                        );
×
751
                        unset( $post_types['attachment'] );
×
752

753
                        $post_types_to_check = array_merge( [ 'post', 'page' ], $scannable_post_types );
×
754

755
                        $by_types = [];
×
756

757
                        foreach ( $post_types as $post_type ) {
×
758

759
                                $by_types[ $post_type ] = false;
×
760
                                if ( in_array( $post_type, $scannable_post_types, true ) && in_array( $post_type, $post_types_to_check, true ) ) {
×
761
                                        $by_types[ $post_type ] = $scans_stats->issues_summary_by_post_type( $post_type );
×
762
                                }
763
                        }
764

765
                        return new \WP_REST_Response(
×
766
                                [
×
767
                                        'success' => true,
×
768
                                        'stats'   => $by_types,
×
769
                                ]
×
770
                        );
×
771

772
                } catch ( \Exception $ex ) {
×
773

774
                        return new \WP_REST_Response(
×
775
                                [
×
776
                                        'message' => $ex->getMessage(),
×
777
                                ],
×
778
                                500
×
779
                        );
×
780

781
                }
782
        }
783

784
        /**
785
         * REST handler that gets stats about the scans
786
         *
787
         * @param \WP_REST_Request $request The request passed from the REST call.
788
         *
789
         * @return \WP_REST_Response
790
         */
791
        public function get_site_summary( \WP_REST_Request $request ) {
792

793
                try {
794
                        $scan_stats = new Scans_Stats();
×
795
                        if ( (bool) $request->get_param( 'clearCache' ) ) {
×
796
                                $scan_stats->clear_cache();
×
797
                        }
798

799
                        return new \WP_REST_Response(
×
800
                                [
×
801
                                        'success' => true,
×
802
                                        'stats'   => $scan_stats->summary(),
×
803
                                ]
×
804
                        );
×
805
                } catch ( \Exception $ex ) {
×
806
                        return new \WP_REST_Response(
×
807
                                [
×
808
                                        'message' => $ex->getMessage(),
×
809
                                ],
×
810
                                500
×
811
                        );
×
812
                }
813
        }
814

815
        /**
816
         * REST handler that gets all sidebar data for a post (summary, details, readability).
817
         *
818
         * @since 1.xx.x
819
         *
820
         * @param \WP_REST_Request $request The request passed from the REST call.
821
         *
822
         * @return \WP_REST_Response
823
         */
824
        public function get_sidebar_data( \WP_REST_Request $request ) {
825
                $post_id = (int) $request['id'];
8✔
826

827
                try {
828
                        $data = [
8✔
829
                                'post_id'     => $post_id,
8✔
830
                                'summary'     => $this->get_summary_data( $post_id ),
8✔
831
                                'details'     => $this->get_details_data( $post_id ),
8✔
832
                                'readability' => $this->get_readability_data( $post_id ),
8✔
833
                        ];
8✔
834

835
                        return new \WP_REST_Response(
8✔
836
                                [
8✔
837
                                        'success' => true,
8✔
838
                                        'data'    => $data,
8✔
839
                                ],
8✔
840
                                200
8✔
841
                        );
8✔
NEW
842
                } catch ( \Exception $ex ) {
×
NEW
843
                        return new \WP_REST_Response(
×
NEW
844
                                [
×
NEW
845
                                        'success' => false,
×
NEW
846
                                        'message' => $ex->getMessage(),
×
NEW
847
                                ],
×
NEW
848
                                500
×
NEW
849
                        );
×
850
                }
851
        }
852

853
        /**
854
         * Get summary data for a post.
855
         *
856
         * Returns cached summary data from post meta. If no cache exists, returns defaults.
857
         *
858
         * @since 1.xx.x
859
         *
860
         * @param int $post_id The post ID.
861
         *
862
         * @return array
863
         */
864
        private function get_summary_data( $post_id ) {
865
                // Get summary from post meta.
866
                $summary = get_post_meta( $post_id, '_edac_summary', true );
12✔
867

868
                // If summary doesn't exist or is invalid, return defaults.
869
                if ( ! $summary || ! is_array( $summary ) ) {
12✔
870
                        $summary = [
10✔
871
                                'passed_tests'    => 0,
10✔
872
                                'errors'          => 0,
10✔
873
                                'contrast_errors' => 0,
10✔
874
                                'warnings'        => 0,
10✔
875
                                'ignored'         => 0,
10✔
876
                                'readability'     => 0,
10✔
877
                        ];
10✔
878
                }
879

880
                return $summary;
12✔
881
        }
882

883
        /**
884
         * Get details data for a post (errors, warnings, passed rules).
885
         *
886
         * @since 1.xx.x
887
         *
888
         * @param int $post_id The post ID.
889
         *
890
         * @throws \Exception If the database table name is invalid.
891
         * @return array
892
         */
893
        private function get_details_data( $post_id ) {
894
                global $wpdb;
10✔
895
                $table_name = edac_get_valid_table_name( $wpdb->prefix . 'accessibility_checker' );
10✔
896
                $siteid     = get_current_blog_id();
10✔
897

898
                if ( ! $table_name ) {
10✔
NEW
899
                        throw new \Exception( esc_html__( 'Invalid table name', 'accessibility-checker' ) );
×
900
                }
901

902
                $rules = edac_register_rules();
10✔
903
                if ( ! $rules ) {
10✔
NEW
904
                        return [
×
NEW
905
                                'errors'   => [],
×
NEW
906
                                'warnings' => [],
×
NEW
907
                                'passed'   => [],
×
NEW
908
                        ];
×
909
                }
910

911
                if ( ! current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ) ) {
10✔
912
                        foreach ( $rules as $rule_key => $rule ) {
2✔
913
                                if ( isset( $rule['fixes'] ) ) {
2✔
914
                                        unset( $rules[ $rule_key ]['fixes'] );
2✔
915
                                }
916
                        }
917
                }
918

919
                // If ANWW is active remove link_blank for details.
920
                if ( defined( 'ANWW_VERSION' ) ) {
10✔
NEW
921
                        $rules = edac_remove_element_with_value( $rules, 'slug', 'link_blank' );
×
922
                }
923

924
                $passed_rules  = [];
10✔
925
                $error_rules   = edac_filter_by_value( $rules, 'rule_type', 'error' );
10✔
926
                $warning_rules = edac_filter_by_value( $rules, 'rule_type', 'warning' );
10✔
927

928
                // Process both error and warning rules.
929
                $error_rules   = $this->process_rules_for_details( $error_rules, $post_id, $table_name, $siteid, $passed_rules );
10✔
930
                $warning_rules = $this->process_rules_for_details( $warning_rules, $post_id, $table_name, $siteid, $passed_rules );
10✔
931

932
                return [
10✔
933
                        'errors'   => array_values( $error_rules ),
10✔
934
                        'warnings' => array_values( $warning_rules ),
10✔
935
                        'passed'   => $passed_rules,
10✔
936
                ];
10✔
937
        }
938

939
        /**
940
         * Process rules and fetch issue details from the database.
941
         *
942
         * @since 1.xx.x
943
         *
944
         * @param array  $rules         The rules to process.
945
         * @param int    $post_id       The post ID.
946
         * @param string $table_name    The database table name.
947
         * @param int    $siteid        The site/blog ID.
948
         * @param array  &$passed_rules Reference to passed rules array (populated by this method).
949
         *
950
         * @return array The processed rules with counts and details.
951
         */
952
        private function process_rules_for_details( $rules, $post_id, $table_name, $siteid, &$passed_rules ) {
953
                global $wpdb;
10✔
954
                static $user_cache = [];
10✔
955

956
                // Early return if no rules to process.
957
                if ( empty( $rules ) ) {
10✔
NEW
958
                        return $rules;
×
959
                }
960

961
                // Extract rule slugs for IN clause.
962
                $rule_slugs = array_column( $rules, 'slug' );
10✔
963

964
                // Build a simple, escaped IN clause.
965
                $safe_table    = esc_sql( $table_name );
10✔
966
                $escaped_slugs = array_map( 'esc_sql', $rule_slugs );
10✔
967
                $in_clause     = "'" . implode( "','", $escaped_slugs ) . "'";
10✔
968

969
                // Direct SQL query (table and values already escaped).
970
                $sql = "SELECT *\n"
10✔
971
                        . "FROM `{$safe_table}`\n"
10✔
972
                        . "WHERE postid = {$post_id}\n"
10✔
973
                        . "AND rule IN ( {$in_clause} )\n"
10✔
974
                        . "AND siteid = {$siteid}";
10✔
975

976
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
977
                $all_results = $wpdb->get_results( $sql, ARRAY_A );
10✔
978

979
                // Group results by rule slug.
980
                $results_by_rule = [];
10✔
981
                foreach ( $all_results as $result ) {
10✔
982
                        $rule_slug = $result['rule'];
2✔
983
                        if ( ! isset( $results_by_rule[ $rule_slug ] ) ) {
2✔
984
                                $results_by_rule[ $rule_slug ] = [];
2✔
985
                        }
986
                        // If we have non-zero ignre_user then get the username.
987
                        if ( isset( $result['ignre_user'] ) && (int) $result['ignre_user'] > 0 ) {
2✔
NEW
988
                                $user_id = (int) $result['ignre_user'];
×
NEW
989
                                if ( ! array_key_exists( $user_id, $user_cache ) ) {
×
NEW
990
                                        $user_info              = get_userdata( $user_id );
×
NEW
991
                                        $user_cache[ $user_id ] = $user_info ? $user_info->user_login : __( 'Unknown', 'accessibility-checker' );
×
992
                                }
NEW
993
                                $result['ignre_user_name'] = $user_cache[ $user_id ];
×
994
                        }
995
                        $results_by_rule[ $rule_slug ][] = $result;
2✔
996
                }
997

998
                // Process each rule with its results.
999
                foreach ( $rules as $key => $rule ) {
10✔
1000
                        $rule_slug = $rule['slug'];
10✔
1001
                        $results   = $results_by_rule[ $rule_slug ] ?? [];
10✔
1002
                        $count     = count( $results );
10✔
1003

1004
                        if ( $count ) {
10✔
1005
                                $rules[ $key ]['count']   = $count;
2✔
1006
                                $rules[ $key ]['details'] = $results;
2✔
1007
                                // Add WCAG URL based on wcag number.
1008
                                if ( isset( $rule['wcag'] ) ) {
2✔
1009
                                        $rules[ $key ] += $this->get_wcag_url_and_title_from_number( $rule['wcag'] );
2✔
1010
                                }
1011
                        } else {
1012
                                $rule['count']  = 0;
10✔
1013
                                $passed_rules[] = $rule;
10✔
1014
                                unset( $rules[ $key ] );
10✔
1015
                        }
1016
                }
1017

1018
                return $rules;
10✔
1019
        }
1020

1021
        /**
1022
         * Get readability data for a post.
1023
         *
1024
         * @since 1.xx.x
1025
         *
1026
         * @param int $post_id The post ID.
1027
         *
1028
         * @throws \Exception If the post is not found.
1029
         * @return array
1030
         */
1031
        private function get_readability_data( $post_id ) {
1032
                $simplified_summary          = (string) get_post_meta( $post_id, '_edac_simplified_summary', true );
8✔
1033
                $simplified_summary_position = get_option( 'edac_simplified_summary_position', false );
8✔
1034

1035
                $content_post = get_post( $post_id );
8✔
1036
                if ( ! $content_post ) {
8✔
NEW
1037
                        throw new \Exception( esc_html__( 'Post not found', 'accessibility-checker' ) );
×
1038
                }
1039

1040
                $content = $content_post->post_content;
8✔
1041
                $content = apply_filters( 'the_content', $content );
8✔
1042

1043
                /**
1044
                 * Filter the content used for reading grade readability analysis.
1045
                 *
1046
                 * @since 1.4.0
1047
                 *
1048
                 * @param string $content The content to be filtered.
1049
                 * @param int    $post_id The post ID.
1050
                 */
1051
                $content = apply_filters( 'edac_filter_readability_content', $content, $post_id );
8✔
1052
                $content = wp_filter_nohtml_kses( $content );
8✔
1053
                $content = str_replace( ']]>', ']]&gt;', $content );
8✔
1054

1055
                // Get readability metadata.
1056
                $edac_summary           = get_post_meta( $post_id, '_edac_summary', true );
8✔
1057
                $post_grade_readability = isset( $edac_summary['readability'] ) ? $edac_summary['readability'] : 0;
8✔
1058
                $post_grade             = (int) filter_var( $post_grade_readability, FILTER_SANITIZE_NUMBER_INT );
8✔
1059
                $post_grade_failed      = $post_grade > 9; // Treat Flesch-Kincaid grade 9+ (above roughly 8th-grade reading level recommended for plain language) as a readability failure.
8✔
1060

1061
                $simplified_summary_grade = 0;
8✔
1062
                if ( class_exists( 'DaveChild\TextStatistics\TextStatistics' ) ) {
8✔
1063
                        $text_statistics          = new \DaveChild\TextStatistics\TextStatistics();
8✔
1064
                        $simplified_summary_grade = (int) floor( $text_statistics->fleschKincaidGradeLevel( $simplified_summary ) );
8✔
1065
                }
1066

1067
                $simplified_summary_grade_failed      = $simplified_summary_grade >= 9;
8✔
1068
                $simplified_summary_grade_readability = edac_ordinal( $simplified_summary_grade );
8✔
1069
                $simplified_summary_prompt            = get_option( 'edac_simplified_summary_prompt' );
8✔
1070

1071
                return [
8✔
1072
                        'post_grade'                           => $post_grade,
8✔
1073
                        'post_grade_readability'               => $post_grade_readability,
8✔
1074
                        'post_grade_failed'                    => $post_grade_failed,
8✔
1075
                        'simplified_summary'                   => $simplified_summary,
8✔
1076
                        'simplified_summary_grade'             => $simplified_summary_grade,
8✔
1077
                        'simplified_summary_grade_readability' => $simplified_summary_grade_readability,
8✔
1078
                        'simplified_summary_grade_failed'      => $simplified_summary_grade_failed,
8✔
1079
                        'simplified_summary_prompt'            => $simplified_summary_prompt,
8✔
1080
                        'simplified_summary_position'          => $simplified_summary_position,
8✔
1081
                        'content_length'                       => strlen( $content ),
8✔
1082
                ];
8✔
1083
        }
1084

1085
        /**
1086
         * Save simplified summary for a post.
1087
         *
1088
         * @since 1.xx.x
1089
         *
1090
         * @param \WP_REST_Request $request The REST request object.
1091
         * @return \WP_REST_Response|\WP_Error
1092
         */
1093
        public function save_simplified_summary( \WP_REST_Request $request ) {
NEW
1094
                $post_id = (int) $request['id'];
×
NEW
1095
                $summary = sanitize_textarea_field( wp_unslash( $request['summary'] ) );
×
1096

1097
                // Update the post meta with the simplified summary (matching AJAX behavior).
NEW
1098
                update_post_meta(
×
NEW
1099
                        $post_id,
×
NEW
1100
                        '_edac_simplified_summary',
×
NEW
1101
                        $summary
×
NEW
1102
                );
×
1103

1104
                // Get the complete readability data structure (same as the main readability endpoint).
1105
                try {
NEW
1106
                        $readability_data = $this->get_readability_data( $post_id );
×
1107

1108
                        // Return data structure that matches the readability endpoint format.
NEW
1109
                        return new \WP_REST_Response(
×
NEW
1110
                                [
×
NEW
1111
                                        'success'                              => true,
×
NEW
1112
                                        'post_grade'                           => $readability_data['post_grade'],
×
NEW
1113
                                        'post_grade_readability'               => $readability_data['post_grade_readability'],
×
NEW
1114
                                        'post_grade_failed'                    => $readability_data['post_grade_failed'],
×
NEW
1115
                                        'simplified_summary'                   => $readability_data['simplified_summary'],
×
NEW
1116
                                        'simplified_summary_grade'             => $readability_data['simplified_summary_grade'],
×
NEW
1117
                                        'simplified_summary_grade_readability' => $readability_data['simplified_summary_grade_readability'],
×
NEW
1118
                                        'simplified_summary_grade_failed'      => $readability_data['simplified_summary_grade_failed'],
×
NEW
1119
                                        'simplified_summary_prompt'            => $readability_data['simplified_summary_prompt'],
×
NEW
1120
                                        'simplified_summary_position'          => $readability_data['simplified_summary_position'],
×
NEW
1121
                                        'content_length'                       => $readability_data['content_length'],
×
NEW
1122
                                ],
×
NEW
1123
                                200
×
NEW
1124
                        );
×
NEW
1125
                } catch ( \Exception $e ) {
×
NEW
1126
                        return new \WP_Error(
×
NEW
1127
                                'readability_data_error',
×
NEW
1128
                                $e->getMessage(),
×
NEW
1129
                                [ 'status' => 500 ]
×
NEW
1130
                        );
×
1131
                }
1132
        }
1133

1134
        /**
1135
         * Get WCAG URL from wcag number
1136
         *
1137
         * @param string $wcag_number The WCAG number (e.g., '1.1.1').
1138
         * @return array An array containing 'wcag_title' and 'wcag_url' keys. Both values will be empty strings if the WCAG number is not found.
1139
         */
1140
        private function get_wcag_url_and_title_from_number( $wcag_number ) {
1141
                $wcag_data_to_return = [
2✔
1142
                        'wcag_title' => '',
2✔
1143
                        'wcag_url'   => '',
2✔
1144
                ];
2✔
1145

1146
                if ( ! $wcag_number ) {
2✔
NEW
1147
                        return $wcag_data_to_return;
×
1148
                }
1149

1150
                static $wcag_lookup = null;
2✔
1151

1152
                if ( null === $wcag_lookup ) {
2✔
1153
                        // Load the WCAG data file.
1154
                        $wcag_file = EDAC_PLUGIN_DIR . 'includes/wcag.php';
2✔
1155
                        if ( ! file_exists( $wcag_file ) ) {
2✔
NEW
1156
                                $wcag_lookup = [];
×
NEW
1157
                                return $wcag_data_to_return;
×
1158
                        }
1159

1160
                        $wcag_data = include $wcag_file;
2✔
1161
                        if ( ! is_array( $wcag_data ) ) {
2✔
NEW
1162
                                $wcag_lookup = [];
×
NEW
1163
                                return $wcag_data_to_return;
×
1164
                        }
1165

1166
                        // Re-key the array by WCAG number for O(1) lookups.
1167
                        $wcag_lookup = array_column( $wcag_data, null, 'number' );
2✔
1168
                }
1169

1170
                // O(1) lookup by WCAG number.
1171
                if ( isset( $wcag_lookup[ $wcag_number ] ) ) {
2✔
1172
                        $entry               = $wcag_lookup[ $wcag_number ];
2✔
1173
                        $wcag_data_to_return = [
2✔
1174
                                'wcag_title' => $entry['title'] ?? '',
2✔
1175
                                'wcag_url'   => $entry['wcag_url'] ?? '',
2✔
1176
                        ];
2✔
1177
                }
1178

1179
                return $wcag_data_to_return;
2✔
1180
        }
1181

1182
        /**
1183
         * REST handler for dismissing or restoring an issue.
1184
         *
1185
         * @param \WP_REST_Request $request The request object.
1186
         * @return \WP_REST_Response|\WP_Error Response object on success, WP_Error on failure.
1187
         */
1188
        public function dismiss_issue( $request ) {
NEW
1189
                global $wpdb;
×
1190

NEW
1191
                $issue_id      = (int) $request['issue_id'];
×
NEW
1192
                $action        = $request->get_param( 'action' );
×
NEW
1193
                $reason        = $request->get_param( 'reason' ) ?? '';
×
NEW
1194
                $comment       = $request->get_param( 'comment' ) ?? '';
×
NEW
1195
                $ignore_global = $request->get_param( 'ignore_global' ) ?? 0;
×
NEW
1196
                $large_batch   = $request->get_param( 'largeBatch' ) ?? false;
×
1197

NEW
1198
                $table_name = $wpdb->prefix . 'accessibility_checker';
×
NEW
1199
                $site_id    = get_current_blog_id();
×
1200

NEW
1201
                $allowed_ignore_actions = [ 'enable', 'ignore', 'dismiss' ];
×
1202
                // Set values based on action (matching AJAX endpoint behavior).
NEW
1203
                $is_ignoring          = in_array( $action, $allowed_ignore_actions, true ); // old systems send 'enable' when ignoring. This handles both for back compat but 'enable' is very unclear and should be swapped.
×
NEW
1204
                $ignre                = $is_ignoring ? 1 : 0;
×
NEW
1205
                $ignre_user           = $is_ignoring ? get_current_user_id() : null;
×
NEW
1206
                $ignre_user_info      = $is_ignoring ? get_userdata( $ignre_user ) : null;
×
NEW
1207
                $ignre_username       = $is_ignoring && $ignre_user_info ? $ignre_user_info->user_login : '';
×
NEW
1208
                $ignre_date           = $is_ignoring ? edac_get_current_utc_datetime() : null;
×
NEW
1209
                $ignre_date_formatted = $is_ignoring ? edac_format_datetime_from_utc( $ignre_date ) : '';
×
NEW
1210
                $ignre_reason         = $is_ignoring ? $reason : null;
×
NEW
1211
                $ignre_comment        = $is_ignoring ? $comment : null;
×
NEW
1212
                $ignre_global         = $is_ignoring ? (int) $ignore_global : 0;
×
1213

1214
                // If largeBatch is set, update using the 'object' instead of ID.
1215
                // This handles cases where the same issue appears multiple times.
NEW
1216
                if ( $large_batch ) {
×
1217
                        // Get the 'object' from the issue id.
1218
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Need fresh data.
NEW
1219
                        $object = $wpdb->get_var( $wpdb->prepare( 'SELECT object FROM %i WHERE id = %d', $table_name, $issue_id ) );
×
1220

NEW
1221
                        if ( ! $object ) {
×
NEW
1222
                                return new \WP_Error(
×
NEW
1223
                                        'issue_not_found',
×
NEW
1224
                                        __( 'Issue not found.', 'accessibility-checker' ),
×
NEW
1225
                                        [ 'status' => 404 ]
×
NEW
1226
                                );
×
1227
                        }
1228

1229
                        // Update all issues with the same object.
1230
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update required, no caching needed.
NEW
1231
                        $result = $wpdb->query(
×
NEW
1232
                                $wpdb->prepare(
×
NEW
1233
                                        'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_reason = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d AND object = %s',
×
NEW
1234
                                        $table_name,
×
NEW
1235
                                        $ignre,
×
NEW
1236
                                        $ignre_user,
×
NEW
1237
                                        $ignre_date,
×
NEW
1238
                                        $ignre_reason,
×
NEW
1239
                                        $ignre_comment,
×
NEW
1240
                                        $ignre_global,
×
NEW
1241
                                        $site_id,
×
NEW
1242
                                        $object
×
NEW
1243
                                )
×
NEW
1244
                        );
×
1245
                } else {
1246
                        // Update single issue by ID.
1247
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update required, no caching needed.
NEW
1248
                        $result = $wpdb->query(
×
NEW
1249
                                $wpdb->prepare(
×
NEW
1250
                                        'UPDATE %i SET ignre = %d, ignre_user = %d, ignre_date = %s, ignre_reason = %s, ignre_comment = %s, ignre_global = %d WHERE siteid = %d AND id = %d',
×
NEW
1251
                                        $table_name,
×
NEW
1252
                                        $ignre,
×
NEW
1253
                                        $ignre_user,
×
NEW
1254
                                        $ignre_date,
×
NEW
1255
                                        $ignre_reason,
×
NEW
1256
                                        $ignre_comment,
×
NEW
1257
                                        $ignre_global,
×
NEW
1258
                                        $site_id,
×
NEW
1259
                                        $issue_id
×
NEW
1260
                                )
×
NEW
1261
                        );
×
1262
                }
1263

NEW
1264
                if ( false === $result ) {
×
NEW
1265
                        return new \WP_Error(
×
NEW
1266
                                'database_error',
×
NEW
1267
                                __( 'Failed to update the issue.', 'accessibility-checker' ),
×
NEW
1268
                                [ 'status' => 500 ]
×
NEW
1269
                        );
×
1270
                }
1271

NEW
1272
                return new \WP_REST_Response(
×
NEW
1273
                        [
×
NEW
1274
                                'success'         => true,
×
NEW
1275
                                'issue_id'        => $issue_id,
×
NEW
1276
                                'action'          => $action,
×
NEW
1277
                                'ignre'           => $is_ignoring,
×
NEW
1278
                                'ignre_global'    => $ignre_global,
×
NEW
1279
                                'ignre_user'      => $ignre_user,
×
NEW
1280
                                'ignre_user_name' => $ignre_username,
×
NEW
1281
                                'ignre_date'      => $ignre_date_formatted,
×
NEW
1282
                                'ignre_reason'    => $ignre_reason,
×
NEW
1283
                                'ignre_comment'   => $ignre_comment,
×
NEW
1284
                                'large_batch'     => $large_batch,
×
NEW
1285
                        ],
×
NEW
1286
                        200
×
NEW
1287
                );
×
1288
        }
1289
}
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