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

equalizedigital / accessibility-checker / 13163549850

05 Feb 2025 05:55PM UTC coverage: 24.736%. First build
13163549850

Pull #850

github

web-flow
Merge 9a27bde64 into bb25d453d
Pull Request #850: Release v1.20.0

4 of 14 new or added lines in 5 files covered. (28.57%)

1755 of 7095 relevant lines covered (24.74%)

5.54 hits per line

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

7.21
/includes/validate.php
1
<?php
2
/**
3
 * Accessibility Checker pluign file.
4
 *
5
 * @package Accessibility_Checker
6
 */
7

8
use EDAC\Admin\Helpers;
9
use EDAC\Admin\Insert_Rule_Data;
10
use EDAC\Admin\Purge_Post_Data;
11

12
/**
13
 * Oxygen Builder on save
14
 *
15
 * @since 1.2.0
16
 *
17
 * @param int    $meta_id    The ID of the metadata entry in the database.
18
 * @param int    $post_id    The ID of the post being saved.
19
 * @param string $meta_key   The key of the metadata being saved.
20
 * @param mixed  $meta_value The value of the metadata being saved.
21
 *
22
 * @return void
23
 */
24
function edac_oxygen_builder_save_post( $meta_id, $post_id, $meta_key, $meta_value ) { // phpcs:ignore -- This function is a hook and the parameters are required.
25
        if ( 'ct_builder_shortcodes' === $meta_key ) {
×
26

27
                $post = get_post( $post_id, OBJECT );
×
28
                edac_validate( $post_id, $post, $action = 'save' );
×
29

30
        }
31
}
32

33
/**
34
 * Check if current post has been checked, if not check on page load
35
 *
36
 * @return void
37
 */
38
function edac_post_on_load() {
39
        global $pagenow;
×
40
        if ( 'post.php' === $pagenow ) {
×
41
                global $post;
×
42
                $checked = get_post_meta( $post->ID, '_edac_post_checked', true );
×
43
                if ( false === (bool) $checked ) {
×
44
                        edac_validate( $post->ID, $post, $action = 'load' );
×
45
                }
46
        }
47
}
48

49
/**
50
 * Post on save
51
 *
52
 * @param int    $post_ID The ID of the post being saved.
53
 * @param object $post    The post object being saved.
54
 * @param bool   $update  Whether this is an existing post being updated.
55
 *
56
 * @modified 1.10.0 to add a return when post_status is trash.
57
 *
58
 * @return void
59
 */
60
function edac_save_post( $post_ID, $post, $update ) {
61
        // check post type.
62
        $post_types = get_option( 'edac_post_types' );
58✔
63
        if ( is_array( $post_types ) && ! in_array( $post->post_type, $post_types, true ) ) {
58✔
64
                return;
×
65
        }
66

67
        // prevents first past of save_post due to meta boxes on post editor in gutenberg.
68
        if ( empty( $_POST ) ) {
58✔
69
                return;
50✔
70
        }
71

72
        // ignore revisions.
73
        if ( wp_is_post_revision( $post_ID ) ) {
8✔
74
                return;
2✔
75
        }
76

77
        // ignore autosaves.
78
        if ( wp_is_post_autosave( $post_ID ) ) {
8✔
79
                return;
×
80
        }
81

82
        // check if update.
83
        if ( ! $update ) {
8✔
84
                return;
8✔
85
        }
86

87
        // handle the case when the custom post is quick edited.
88
        if ( isset( $_POST['_inline_edit'] ) ) {
2✔
89
                $inline_edit = sanitize_text_field( $_POST['_inline_edit'] );
×
90
                if ( wp_verify_nonce( $inline_edit, 'inlineeditnonce' ) ) {
×
91
                        return;
×
92
                }
93
        }
94

95
        // Post in, or going to, trash.
96
        if ( 'trash' === $post->post_status ) {
2✔
97
                // Gutenberg does not fire the `wp_trash_post` action when moving posts to the
98
                // trash. Instead it uses `rest_delete_{$post_type}` which passes a different shape
99
                // so instead of hooking in there for every post type supported the data gets
100
                // purged here instead which produces the same result.
101
                Purge_Post_Data::delete_post( $post_ID );
2✔
102
                return;
2✔
103
        }
104

105
        edac_validate( $post_ID, $post, $action = 'save' );
×
106
}
107

108
/**
109
 * Post on save
110
 *
111
 * @param int    $post_ID The ID of the post being saved.
112
 * @param object $post    The post object being saved.
113
 * @param bool   $action  Whether this is an existing post being updated.
114
 *
115
 * @return void
116
 */
