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

equalizedigital / accessibility-checker / 24683125413

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

push

github

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

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

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

2 existing lines in 2 files now uncovered.

5426 of 9437 relevant lines covered (57.5%)

4.5 hits per line

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

59.28
/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
use EqualizeDigital\AccessibilityChecker\MyDot\Connector;
15

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

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

25

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

32

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

41

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

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

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

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

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

102
                add_action(
20✔
103
                        'rest_api_init',
20✔
104
                        function () use ( $ns, $version ) {
20✔
105
                                register_rest_route(
20✔
106
                                        $ns . $version,
20✔
107
                                        '/scans-stats',
20✔
108
                                        [
20✔
109
                                                'methods'             => 'GET',
20✔
110
                                                'callback'            => [ $this, 'get_scans_stats' ],
20✔
111
                                                'permission_callback' => function ( $request ) {
20✔
NEW
112
                                                        if ( Connector::validate_jwt_token_in_request_with_fallback( $request ) ) {
×
113
                                                                // Only allow if the site is still registered (site_id present).
NEW
114
                                                                $site_id = (string) get_option( 'edac_site_id', '' );
×
NEW
115
                                                                return '' !== $site_id;
×
116
                                                        }
UNCOV
117
                                                        return current_user_can( 'edit_posts' );
×
118
                                                },
20✔
119
                                        ]
20✔
120
                                );
20✔
121
                        }
20✔
122
                );
20✔
123

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

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

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

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

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

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

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

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

350
                                                        $table_name = edac_get_valid_table_name( $wpdb->prefix . 'accessibility_checker' );
×
351
                                                        if ( ! $table_name ) {
×
352
                                                                return false;
×
353
                                                        }
354

355
                                                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Permission check requires direct lookup.
356
                                                        $post_id = (int) $wpdb->get_var(
×
357
                                                                $wpdb->prepare( 'SELECT postid FROM %i WHERE id = %d', $table_name, $issue_id )
×
358
                                                        );
×
359

360
                                                        return (bool) ( $post_id > 0 && current_user_can( 'edit_post', $post_id ) );
×
361
                                                },
20✔
362
                                        ]
20✔
363
                                );
20✔
364
                        }
20✔
365
                );
20✔
366
        }
367

368
        /**
369
         * REST handler to clear issues results for a given post ID.
370
         *
371
         * @param \WP_REST_Request $request  The request passed from the REST call.
372
         *
373
         * @return \WP_REST_Response
374
         */
375
        public function clear_issues_for_post( $request ) {
376

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

381
                $json    = $request->get_json_params();
4✔
382
                $post_id = (int) $request['id'];
4✔
383
                if ( ! isset( $json['skip_post_exists_check'] ) ) {
4✔
384
                        $post = get_post( $post_id );
4✔
385
                        if ( ! is_object( $post ) ) {
4✔
386
                                return new \WP_REST_Response( [ 'message' => 'The post is not valid.' ], 400 );
×
387
                        }
388

389
                        $post_type  = get_post_type( $post );
4✔
390
                        $post_types = Settings::get_scannable_post_types();
4✔
391
                        if ( empty( $post_types ) || ! in_array( $post_type, $post_types, true ) ) {
4✔
392
                                return new \WP_REST_Response( [ 'message' => 'The post type is not set to be scanned.' ], 400 );
×
393
                        }
394
                }
395

396
                // if flush is set then clear the issues for that ID.
397
                if ( isset( $json['flush'] ) ) {
4✔
398
                        // purge the issues for this post.
399
                        Purge_Post_Data::delete_post( $post_id );
4✔
400
                }
401

402
                return new \WP_REST_Response(
4✔
403
                        [
4✔
404
                                'success' => true,
4✔
405
                                'flushed' => isset( $json['flush'] ),
4✔
406
                                'id'      => $post_id,
4✔
407
                        ]
4✔
408
                );
4✔
409
        }
410

411

412
        /**
413
         * Filter the html of the js validation violation.
414
         *
415
         * This can be used to store additional data in the html of the violation.
416
         *
417
         * @since 1.13.0
418
         * @param string $html      The html of the violation.
419
         * @param string $rule_id   The id of the rule.
420
         * @param array  $violation The violation data.
421
         *
422
         * @return string
423
         */
