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

equalizedigital / accessibility-checker / 13016226847

28 Jan 2025 05:35PM UTC coverage: 24.679%. First build
13016226847

Pull #843

github

web-flow
Merge 5b76a47bb into 96c2059cd
Pull Request #843: Release v1.19.0

2 of 28 new or added lines in 5 files covered. (7.14%)

1751 of 7095 relevant lines covered (24.68%)

5.54 hits per line

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

7.25
/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
        // apply filters to content.
135
        $content = edac_get_content( $post );
×
136

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

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

157
                update_option( 'edac_password_protected', true );
×
158
                return;
×
159
        } else {
160
                update_option( 'edac_password_protected', false );
×
161
        }
162
        delete_option( 'edac_password_protected' );
×
163

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

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

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

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

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

237
        // remove corrected records.
238
        edac_remove_corrected_posts( $post_ID, $post->post_type, $pre = 2, 'php' );
×
239

240
        // set post meta checked.
241
        update_post_meta( $post_ID, '_edac_post_checked', true );
×
242

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

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

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

284
        if ( 1 === $pre ) {
×
285

286
                // Set record flag before validating content.
287
                // 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.
288
                $wpdb->query(
×
289
                        $wpdb->prepare(
×
290
                                sprintf(
×
291
                                        "UPDATE {$wpdb->prefix}accessibility_checker SET recordcheck = %%d WHERE siteid = %%d and postid = %%d and type = %%s AND rule IN (%s)",
×
292
                                        implode( ',', array_fill( 0, count( $rule_slugs ), '%s' ) )
×
293
                                ),
×
294
                                array_merge(
×
295
                                        [ 0, get_current_blog_id(), $post_ID, $type ],
×
296
                                        $rule_slugs
×
297
                                )
×
298
                        )
×
299
                );
×
300

301
        } elseif ( 2 === $pre ) {
×
302
                // 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.
303
                $wpdb->query(
×
304
                        $wpdb->prepare(
×
305
                                sprintf(
×
306
                                        "DELETE FROM {$wpdb->prefix}accessibility_checker WHERE siteid = %%d and postid = %%d and type = %%s and recordcheck = %%d AND rule IN (%s)",
×
307
                                        implode( ',', array_fill( 0, count( $rule_slugs ), '%s' ) )
×
308
                                ),
×
309
                                array_merge(
×
310
                                        [ get_current_blog_id(), $post_ID, $type, 0 ],
×
311
                                        $rule_slugs
×
312
                                )
×
313
                        )
×
314
                );
×
315
        }
316
}
317

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

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

338
        $username = get_option( 'edacp_authorization_username' );
×
339
        $password = get_option( 'edacp_authorization_password' );
×
340

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

347
        $no_verify_ssl = false; // Verify by default.
×
348

349
        $is_local_loopback = get_option( 'edac_local_loopback', null );
×
350

351
        if ( null === $is_local_loopback ) {
×
352

353
                $parsed_url = wp_parse_url( home_url() );
×
354

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

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

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

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

386
        $parsed_url      = wp_parse_url( get_the_permalink( $post->ID ) );
×
387
        $parsed_site_url = wp_parse_url( get_site_url() );
×
388

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

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

396
                // set token if post status is 'draft' or 'pending'.
397
                if ( 'draft' === $post->post_status || 'pending' === $post->post_status ) {
×
398

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

402
                        // Add the token to the URL.
403
                        $url = add_query_arg( 'edac_token', $token, $url );
×
404

405
                }
406

407
                try {
408

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

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

426
                        // Write density data to post meta.
427
                        if ( $content['html'] ) {
×
428

429
                                $page_html         = $content['html']->save();
×
430
                                $body_density_data = edac_get_body_density_data( $page_html );
×
431

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

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

450
                $content['html'] = false;
×
451
        }
452

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

458
        // get styles and parse.
459
        if ( $content['html'] ) {
×
460

461
                $content['css'] = '';
×
462

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

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

476
                        $css_args['edac_cache'] = time();
×
477

478
                        if ( isset( $token ) ) {
×
479
                                $css_args['edac_token'] = $token;
×
480

481
                        }
482

483
                        // Add the query vars to the URL.
484
                        $stylesheet_url = add_query_arg(
×
485
                                $css_args,
×
486
                                $stylesheet_url
×
487
                        );
×
488

489
                        $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.
×
490

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

497
                $content['css_parsed'] = edac_parse_css( $content['css'] );
×
498
        }
499

500
        return $content;
×
501
}
502

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

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

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

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

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

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

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

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