117
function edac_validate( $post_ID, $post, $action ) {
118
        // check post type.
119
        $post_types = get_option( 'edac_post_types' );
×
120
        if ( is_array( $post_types ) && ! in_array( $post->post_type, $post_types, true ) ) {
×
121
                return;
×
122
        }
123

124
        /**
125
         * Allows to hook in before the validation process starts for a post.
126
         *
127
         * @since 1.4.0
128
         *
129
         * @param int    $post_ID The ID of the post being saved.
130
         * @param string $action  The action being performed.
131
         */
132
        do_action( 'edac_before_validate', $post_ID, $action );
×
133
        
134
        // Ensure dynamic blocks and oEmbeds are processed before validation.
NEW
135
        $post->post_content = do_blocks( $post->post_content );
×
136

137
        // apply filters to content.
138
        $content = edac_get_content( $post );
×
139

140
        /**
141
         * Allows to hook in after the content has been retrieved for a post.
142
         *
143
         * @since 1.4.0
144
         *
145
         * @param int    $post_ID The ID of the post being saved.
146
         * @param array  $content The content being retrieved.
147
         * @param string $action  The action being performed.
148
         */
149
        do_action( 'edac_after_get_content', $post_ID, $content, $action );
×
150

151
        if ( ! $content['html'] ) {
×
152
                // The woocommerce checkout page will always be a redirect when it has no items. The redirect
153
                // will cause the content to be empty.
154
                // TEMPORARY FIX: Just return without setting this as a password protected page. In future we
155
                // will need to fix this properly by adding a product to the cart before checking.
156
                if ( edac_check_if_post_id_is_woocommerce_checkout_page( $post_ID ) ) {
×
157
                        return;
×
158
                }
159

160
                update_option( 'edac_password_protected', true );
×
161
                return;
×
162
        } else {
163
                update_option( 'edac_password_protected', false );
×
164
        }
165
        delete_option( 'edac_password_protected' );
×
166

167
        // set record check flag on previous error records.
168
        edac_remove_corrected_posts( $post_ID, $post->post_type, $pre = 1, 'php' );
×
169

170
        // check and validate content.
171
        $rules = edac_register_rules();
×
172
        if ( EDAC_DEBUG === true ) {
×
173
                $rule_performance_results = [];
×
174
                $all_rules_process_time   = microtime( true );
×
175
        }
176
        if ( $rules ) {
×
177
                foreach ( $rules as $rule ) {
×
178

179
                        // Run php-base rules.
180
                        if ( ( array_key_exists( 'ruleset', $rule ) && 'php' === $rule['ruleset'] ) ||
×
181
                                ( ! array_key_exists( 'ruleset', $rule ) && $rule['slug'] )
×
182
                        ) {
183
                                /**
184
                                 * Allows to hook in before the rule has been run on the content.
185
                                 *
186
                                 * @since 1.4.0
187
                                 *
188
                                 * @param int    $post_ID The ID of the post being saved.
189
                                 * @param array  $rule    The rule being validated against the content.
190
                                 * @param string $action  The action being performed.
191
                                 */
192
                                do_action( 'edac_before_rule', $post_ID, $rule, $action );
×
193
                                if ( EDAC_DEBUG === true ) {
×
194
                                        $rule_process_time = microtime( true );
×
195
                                }
196
                                $errors = call_user_func( 'edac_rule_' . $rule['slug'], $content, $post );
×
197

198
                                if ( $errors && is_array( $errors ) ) {
×
199
                                        /**
200
                                         * Allows to hook in after the rule has been and get the errors list.
201
                                         *
202
                                         * @since 1.4.0
203
                                         *
204
                                         * @param int    $post_ID The ID of the post being saved.
205
                                         * @param array  $rule    The rule being validated against the content.
206
                                         * @param array  $errors  The errors list generated by this rule from the content.
207
                                         * @param string $action  The action being performed.
208
                                         */
209
                                        do_action( 'edac_rule_errors', $post_ID, $rule, $errors, $action );
×
210
                                        foreach ( $errors as $error ) {
×
211
                                                ( new Insert_Rule_Data() )->insert( $post, $rule['slug'], $rule['rule_type'], $object = $error );
×
212
                                        }
213
                                }
214
                                if ( EDAC_DEBUG === true ) {
×
215
                                        $time_elapsed_secs                         = microtime( true ) - $rule_process_time;
×
216
                                        $rule_performance_results[ $rule['slug'] ] = $time_elapsed_secs;
×
217
                                }
218

219
                                /**
220
                                 * Allows to hook in after the rule has been run on the content.
221
                                 *
222
                                 * @since 1.4.0
223
                                 *
224
                                 * @param int    $post_ID The ID of the post being saved.
225
                                 * @param array  $rule    The rule being validated against the content.
226
                                 * @param string $action  The action being performed.
227
                                 */
228
                                do_action( 'edac_after_rule', $post_ID, $rule, $action );
×
229
                        }
230
                }
231
                if ( EDAC_DEBUG === true ) {
×
232
                        edacp_log( $rule_performance_results );
×
233
                }
234
        }
235
        if ( EDAC_DEBUG === true ) {
×
236
                $time_elapsed_secs = microtime( true ) - $all_rules_process_time;
×
237
                edacp_log( 'rules validate time: ' . $time_elapsed_secs );
×
238
        }
239

240
        // remove corrected records.
241
        edac_remove_corrected_posts( $post_ID, $post->post_type, $pre = 2, 'php' );
×
242

243
        // set post meta checked.
244
        update_post_meta( $post_ID, '_edac_post_checked', true );
×
245

246
        /**
247
         * Allows to hook in after the validation process has completed for a post.
248
         *
249
         * @since 1.4.0
250
         *
251
         * @param int    $post_ID The ID of the post being saved.
252
         * @param string $action  The action being performed.
253
         */
254
        do_action( 'edac_after_validate', $post_ID, $action );
×
255
}
256