424
        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.
425
                // Use just the opening <html> and closing </html> tag, prevents storing entire page as the affected code.
426
                if ( 'html-has-lang' === $rule_id || 'document-title' === $rule_id ) {
×
427
                        $html = preg_replace( '/^.*(<html.*?>).*(<\/html>).*$/s', '$1...$2', $html );
×
428

429
                }
430
                return $html;
×
431
        }
432

433
        /**
434
         * REST handler that saves to the DB a list of js rule violations for a post.
435
         *
436
         * @param WP_REST_Request $request  The request passed from the REST call.
437
         *
438
         * @return \WP_REST_Response
439
         */
440
        public function set_post_scan_results( $request ) {
441

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

446
                $post_id = (int) $request['id'];
4✔
447
                $post    = get_post( $post_id );
4✔
448
                if ( ! is_object( $post ) ) {
4✔
449

450
                        return new \WP_REST_Response( [ 'message' => 'The post is not valid.' ], 400 );
×
451
                }
452

453
                $post_type  = get_post_type( $post );
4✔
454
                $post_types = Settings::get_scannable_post_types();
4✔
455
                if ( empty( $post_types ) || ! in_array( $post_type, $post_types, true ) ) {
4✔
456

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

459
                }
460

461
                //phpcs:ignore Generic.Commenting.Todo.TaskFound
462
                // TODO: setup a rules class for loading/filtering rules.
463
                $rules             = edac_register_rules();
4✔
464
                $js_rule_ids       = [];
4✔
465
                $combined_rule_ids = [];
4✔
466
                foreach ( $rules as $rule ) {
4✔
467
                        if ( array_key_exists( 'ruleset', $rule ) && 'js' === $rule['ruleset'] ) {
4✔
468
                                $js_rule_ids[] = $rule['slug'];
4✔
469

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

479
                try {
480

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

493
                        $violations = $request['violations'];
4✔
494

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

498
                        if ( is_array( $violations ) && count( $violations ) > 0 ) {
4✔
499

500
                                foreach ( $violations as $violation ) {
4✔
501
                                        $rule_id = $violation['ruleId'];
4✔
502

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

506
                                        if ( in_array( $actual_rule_id, $js_rule_ids, true ) ) {
4✔
507

508
                                                // This rule is one that we've included in our js ruleset.
509

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

513
                                                //phpcs:ignore Generic.Commenting.Todo.TaskFound
514
                                                // TODO: setup a rules class for loading/filtering rules.
515
                                                foreach ( $rules as $rule ) {
×
516
                                                        if ( $rule['slug'] === $actual_rule_id ) {
×
517
                                                                $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.
×
518
                                                        }
519
                                                }
520

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

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

537
                                                $landmark          = $violation['landmark'] ?? null;
×
538
                                                $landmark_selector = $violation['landmarkSelector'] ?? null;
×
539

540
                                                $selectors = [
×
541
                                                        'selector' => $violation['selector'] ?? [],
×
542
                                                        'ancestry' => $violation['ancestry'] ?? [],
×
543
                                                        'xpath'    => $violation['xpath'] ?? [],
×
544
                                                ];
×
545
                                                ( new Insert_Rule_Data() )->insert( $post, $actual_rule_id, $impact, $html, $landmark, $landmark_selector, $selectors );
×
546

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

560
                                        }
561
                                }
562
                        }
563

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

576
                        // remove corrected records.
577
                        edac_remove_corrected_posts( $post_id, $post->post_type, $pre = 2, 'js' );
4✔
578

579
                        // Save the density metrics before the summary is generated.
580
                        $metrics = $request['densityMetrics'] ?? [ 0, 0 ];
4✔
581
                        if ( is_array( $metrics ) && count( $metrics ) > 0 ) {
4✔
582
                                update_post_meta(
4✔
583
                                        $post_id,
4✔
584
                                        '_edac_density_data',
4✔
585
                                        [
4✔
586
                                                $metrics['elementCount'] ?? 0,
4✔
587
                                                $metrics['contentLength'] ?? 0,
4✔
588
                                        ]
4✔
589
                                );
4✔
590
                        }
591

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

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

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

609
                        return new \WP_REST_Response(
4✔
610
                                [
4✔
611
                                        'success'   => true,
4✔
612
                                        'id'        => $post_id,
4✔
613
                                        'timestamp' => time(),
4✔
614
                                ]
4✔
615
                        );
4✔
616

617
                } catch ( \Exception $ex ) {
×
618

619
                        return new \WP_REST_Response(
×
620
                                [
×
621
                                        'message' => $ex->getMessage(),
×
622
                                ],
×
623
                                500
×
624
                        );
×
625

626
                }
627
        }
628

629

630
        /**
631
         * REST handler that clears the cached stats about the scans
632
         *
633
         * @return \WP_REST_Response
634
         */
635
        public function clear_cached_scans_stats() {
636

637
                try {
638

639
                        // Clear the cache.
640
                        $scans_stats = new Scans_Stats();
×
641
                        $scans_stats->clear_cache();
×
642

643
                        // Prime the cache.
644
                        $scans_stats = new Scans_Stats();
×
645

646
                        return new \WP_REST_Response(
×
647
                                [
×
648
                                        'success' => true,
×
649
                                ]
×
650
                        );
×
651

652
                } catch ( \Exception $ex ) {
×
653

654
                        return new \WP_REST_Response(
×
655
                                [
×
656
                                        'message' => $ex->getMessage(),
×
657
                                ],
×
658
                                500
×
659
                        );
×
660

661
                }
662
        }
663

664
        /**
665
         * REST handler that gets stats about the scans
666
         *
667
         * @return \WP_REST_Response
668
         */
669
        public function get_scans_stats() {
670

671
                try {
672

673
                        $scans_stats = new Scans_Stats( 60 * 5 );
×
674
                        $stats       = $scans_stats->summary();
×
675

676
                        return new \WP_REST_Response(
×
677
                                [
×
678
                                        'success' => true,
×
679
                                        'stats'   => $stats,
×
680
                                ]
×
681
                        );
×
682

683
                } catch ( \Exception $ex ) {
×
684

685
                        return new \WP_REST_Response(
×
686
                                [
×
687
                                        'message' => $ex->getMessage(),
×
688
                                ],
×
689
                                500
×
690
                        );
×
691

692
                }
693
        }
694

695

696
        /**
697
         * REST handler that gets stats about the scans by post type
698
         *
699
         * @param WP_REST_Request $request The request passed from the REST call.
700
         *
701
         * @return \WP_REST_Response
702
         */
703
        public function get_scans_stats_by_post_type( $request ) {
704

705
                if ( ! isset( $request['slug'] ) ) {
×
706
                        return new \WP_REST_Response( [ 'message' => 'A required parameter is missing.' ], 400 );
×
707
                }
708

709
                try {
710

711
                        $post_type            = strval( $request['slug'] );
×
712
                        $scannable_post_types = Settings::get_scannable_post_types();
×
713

714
                        if ( in_array( $post_type, $scannable_post_types, true ) ) {
×
715

716
                                $scans_stats = new Scans_Stats( 60 * 5 );
×
717
                                $by_type     = $scans_stats->issues_summary_by_post_type( $post_type );
×
718

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

737
        /**
738
         * REST handler that gets stats about the scans by post types
739
         *
740
         * @param WP_REST_Request $request The request passed from the REST call.
741
         *
742
         * @return \WP_REST_Response
743
         */
744
        public function get_scans_stats_by_post_types( $request ) { //phpcs:ignore
745

746
                try {
747

748
                        $scans_stats = new Scans_Stats( 60 * 5 );
×
749

750
                        $scannable_post_types = Settings::get_scannable_post_types();
×
751

752
                        $post_types = get_post_types(
×
753
                                [
×
754
                                        'public' => true,
×
755
                                ]
×
756
                        );
×
757
                        unset( $post_types['attachment'] );
×
758

759
                        $post_types_to_check = array_merge( [ 'post', 'page' ], $scannable_post_types );
×
760

761
                        $by_types = [];
×
762

763
                        foreach ( $post_types as $post_type ) {
×
764

765
                                $by_types[ $post_type ] = false;
×
766
                                if ( in_array( $post_type, $scannable_post_types, true ) && in_array( $post_type, $post_types_to_check, true ) ) {
×
767
                                        $by_types[ $post_type ] = $scans_stats->issues_summary_by_post_type( $post_type );
×
768
                                }
769
                        }
770

771
                        return new \WP_REST_Response(
×
772
                                [
×
773
                                        'success' => true,
×
774
                                        'stats'   => $by_types,
×
775
                                ]
×
776
                        );
×
777

778
                } catch ( \Exception $ex ) {
×
779

780
                        return new \WP_REST_Response(
×
781
                                [
×
782
                                        'message' => $ex->getMessage(),
×
783
                                ],
×
784
                                500
×
785
                        );
×
786

787
                }
788
        }
789

790
        /**
791
         * REST handler that gets stats about the scans
792
         *
793
         * @param \WP_REST_Request $request The request passed from the REST call.
794
         *
795
         * @return \WP_REST_Response
796
         */
797
        public function get_site_summary( \WP_REST_Request $request ) {
798

799
                try {
800
                        $scan_stats = new Scans_Stats();
×
801
                        if ( (bool) $request->get_param( 'clearCache' ) ) {
×
802
                                $scan_stats->clear_cache();
×
803
                        }
804

805
                        return new \WP_REST_Response(
×
806
                                [
×
807
                                        'success' => true,
×
808
                                        'stats'   => $scan_stats->summary(),
×
809
                                ]
×
810
                        );
×
811
                } catch ( \Exception $ex ) {
×
812
                        return new \WP_REST_Response(
×
813
                                [
×
814
                                        'message' => $ex->getMessage(),
×
815
                                ],
×
816
                                500
×
817
                        );
×
818
                }
819
        }
820

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

833
                try {
834
                        $data = [
8✔
835
                                'post_id'     => $post_id,
8✔
836
                                'summary'     => $this->get_summary_data( $post_id ),
8✔
837
                                'details'     => $this->get_details_data( $post_id ),
8✔
838
                                'readability' => $this->get_readability_data( $post_id ),
8✔
839
                        ];
8✔
840

841
                        return new \WP_REST_Response(
8✔
842
                                [
8✔
843
                                        'success' => true,
8✔
844
                                        'data'    => $data,
8✔
845
                                ],
8✔
846
                                200
8✔
847
                        );
8✔
848
                } catch ( \Exception $ex ) {
×
849
                        return new \WP_REST_Response(
×
850
                                [
×
851
                                        'success' => false,
×
852
                                        'message' => $ex->getMessage(),
×
853
                                ],
×
854
                                500
×
855
                        );
×
856
                }
857
        }
858

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

874
                // If summary doesn't exist or is invalid, return defaults.
875
                if ( ! $summary || ! is_array( $summary ) ) {
12✔
876
                        $summary = [
10✔
877
                                'passed_tests'    => 0,
10✔
878
                                'errors'          => 0,
10✔
879
                                'contrast_errors' => 0,
10✔
880
                                'warnings'        => 0,
10✔
881
                                'ignored'         => 0,
10✔
882
                                'readability'     => 0,
10✔
883
                        ];
10✔
884
                }
885

886
                return $summary;
12✔
887
        }
888

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

904
                if ( ! $table_name ) {
10✔
905
                        throw new \Exception( esc_html__( 'Invalid table name', 'accessibility-checker' ) );
×
906
                }
907

908
                $rules = edac_register_rules();
10✔
909
                if ( ! $rules ) {
10✔
910
                        return [
×
911
                                'errors'   => [],
×
912
                                'warnings' => [],
×
913
                                'passed'   => [],
×
914
                        ];
×
915
                }
916

917
                if ( ! current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ) ) {
10✔
918
                        foreach ( $rules as $rule_key => $rule ) {
2✔
919
                                if ( isset( $rule['fixes'] ) ) {
2✔
920
                                        unset( $rules[ $rule_key ]['fixes'] );
2✔
921
                                }
922
                        }
923
                }
924

925
                // If ANWW is active remove link_blank for details.
926
                if ( defined( 'ANWW_VERSION' ) ) {
10✔
927
                        $rules = edac_remove_element_with_value( $rules, 'slug', 'link_blank' );
×
928
                }
929

930
                $passed_rules  = [];
10✔
931
                $error_rules   = edac_filter_by_value( $rules, 'rule_type', 'error' );
10✔
932
                $warning_rules = edac_filter_by_value( $rules, 'rule_type', 'warning' );
10✔
933

934
                // Process both error and warning rules.
935
                $error_rules   = $this->process_rules_for_details( $error_rules, $post_id, $table_name, $siteid, $passed_rules );
10✔
936
                $warning_rules = $this->process_rules_for_details( $warning_rules, $post_id, $table_name, $siteid, $passed_rules );
10✔
937

938
                return [
10✔
939
                        'errors'   => array_values( $error_rules ),
10✔
940
                        'warnings' => array_values( $warning_rules ),
10✔
941
                        'passed'   => $passed_rules,
10✔
942
                ];
10✔
943
        }
944

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

962
                // Early return if no rules to process.
963
                if ( empty( $rules ) ) {
10✔
964
                        return $rules;
×
965
                }
966

967
                // Extract rule slugs for IN clause.
968
                $rule_slugs = array_column( $rules, 'slug' );
10✔
969

970
                // Build a simple, escaped IN clause.
971
                $safe_table    = esc_sql( $table_name );
10✔
972
                $escaped_slugs = array_map( 'esc_sql', $rule_slugs );
10✔
973
                $in_clause     = "'" . implode( "','", $escaped_slugs ) . "'";
10✔
974

975
                // Direct SQL query (table and values already escaped).
976
                $sql = "SELECT *\n"
10✔
977
                        . "FROM `{$safe_table}`\n"
10✔
978
                        . "WHERE postid = {$post_id}\n"
10✔
979
                        . "AND rule IN ( {$in_clause} )\n"
10✔
980
                        . "AND siteid = {$siteid}";
10✔
981

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

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

1004
                // Process each rule with its results.
1005
                foreach ( $rules as $key => $rule ) {
10✔
1006
                        $rule_slug = $rule['slug'];
10✔
1007
                        $results   = $results_by_rule[ $rule_slug ] ?? [];
10✔
1008
                        $count     = count( $results );
10✔
1009

1010
                        if ( $count ) {
10✔
1011
                                $rules[ $key ]['count']   = $count;
2✔
1012
                                $rules[ $key ]['details'] = $results;
2✔
1013
                                // Add WCAG URL based on wcag number.
1014
                                if ( isset( $rule['wcag'] ) ) {
2✔
1015
                                        $rules[ $key ] += $this->get_wcag_url_and_title_from_number( $rule['wcag'] );
2✔
1016
                                }
1017
                        } else {
1018
                                $rule['count']  = 0;
10✔
1019
                                $passed_rules[] = $rule;
10✔
1020
                                unset( $rules[ $key ] );
10✔
1021
                        }
1022
                }
1023

1024
                return $rules;
10✔
1025
        }
1026

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

1041
                $content_post = get_post( $post_id );
8✔
1042
                if ( ! $content_post ) {
8✔
1043
                        throw new \Exception( esc_html__( 'Post not found', 'accessibility-checker' ) );
×
1044
                }
1045

1046
                $content = $content_post->post_content;
8✔
1047
                $content = apply_filters( 'the_content', $content );
8✔
1048

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

1061
                // Get readability metadata.
1062
                $edac_summary           = get_post_meta( $post_id, '_edac_summary', true );
8✔
1063
                $post_grade_readability = isset( $edac_summary['readability'] ) ? $edac_summary['readability'] : 0;
8✔
1064
                $post_grade             = (int) filter_var( $post_grade_readability, FILTER_SANITIZE_NUMBER_INT );
8✔
1065
                $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✔
1066

1067
                $simplified_summary_grade = 0;
8✔
1068
                if ( class_exists( 'DaveChild\TextStatistics\TextStatistics' ) ) {
8✔
1069
                        $text_statistics          = new \DaveChild\TextStatistics\TextStatistics();
8✔
1070
                        $simplified_summary_grade = (int) floor( $text_statistics->fleschKincaidGradeLevel( $simplified_summary ) );
8✔
1071
                }
1072

1073
                $simplified_summary_grade_failed      = $simplified_summary_grade >= 9;