257
/**
258
 * Remove corrected posts
259
 *
260
 * @param int    $post_ID The ID of the post.
261
 * @param string $type    The type of the post.
262
 * @param int    $pre     The flag indicating the removal stage (1 for before validation php based rules, 2 for after validation).
263
 * @param string $ruleset    The type of the ruleset to correct (php or js).
264
 *
265
 * @return void
266
 */
267
function edac_remove_corrected_posts( $post_ID, $type, $pre = 1, $ruleset = 'php' ) {
268
        global $wpdb;
×
269

270
        $rules          = edac_register_rules();
×
271
        $js_rule_slugs  = [];
×
272
        $php_rule_slugs = [];
×
273
        // Separate the JS rules and the PHP rules.
274
        foreach ( $rules as $rule ) {
×
275
                if ( isset( $rule['ruleset'] ) && 'js' === $rule['ruleset'] ) {
×
276
                        $js_rule_slugs[] = $rule['slug'];
×
277
                } else {
278
                        $php_rule_slugs[] = $rule['slug'];
×
279
                }
280
        }
281
        // Operate only on the slugs for the ruleset we are checking in this call.
282
        $rule_slugs = 'js' === $ruleset ? $js_rule_slugs : $php_rule_slugs;
×
283
        if ( 0 === count( $rule_slugs ) ) {
×
284
                return;
×
285
        }
286

287
        if ( 1 === $pre ) {
×
288

289
                // Set record flag before validating content.
290
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
291
                $wpdb->query(
×
292
                        $wpdb->prepare(
×
293
                                sprintf(
×
294
                                        "UPDATE {$wpdb->prefix}accessibility_checker SET recordcheck = %%d WHERE siteid = %%d and postid = %%d and type = %%s AND rule IN (%s)",
×
295
                                        implode( ',', array_fill( 0, count( $rule_slugs ), '%s' ) )
×
296
                                ),
×
297
                                array_merge(
×
298
                                        [ 0, get_current_blog_id(), $post_ID, $type ],
×
299
                                        $rule_slugs
×
300
                                )
×
301
                        )
×
302
                );
×
303

304
        } elseif ( 2 === $pre ) {
×
305
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Using direct query for adding data to database, caching not required for one time operation.
306
                $wpdb->query(
×
307
                        $wpdb->prepare(
×
308
                                sprintf(
×
309
                                        "DELETE FROM {$wpdb->prefix}accessibility_checker WHERE siteid = %%d and postid = %%d and type = %%s and recordcheck = %%d AND rule IN (%s)",
×
310
                                        implode( ',', array_fill( 0, count( $rule_slugs ), '%s' ) )
×
311
                                ),
×
312
                                array_merge(
×
313
                                        [ get_current_blog_id(), $post_ID, $type, 0 ],
×
314
                                        $rule_slugs
×
315
                                )
×
316
                        )
×
317
                );
×
318
        }
319
}
320

321
/**
322
 * Get content
323
 *
324
 * @param WP_Post $post The post object.
325
 * @return simple_html_dom|bool Returns the parsed HTML content or false on failure.
326
 */