8✔
1074
                $simplified_summary_grade_readability = edac_ordinal( $simplified_summary_grade );
8✔
1075
                $simplified_summary_prompt            = get_option( 'edac_simplified_summary_prompt' );
8✔
1076

1077
                return [
8✔
1078
                        'post_grade'                           => $post_grade,
8✔
1079
                        'post_grade_readability'               => $post_grade_readability,
8✔
1080
                        'post_grade_failed'                    => $post_grade_failed,
8✔
1081
                        'simplified_summary'                   => $simplified_summary,
8✔
1082
                        'simplified_summary_grade'             => $simplified_summary_grade,
8✔
1083
                        'simplified_summary_grade_readability' => $simplified_summary_grade_readability,
8✔
1084
                        'simplified_summary_grade_failed'      => $simplified_summary_grade_failed,
8✔
1085
                        'simplified_summary_prompt'            => $simplified_summary_prompt,
8✔
1086
                        'simplified_summary_position'          => $simplified_summary_position,
8✔
1087
                        'content_length'                       => strlen( $content ),
8✔
1088
                ];
8✔
1089
        }
1090

1091
        /**
1092
         * Save simplified summary for a post.
1093
         *
1094
         * @since 1.xx.x
1095
         *
1096
         * @param \WP_REST_Request $request The REST request object.
1097
         * @return \WP_REST_Response|\WP_Error
1098
         */