327
function edac_get_content( $post ) {
328
        $content         = [];
×
329
        $content['html'] = false;
×
330

331
        $context              = '';
×
332
        $context_opts         = [];
×
333
        $default_context_opts = [
×
334
                // See: https://www.php.net/manual/en/context.http.php.
335
                'http' => [
×
336
                        'user_agent'      => 'PHP Accessibility Checker',
×
337
                        'follow_location' => false,
×
338
                ],
×
339
        ];
×
340

341
        $username = get_option( 'edacp_authorization_username' );
×
342
        $password = get_option( 'edacp_authorization_password' );
×
343

344
        // Check if server returns that the domain IP is a local/loopback address.
345
        // If so then file_get_contents calls from this server to this domain will
346
        // likely not be able to verify ssl. So we need to use a context that
347
        // does not try to validate the ssl, otherwise file_get_contents will fail.
348
        // See: https://www.php.net/manual/en/context.ssl.php .
349

350
        $no_verify_ssl = false; // Verify by default.
×
351

352
        $is_local_loopback = get_option( 'edac_local_loopback', null );
×
353

354
        if ( null === $is_local_loopback ) {
×
355

356
                $parsed_url = wp_parse_url( home_url() );
×
357

358
                if ( isset( $parsed_url['host'] ) ) {
×
359
                        $is_local_loopback = Helpers::is_domain_loopback( $parsed_url['host'] );
×
360
                        // can only be bool.
361
                        update_option( 'edac_local_loopback', $is_local_loopback );
×
362
                }
363
        }
364

365
        /**
366
         * Indicates file_get_html should not verify SSL.
367
         *
368
         * For site security it is not recommended to use this filter in production.
369
         *
370
         * @since 1.4.0
371
         *
372
         * @param bool $no_verify_ssl True if verify SSL should be disabled (as it must be in loopback connections), false if not.
373
         */
374
        $no_verify_ssl = apply_filters( 'edac_no_verify_ssl', $is_local_loopback );
×
375

376
        if ( $no_verify_ssl ) {
×
377
                $context_opts['ssl'] = [
×
378
                        'verify_peer'      => false,
×
379
                        'verify_peer_name' => false,
×
380
                ];
×
381
        }
382

383
        // http authorization.
384
        if ( is_plugin_active( 'accessibility-checker-pro/accessibility-checker-pro.php' ) && EDAC_KEY_VALID === true && $username && $password ) {
×
385
                // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- This is a valid use case for base64_encode.
386
                $context_opts['http']['header'] = 'Authorization: Basic ' . base64_encode( "$username:$password" );
×
387
        }
388

389
        $parsed_url      = wp_parse_url( get_the_permalink( $post->ID ) );
×
390
        $parsed_site_url = wp_parse_url( get_site_url() );
×
391

392
        // sanity check: confirm the permalink url is on this site.
393
        if ( $parsed_url['host'] === $parsed_site_url['host'] ) {
×
394

395
                $url = ( array_key_exists( 'query', $parsed_url ) && $parsed_url['query'] )
×
396
                        ? get_the_permalink( $post->ID ) . '&edac_cache=' . time()  // Permalink structure using a querystring.
×
397
                        : get_the_permalink( $post->ID ) . '?edac_cache=' . time(); // Permalink structure not using a querystring.
×
398

399
                // set token if post status is 'draft' or 'pending'.
400
                if ( 'draft' === $post->post_status || 'pending' === $post->post_status ) {
×
401

402
                        // Generate a token that is valid for a short period of time.
403
                        $token = edac_generate_nonce( 'draft-or-pending-status', 120 );
×
404

405
                        // Add the token to the URL.
406
                        $url = add_query_arg( 'edac_token', $token, $url );
×
407

408
                }
409

410
                try {
411

412
                        // setup the context for the request.
413
                        // note - if follow_location => false, permalinks that redirect (both offsite and on).
414
                        // will not be followed, so $content['html] will be false.
415
                        $merged_context_opts = array_merge_recursive( $default_context_opts, $context_opts );
×
416
                        $context             = stream_context_create( $merged_context_opts );
×
417

418
                        $dom             = file_get_html( $url, false, $context );
×
419
                        $content['html'] = edac_remove_elements(
×
420
                                $dom,
×
421
                                [
×
422
                                        '#wpadminbar',            // wp admin bar.
×
423
                                        '.edac-highlight-panel',  // frontend highlighter.
×
424
                                        '#query-monitor-main',    // query-monitor.
×
425
                                        '#qm-icon-container',     // query-monitor.
×
426
                                ]
×
427
                        );
×
428

429
                        // Write density data to post meta.
430
                        if ( $content['html'] ) {
×
431

432
                                $page_html         = $content['html']->save();
×
433
                                $body_density_data = edac_get_body_density_data( $page_html );
×
434

435
                                if ( false !== $body_density_data ) {
×
436
                                        update_post_meta(
×
437
                                                $post->ID,
×
438
                                                '_edac_density_data',
×
439
                                                array_map( 'intval', $body_density_data )
×
440
                                        );
×
441
                                } else {
442
                                        delete_post_meta( $post->ID, '_edac_density_data' );
×
443
                                }
444
                        }
445
                } catch ( Exception $e ) {
×
446
                        update_post_meta( $post->ID, '_edac_density_data', [ 0, 0 ] );
×
447

448
                        $content['html'] = false;
×
449
                }
450
        } else {
451
                update_post_meta( $post->ID, '_edac_density_data', [ 0, 0 ] );
×
452

453
                $content['html'] = false;
×
454
        }
455

456
        // check for restricted access plugin.
457
        if ( ! is_plugin_active( 'accessibility-checker-pro/accessibility-checker-pro.php' ) && is_plugin_active( 'restricted-site-access/restricted_site_access.php' ) ) {
×
458
                $content['html'] = false;
×
459
        }
460

461
        // get styles and parse.
462
        if ( $content['html'] ) {
×
463

464
                $content['css'] = '';
×
465

466
                // css from style tags.
467
                $style_tag_styles = $content['html']->find( 'style' );
×
468
                if ( $style_tag_styles ) {
×
469
                        foreach ( $style_tag_styles as $style ) {
×
470
                                $content['css'] .= $style->innertext;
×
471
                        }
472
                }
473

474
                // css from files.
475
                $style_files = $content['html']->find( 'link[rel="stylesheet"]' );
×
476
                foreach ( $style_files as $stylesheet ) {
×
477
                        $stylesheet_url = $stylesheet->href;
×
478

479
                        $css_args['edac_cache'] = time();
×
480

481
                        if ( isset( $token ) ) {
×
482
                                $css_args['edac_token'] = $token;
×
483

484
                        }
485

486
                        // Add the query vars to the URL.
487
                        $stylesheet_url = add_query_arg(
×
488
                                $css_args,
×
489
                                $stylesheet_url
×
490
                        );
×
491

492
                        $response = wp_remote_get( $stylesheet_url ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get -- This is a valid use case for wp_remote_get as plugin can be used on environments other than WPVIP.
×
493

494
                        if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
×
495
                                $styles          = wp_remote_retrieve_body( $response );
×
496
                                $content['css'] .= $styles;
×
497
                        }
498
                }
499

500
                $content['css_parsed'] = edac_parse_css( $content['css'] );
×
501
        }
502

503
        return $content;
×
504
}
505

506
/**
507
 * Show draft posts.
508
 *
509
 * This function alters the main query on the front-end to show draft and pending posts when a specific
510
 * token is present in the URL. This token is stored as an option in the database and is regenerated every time
511
 * it's used, to prevent unauthorized access to drafts and pending posts.
512
 *
513
 * @param WP_Query $query The WP_Query instance (passed by reference).
514
 */
515
function edac_show_draft_posts( $query ) {
516

517
        // Do not run if it's not the main query.
518
        if ( ! $query->is_main_query() ) {
58✔
519
                return;
58✔
520
        }
521

522
        // Do not run on admin pages, feeds, REST API or AJAX calls.
523
        if ( is_admin() || is_feed() || wp_doing_ajax() || ( function_exists( 'rest_doing_request' ) && rest_doing_request() ) ) {
×
524
                return;
×
525
        }
526

527
        // Do not run if the query variable 'edac_cache' is not set.
528
        // phpcs:ignore WordPress.Security.NonceVerification
529
        $url_cache = isset( $_GET['edac_cache'] ) ? sanitize_text_field( $_GET['edac_cache'] ) : '';
×
530
        if ( ! $url_cache ) {
×
531
                return;
×
532
        }
533

534
        // Retrieve the token from the URL.
535
        // phpcs:ignore WordPress.Security.NonceVerification
536
        $url_token = isset( $_GET['edac_token'] ) ? sanitize_text_field( $_GET['edac_token'] ) : false;
×
537

538
        // If the token is not set we do nothing and return early.
539
        if ( false === $url_token ) {
×
540
                return;
×
541
        }
542

543
        // If the passed token is no longer valid, we do nothing and return early.
544
        if ( false === edac_is_valid_nonce( 'draft-or-pending-status', $url_token ) ) {
×
545
                return;
×
546
        }
547

548
        // If we've reached this point, alter the query to include 'publish', 'draft', and 'pending' posts.
549
        $query->set( 'post_status', [ 'publish', 'draft', 'pending' ] );
×
550
}
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