1099
        public function save_simplified_summary( \WP_REST_Request $request ) {
1100
                $post_id = (int) $request['id'];
×
1101
                $summary = sanitize_textarea_field( wp_unslash( $request['summary'] ) );
×
1102

1103
                // Update the post meta with the simplified summary (matching AJAX behavior).
1104
                update_post_meta(
×
1105
                        $post_id,
×
1106
                        '_edac_simplified_summary',
×
1107
                        $summary
×
1108
                );
×
1109

1110
                // Get the complete readability data structure (same as the main readability endpoint).
1111
                try {
1112
                        $readability_data = $this->get_readability_data( $post_id );
×
1113

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

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

1152
                if ( ! $wcag_number ) {
2✔
1153
                        return $wcag_data_to_return;
×
1154
                }
1155

1156
                static $wcag_lookup = null;
2✔
1157

1158
                if ( null === $wcag_lookup ) {
2✔
1159
                        // Load the WCAG data file.
1160
                        $wcag_file = EDAC_PLUGIN_DIR . 'includes/wcag.php';
2✔
1161
                        if ( ! file_exists( $wcag_file ) ) {
2✔
1162
                                $wcag_lookup = [];
×
1163
                                return $wcag_data_to_return;
×
1164
                        }
1165

1166
                        $wcag_data = include $wcag_file;
2✔
1167
                        if ( ! is_array( $wcag_data ) ) {
2✔
1168
                                $wcag_lookup = [];
×
1169
                                return $wcag_data_to_return;
×
1170
                        }
1171

1172
                        // Re-key the array by WCAG number for O(1) lookups.
1173
                        $wcag_lookup = array_column( $wcag_data, null, 'number' );
2✔
1174
                }
1175

1176
                // O(1) lookup by WCAG number.
1177
                if ( isset( $wcag_lookup[ $wcag_number ] ) ) {
2✔
1178
                        $entry               = $wcag_lookup[ $wcag_number ];
2✔
1179
                        $wcag_data_to_return = [
2✔
1180
                                'wcag_title' => $entry['title'] ?? '',
2✔
1181
                                'wcag_url'   => $entry['wcag_url'] ?? '',
2✔
1182
                        ];
2✔
1183
                }
1184

1185
                return $wcag_data_to_return;
2✔
1186
        }
1187

1188
        /**
1189
         * REST handler for dismissing or restoring an issue.
1190
         *
1191
         * @param \WP_REST_Request $request The request object.
1192
         * @return \WP_REST_Response|\WP_Error Response object on success, WP_Error on failure.
1193
         */
1194
        public function dismiss_issue( $request ) {
1195
                global $wpdb;
×
1196

1197
                $issue_id      = (int) $request['issue_id'];
×
1198
                $action        = $request->get_param( 'action' );
×
1199
                $reason        = $request->get_param( 'reason' ) ?? '';
×
1200
                $comment       = $request->get_param( 'comment' ) ?? '';
×
1201
                $ignore_global = $request->get_param( 'ignore_global' ) ?? 0;
×
1202
                $large_batch   = $request->get_param( 'largeBatch' ) ?? false;
×
1203

1204
                $table_name = $wpdb->prefix . 'accessibility_checker';
×
1205
                $site_id    = get_current_blog_id();
×
1206

1207
                $allowed_ignore_actions = [ 'enable', 'ignore', 'dismiss' ];
×
1208
                // Set values based on action (matching AJAX endpoint behavior).
1209
                $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.
×
1210
                $ignre                = $is_ignoring ? 1 : 0;
×
1211
                $ignre_user           = $is_ignoring ? get_current_user_id() : null;
×
1212
                $ignre_user_info      = $is_ignoring ? get_userdata( $ignre_user ) : null;
×
1213
                $ignre_username       = $is_ignoring && $ignre_user_info ? $ignre_user_info->user_login : '';
×
1214
                $ignre_date           = $is_ignoring ? edac_get_current_utc_datetime() : null;
×
1215
                $ignre_date_formatted = $is_ignoring ? edac_format_datetime_from_utc( $ignre_date ) : '';
×
1216
                $ignre_reason         = $is_ignoring ? $reason : null;
×
1217
                $ignre_comment        = $is_ignoring ? $comment : null;
×
1218
                $ignre_global         = $is_ignoring ? (int) $ignore_global : 0;
×
1219

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

1227
                        if ( ! $object ) {
×
1228
                                return new \WP_Error(
×
1229
                                        'issue_not_found',
×
1230
                                        __( 'Issue not found.', 'accessibility-checker' ),
×
1231
                                        [ 'status' => 404 ]
×
1232
                                );
×
1233
                        }
1234

1235
                        // Update all issues with the same object.
1236
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update required, no caching needed.
1237
                        $result = $wpdb->query(
×
1238
                                $wpdb->prepare(
×
1239
                                        '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',
×
1240
                                        $table_name,
×
1241
                                        $ignre,
×
1242
                                        $ignre_user,
×
1243
                                        $ignre_date,
×
1244
                                        $ignre_reason,
×
1245
                                        $ignre_comment,
×
1246
                                        $ignre_global,
×
1247
                                        $site_id,
×
1248
                                        $object
×
1249
                                )
×
1250
                        );
×
1251
                } else {
1252
                        // Update single issue by ID.
1253
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update required, no caching needed.
1254
                        $result = $wpdb->query(
×
1255
                                $wpdb->prepare(
×
1256
                                        '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',
×
1257
                                        $table_name,
×
1258
                                        $ignre,
×
1259
                                        $ignre_user,
×
1260
                                        $ignre_date,
×
1261
                                        $ignre_reason,
×
1262
                                        $ignre_comment,
×
1263
                                        $ignre_global,
×
1264
                                        $site_id,
×
1265
                                        $issue_id
×
1266
                                )
×
1267
                        );
×
1268
                }
1269

1270
                if ( false === $result ) {
×
1271
                        return new \WP_Error(
×
1272
                                'database_error',
×
1273
                                __( 'Failed to update the issue.', 'accessibility-checker' ),
×
1274
                                [ 'status' => 500 ]
×
1275
                        );
×
1276
                }
1277

1278
                return new \WP_REST_Response(
×
1279
                        [
×
1280
                                'success'         => true,
×
1281
                                'issue_id'        => $issue_id,
×
1282
                                'action'          => $action,
×
1283
                                'ignre'           => $is_ignoring,
×
1284
                                'ignre_global'    => $ignre_global,
×
1285
                                'ignre_user'      => $ignre_user,
×
1286
                                'ignre_user_name' => $ignre_username,
×
1287
                                'ignre_date'      => $ignre_date_formatted,
×
1288
                                'ignre_reason'    => $ignre_reason,
×
1289
                                'ignre_comment'   => $ignre_comment,
×
1290
                                'large_batch'     => $large_batch,
×
1291
                        ],
×
1292
                        200
×
1293
                );
×
1294
        }
1295
}
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