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

equalizedigital / accessibility-checker / 19966336216

05 Dec 2025 02:40PM UTC coverage: 57.464%. First build
19966336216

Pull #1298

github

web-flow
Merge 10430a6a3 into fb04bca5f
Pull Request #1298: Release v1.35.0

65 of 108 new or added lines in 10 files covered. (60.19%)

4173 of 7262 relevant lines covered (57.46%)

3.44 hits per line

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

62.85
/includes/helper-functions.php
1
<?php
2
/**
3
 * Accessibility Checker plugin file.
4
 *
5
 * @package Accessibility_Checker
6
 */
7

8
use EDAC\Admin\Settings;
9

10
/**
11
 * Compare strings
12
 *
13
 * @param string $string1 String to compare.
14
 * @param string $string2 String to compare.
15
 * @return boolean
16
 */
17
function edac_compare_strings( $string1, $string2 ) {
18
        /**
19
         * Prepare strings for our comparison.
20
         *
21
         * @param string $content String to prepare.
22
         * @return string
23
         */
24
        $prepare_strings = function ( $content ) {
12✔
25
                // Text to remove.
26
                $remove_text = [
12✔
27
                        __( 'permalink of ', 'accessibility-checker' ),
12✔
28
                        __( 'permalink to ', 'accessibility-checker' ),
12✔
29
                        __( '&nbsp;', 'accessibility-checker' ),
12✔
30
                ];
12✔
31

32
                $content = strtolower( $content );
12✔
33
                $content = str_ireplace( $remove_text, '', $content );
12✔
34
                $content = wp_strip_all_tags( $content );
12✔
35
                $content = trim( $content, " \t\n\r\0\x0B\xC2\xA0" );
12✔
36

37
                return html_entity_decode( $content, ENT_QUOTES | ENT_HTML5 );
12✔
38
        };
12✔
39

40
        return $prepare_strings( $string1 ) === $prepare_strings( $string2 );
12✔
41
}
42

43
/**
44
 * Check if plugin is installed by getting all plugins from the plugins dir
45
 *
46
 * @param string $plugin_slug Slug of the plugin.
47
 * @return bool
48
 */
49
function edac_check_plugin_installed( $plugin_slug ) {
50
        $installed_plugins = get_plugins();
12✔
51

52
        return array_key_exists( $plugin_slug, $installed_plugins ) || in_array( $plugin_slug, $installed_plugins, true );
12✔
53
}
54

55
/**
56
 * Convert cardinal number into ordinal number
57
 *
58
 * @param int|string $number Number to make ordinal.
59
 * @return string
60
 */
61
function edac_ordinal( $number ) {
62

63
        $number = (int) $number;
34✔
64

65
        if ( class_exists( 'NumberFormatter' ) ) {
34✔
66
                return (
34✔
67
                        new NumberFormatter(
34✔
68
                                get_locale(),
34✔
69
                                NumberFormatter::ORDINAL
34✔
70
                        )
34✔
71
                )->format( $number );
34✔
72

73
        } else {
74
                if ( $number % 100 >= 11 && $number % 100 <= 13 ) {
×
75
                        $ordinal = $number . 'th';
×
76
                } else {
77
                        switch ( $number % 10 ) {
×
78
                                case 1:
×
79
                                        $ordinal = $number . 'st';
×
80
                                        break;
×
81
                                case 2:
×
82
                                        $ordinal = $number . 'nd';
×
83
                                        break;
×
84
                                case 3:
×
85
                                        $ordinal = $number . 'rd';
×
86
                                        break;
×
87
                                default:
88
                                        $ordinal = $number . 'th';
×
89
                                        break;
×
90
                        }
91
                }
92
                return $ordinal;
×
93

94
        }
95
}
96

97
/**
98
 * Remove element from multi-dimensional array
99
 *
100
 * @param array  $items The multi-dimensional array.
101
 * @param string $key The key of the element.
102
 * @param string $value The value of the element.
103
 * @return array
104
 */
105
function edac_remove_element_with_value( $items, $key, $value ) {
106
        foreach ( $items as $sub_key => $sub_array ) {
12✔
107
                if ( $sub_array[ $key ] === $value ) {
10✔
108
                        unset( $items[ $sub_key ] );
8✔
109
                }
110
        }
111
        return $items;
12✔
112
}
113

114
/**
115
 * Filter a multi-dimensional array
116
 *
117
 * @param array  $items The multi-dimensional array.
118
 * @param string $index The index of the element.
119
 * @param string $value The element value to match.
120
 * @return array
121
 */
122
function edac_filter_by_value( $items, $index, $value ) {
123
        if ( is_array( $items ) && count( $items ) > 0 ) {
14✔
124
                foreach ( array_keys( $items ) as $key ) {
12✔
125
                        $temp[ $key ] = $items[ $key ][ $index ];
12✔
126

127
                        if ( $temp[ $key ] === $value ) {
12✔
128
                                $newarray[ $key ] = $items[ $key ];
10✔
129
                        }
130
                }
131
        }
132

133
        if ( isset( $newarray ) && is_array( $newarray ) && count( $newarray ) ) {
14✔
134
                return array_values( $newarray );
10✔
135
        }
136
        return [];
4✔
137
}
138

139
/**
140
 * Get days plugin has been active
141
 *
142
 * @return int
143
 */
144
function edac_days_active() {
145
        $activation_date = get_option( 'edac_activation_date' );
24✔
146
        if ( $activation_date ) {
24✔
147
                $diff = strtotime( $activation_date ) - strtotime( gmdate( 'Y-m-d H:i:s' ) );
20✔
148
                return abs( round( $diff / 86400 ) );
20✔
149
        }
150
        return 0;
4✔
151
}
152

153
/**
154
 * Custom Post Types
155
 *
156
 * @return array
157
 */
158
function edac_custom_post_types() {
159
        $args = [
×
160
                'public'   => true,
×
161
                '_builtin' => false,
×
162
        ];
×
163

164
        $output   = 'names'; // names or objects, note names is the default.
×
165
        $operator = 'and'; // Options 'and' or 'or'.
×
166

167
        return get_post_types( $args, $output, $operator );
×
168
}
169

170
/**
171
 * Available Post Types
172
 *
173
 * @return array
174
 */
175
function edac_post_types() {
176
        /**
177
         * Filter the post types that the plugin will check.
178
         *
179
         * @since 1.4.0
180
         *
181
         * @param array $post_types post types.
182
         */
183
        $post_types = apply_filters( 'edac_filter_post_types', [ 'post', 'page' ] );
×
184

185
        // remove duplicates.
186
        $post_types = array_unique( $post_types );
×
187

188
        // validate post types.
189
        foreach ( $post_types as $key => $post_type ) {
×
190
                if ( ! post_type_exists( $post_type ) ) {
×
191
                        unset( $post_types[ $key ] );
×
192
                }
193
        }
194

195
        return $post_types;
×
196
}
197

198
/**
199
 * Retrieve a human readable post type label.
200
 *
201
 * @param string $post_type Post type slug.
202
 * @return string
203
 */
204
function edac_get_post_type_label( string $post_type ): string {
205
        $post_type = sanitize_key( (string) $post_type );
×
206

207
        if ( '' === $post_type ) {
×
208
                return '';
×
209
        }
210

211
        $post_type_object = get_post_type_object( $post_type );
×
212

213
        if ( $post_type_object instanceof \WP_Post_Type && ! empty( $post_type_object->labels->name ) ) {
×
214
                return $post_type_object->labels->name;
×
215
        }
216

217
        return ucfirst( $post_type );
×
218
}
219

220
/**
221
 * This function validates a table name against WordPress naming conventions and checks its existence in the database.
222
 *
223
 * The function first checks if the provided table name only contains alphanumeric characters, underscores, or hyphens.
224
 * If not, it returns null.
225
 *
226
 * After that, it checks if a table with that name actually exists in the database using the SHOW TABLES LIKE query.
227
 * If the table doesn't exist, it also returns null.
228
 *
229
 * If both checks are passed, it returns the valid table name.
230
 *
231
 * @param string $table_name The name of the table to be validated.
232
 *
233
 * @return string|null The validated table name, or null if the table name is invalid or the table does not exist.
234
 */
235
function edac_get_valid_table_name( $table_name ) {
236
        global $wpdb;
46✔
237
        static $found_table_name;
46✔
238

239
        if ( isset( $found_table_name ) ) {
46✔
240
                return $found_table_name;
44✔
241
        }
242

243
        // Check if table name only contains alphanumeric characters, underscores, or hyphens.
244
        if ( ! preg_match( '/^[a-zA-Z0-9_\-]+$/', $table_name ) ) {
2✔
245
                // Invalid table name.
246
                return null;
×
247
        }
248

249
        // Verify that the table actually exists in the database.
250
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
251
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
2✔
252
                // Table does not exist.
253
                return null;
×
254
        }
255

256
        $found_table_name = $table_name;
2✔
257
        return $found_table_name;
2✔
258
}
259

260
/**
261
 * Upcoming meetups in json format
262
 *
263
 * @param string  $meetup meetup name.
264
 * @param integer $count number of meetups to return.
265
 * @return array
266
 */
267
function edac_get_upcoming_meetups_json( $meetup, $count = 5 ) {
268

269
        if ( empty( $meetup ) || ! is_string( $meetup ) ) {
2✔
NEW
270
                return [];
×
271
        }
272

273
        // Min of 1 and max of 25.
274
        $count = absint( max( 1, min( 25, $count ) ) );
2✔
275

276
        // Sanitize meetup name for both cache key and GraphQL query to prevent injection.
277
        $sanitized_meetup = sanitize_title( $meetup );
2✔
278

279
        $key          = '_upcoming_meetups__' . $sanitized_meetup . '__' . (int) $count;
2✔
280
        $stale_key    = $key . '__stale';
2✔
281
        $cached_value = get_transient( $key );
2✔
282

283
        if ( false !== $cached_value ) {
2✔
NEW
284
                return is_array( $cached_value ) ? $cached_value : [];
×
285
        }
286

287
        $output = [];
2✔
288

289
        $request_uri = 'https://api.meetup.com/gql-ext';
2✔
290
        $query       = '
2✔
291
        query Group {
292
                groupByUrlname(urlname: "' . $sanitized_meetup . '") {
2✔
293
                        events(first: ' . (int) $count . ') {
2✔
294
                                totalCount
295
                                edges {
296
                                        node {
297
                                                dateTime
298
                                                eventUrl
299
                                                id
300
                                                title
301
                                        }
302
                                }
303
                        }
304
                }
305
        }';
2✔
306

307
        $request = wp_remote_post(
2✔
308
                $request_uri,
2✔
309
                [
2✔
310
                        'headers' => [
2✔
311
                                'Content-Type' => 'application/json',
2✔
312
                        ],
2✔
313
                        'timeout' => 10, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- Timeout set for external request.
2✔
314
                        'body'    => wp_json_encode(
2✔
315
                                [
2✔
316
                                        'query' => $query,
2✔
317
                                ]
2✔
318
                        ),
2✔
319
                ]
2✔
320
        );
2✔
321

322
        if ( ! is_wp_error( $request ) && 200 === (int) wp_remote_retrieve_response_code( $request ) ) {
2✔
323
                $response_body = json_decode( wp_remote_retrieve_body( $request ) );
2✔
324

325
                $edges = $response_body->data->groupByUrlname->events->edges ?? null;
2✔
326

327
                if ( is_array( $edges ) ) {
2✔
328
                        foreach ( $edges as $edge ) {
2✔
329
                                if ( ! isset( $edge->node ) ) {
2✔
NEW
330
                                        continue;
×
331
                                }
332

333
                                $event = $edge->node;
2✔
334

335
                                // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- GraphQL response uses camelCase.
336
                                if ( empty( $event->title ) || empty( $event->dateTime ) || empty( $event->eventUrl ) || empty( $event->id ) ) {
2✔
NEW
337
                                        continue;
×
338
                                }
339

340
                                $timestamp = strtotime( (string) $event->dateTime );
2✔
341
                                if ( false === $timestamp ) {
2✔
NEW
342
                                        continue;
×
343
                                }
344

345
                                $event_data       = new stdClass();
2✔
346
                                $event_data->name = (string) $event->title;
2✔
347
                                $event_data->time = $timestamp * 1000; // Convert to milliseconds to match old format.
2✔
348
                                $event_data->link = (string) $event->eventUrl;
2✔
349
                                $event_data->id   = (string) $event->id;
2✔
350
                                // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase.
351

352
                                $output[] = $event_data;
2✔
353
                        }
354
                }
355
        }
356

357
        if ( ! empty( $output ) ) {
2✔
358
                set_transient( $key, $output, DAY_IN_SECONDS );
2✔
359
                update_option( $stale_key, $output, false );
2✔
360
                return $output;
2✔
361
        }
362

NEW
363
        $stale_value = get_option( $stale_key );
×
NEW
364
        if ( is_array( $stale_value ) && ! empty( $stale_value ) ) {
×
365
                // Serve stale data for a short window while retrying upstream requests periodically.
NEW
366
                set_transient( $key, $stale_value, HOUR_IN_SECONDS );
×
NEW
367
                return $stale_value;
×
368
        }
369

NEW
370
        return [];
×
371
}
372

373
/**
374
 * Upcoming meetups in html
375
 *
376
 * @param  string  $meetup meetup name.
377
 * @param  integer $count number of meetups to return.
378
 * @param  string  $heading heading level.
379
 * @return string
380
 */
381
function edac_get_upcoming_meetups_html( $meetup, $count = 5, $heading = '3' ) {
382

383
        $json = edac_get_upcoming_meetups_json( $meetup, $count );
2✔
384

385
        if ( empty( $json ) ) {
2✔
NEW
386
                return '';
×
387
        }
388

389
        $html = '<ul class="edac-upcoming-meetup-list">';
2✔
390

391
        foreach ( $json as $event ) {
2✔
392
                $link_text = esc_html__( 'Attend Free', 'accessibility-checker' );
2✔
393

394
                $html .= '
2✔
395
                <li class="edac-upcoming-meetup-item edac-mb-3">
396
                        <h' . esc_html( $heading ) . ' class="edac-upcoming-meetup-item-name">' . esc_html( $event->name ) . '</h' . esc_html( $heading ) . '>
2✔
397
                        <div class="edac-upcoming-meetup-item-time edac-timestamp-to-local">' . (string) ( (int) $event->time / 1000 ) . '</div>
2✔
398
                        <a aria-label="' . esc_attr( $link_text . ': ' . $event->name ) . '" class="edac-upcoming-meetup-item-link" href="' . esc_url( $event->link ) . '">' . $link_text . '</a>
2✔
399
                </li>';
2✔
400
        }
401

402
        $html .= '</ul>';
2✔
403

404
        return $html;
2✔
405
}
406

407
/**
408
 * Calculate the issue density
409
 *
410
 * @param  int $issue_count number of issues.
411
 * @param  int $element_count number of elements.
412
 * @param  int $content_length length of content.
413
 * @return int
414
 */
415
function edac_get_issue_density( $issue_count, $element_count, $content_length ) {
416

417
        if ( $element_count < 1 || $content_length < 1 ) {
4✔
418
                return 0;
4✔
419
        }
420

421
        $element_weight = .8;
×
422
        $content_weight = .2;
×
423

424
        $error_elements_percentage = $issue_count / $element_count;
×
425
        $error_content_percentage  = $issue_count / $content_length;
×
426

427
        $score = (
×
428
                ( $error_elements_percentage * $element_weight ) +
×
429
                ( $error_content_percentage * $content_weight )
×
430
        );
×
431

432
        return round( $score * 100, 2 );
×
433
}
434

435
/**
436
 * Get simplified summary
437
 *
438
 * @param integer $post Post ID.
439
 * @return void
440
 */
441
function edac_get_simplified_summary( $post = null ) {
442
        if ( null === $post ) {
×
443
                $post = get_the_ID();
×
444
        }
445

446
        if ( null === $post ) {
×
447
                return;
×
448
        }
449

450
        echo wp_kses_post(
×
451
                ( new \EDAC\Inc\Simplified_Summary() )->simplified_summary_markup( $post )
×
452
        );
×
453
}
454

455
/**
456
 * Get Post Count by available custom post types
457
 *
458
 * @return mixed
459
 */
460
function edac_get_posts_count() {
461

462
        $output = [];
×
463

464
        $post_types = Settings::get_scannable_post_types();
×
465
        if ( $post_types ) {
×
466
                foreach ( $post_types as $post_type ) {
×
467

468
                        $counts = wp_count_posts( $post_type );
×
469

470
                        if ( $counts ) {
×
471
                                foreach ( $counts as $key => $value ) {
×
472
                                        // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
473
                                        if ( 0 == $value ) {
×
474
                                                unset( $counts->{$key} );
×
475
                                        }
476
                                }
477
                        }
478

479
                        if ( $counts ) {
×
480
                                $array = [];
×
481
                                foreach ( $counts as $key => $value ) {
×
482
                                        $array[] = $key . ' = ' . $value;
×
483
                                }
484
                                if ( $array ) {
×
485
                                        $output[] = $post_type . ': ' . implode( ', ', $array );
×
486
                                }
487
                        }
488
                }
489
        }
490

491
        if ( $output ) {
×
492
                return implode( ', ', $output );
×
493
        }
494
        return false;
×
495
}
496

497
/**
498
 * Get Raw Global Error Count
499
 *
500
 * @return array
501
 */
502
function edac_get_error_count() {
503
        global $wpdb;
×
504

505
        // Define a unique cache key for our data.
506
        $cache_key     = 'edac_errors_' . get_current_blog_id();
×
507
        $stored_errors = wp_cache_get( $cache_key );
×
508

509
        // Check if the result exists in the cache.
510
        if ( false === $stored_errors ) {
×
511
                // If not, perform the database query.
512
                $table_name = $wpdb->prefix . 'accessibility_checker';
×
513

514
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
515
                $stored_errors = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT count(*) FROM %i WHERE siteid = %d AND ruletype = %s', $table_name, get_current_blog_id(), 'error' ) );
×
516

517
                // Save the result in the cache for future use.
518
                wp_cache_set( $cache_key, $stored_errors );
×
519
        }
520

521
        return $stored_errors;
×
522
}
523

524
/**
525
 * Get Raw Global Warning Count
526
 *
527
 * @return array Array of.
528
 */
529
function edac_get_warning_count() {
530
        global $wpdb;
×
531

532
        // Define a unique cache key for our data.
533
        $cache_key       = 'edac_warnings_' . get_current_blog_id();
×
534
        $stored_warnings = wp_cache_get( $cache_key );
×
535

536
        // Check if the result exists in the cache.
537
        if ( false === $stored_warnings ) {
×
538
                // If not, perform the database query.
539
                $table_name = $wpdb->prefix . 'accessibility_checker';
×
540

541
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
542
                $stored_warnings = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT count(*) FROM %i WHERE siteid = %d AND ruletype = %s', $table_name, get_current_blog_id(), 'warning' ) );
×
543

544
                // Save the result in the cache for future use.
545
                wp_cache_set( $cache_key, $stored_warnings );
×
546
        }
547

548
        return $stored_warnings;
×
549
}
550

551
/**
552
 * Get Database Table Count
553
 *
554
 * @param string $table Database table.
555
 * @return int
556
 */
557
function edac_database_table_count( $table ) {
558
        global $wpdb;
×
559

560
        // Create a unique cache key based on the table's name.
561
        $cache_key = 'edac_table_count_' . $table;
×
562

563
        // Try to get the count from the cache first.
564
        $count = wp_cache_get( $cache_key );
×
565

566
        if ( false === $count ) {
×
567
                // If the count is not in the cache, perform the database query.
568
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
569
                $count = $wpdb->get_var( $wpdb->prepare( 'SELECT count(*) FROM %i', $wpdb->prefix . $table ) );
×
570

571
                // Save the count to the cache for future use.
572
                wp_cache_set( $cache_key, $count );
×
573
        }
574

575
        return $count;
×
576
}
577

578
/**
579
 * Generate a summary statistic list item.
580
 *
581
 * @since 1.14.0
582
 *
583
 * @param string $item_class     The base CSS class for the list item.
584
 * @param int    $count     The count of items to display.
585
 * @param string $label      The translated label with count included.
586
 *
587
 * @return string The generated HTML list item.
588
 */
589
function edac_generate_summary_stat( string $item_class, int $count, string $label ): string {
590
        $has_error_class = ( $count > 0 ) ? ' has-errors' : '';
×
591

592
        return '
×
593
                <li class="edac-summary-stat ' . $item_class . $has_error_class . '" aria-label="' . $label . '">
×
594
                        <div class="edac-panel-number">
595
                                ' . $count . '
×
596
                        </div>
597
                        <div class="edac-panel-number-label">' . $label . '</div>
×
598
                </li>';
×
599
}
600

601
/**
602
 * Generate links to pro page with some params.
603
 *
604
 * @param array  $query_args A list of key value pairs to add as query vars to the link.
605
 * @param string $type The type of link to generate. Default is 'pro'.
606
 * @param array  $args Additional arguments to pass on the link.
607
 * @return string
608
 */
609
function edac_generate_link_type( $query_args = [], $type = 'pro', $args = [] ): string {
610

611
        if ( ! is_array( $query_args ) ) {
58✔
612
                $query_args = [];
×
613
        }
614

615
        if ( ! is_array( $args ) ) {
58✔
616
                $args = [];
×
617
        }
618

619
        $date_now        = new DateTime( gmdate( 'Y-m-d H:i:s' ) );
58✔
620
        $activation_date = new DateTime( get_option( 'edac_activation_date', gmdate( 'Y-m-d H:i:s' ) ) );
58✔
621
        $interval        = $date_now->diff( $activation_date );
58✔
622
        $days_active     = $interval->days;
58✔
623
        $query_defaults  = [
58✔
624
                'utm_source'       => 'accessibility-checker',
58✔
625
                'utm_medium'       => 'software',
58✔
626
                'utm_campaign'     => 'wordpress-general',
58✔
627
                'php_version'      => PHP_VERSION,
58✔
628
                'platform'         => 'wordpress',
58✔
629
                'platform_version' => $GLOBALS['wp_version'],
58✔
630
                'software'         => defined( 'EDACP_KEY_VALID' ) && EDACP_KEY_VALID ? 'pro' : 'free',
58✔
631
                'software_version' => defined( 'EDACP_VERSION' ) ? EDACP_VERSION : EDAC_VERSION,
58✔
632
                'days_active'      => $days_active,
58✔
633
        ];
58✔
634

635
        // Add the ref parameter if one is set via filter.
636
        $ref = apply_filters( 'edac_filter_generate_link_type_ref', '' );
58✔
637
        if ( ! empty( $ref ) && is_string( $ref ) ) {
58✔
638
                $query_args['ref'] = $ref;
×
639
        }
640

641
        $query_args = array_merge( $query_defaults, $query_args );
58✔
642

643
        switch ( $type ) {
644
                case 'help':
58✔
645
                        $base_link = trailingslashit( 'https://a11ychecker.com/help' . $args['help_id'] ?? '' );
×
646
                        break;
×
647
                case 'custom': // phpcs:ignore -- intentially only breaking inside the condition because if it's not set we want to hit default.
58✔
648
                        if ( $args['base_link'] ) {
42✔
649
                                $base_link = $args['base_link'];
42✔
650
                                break;
42✔
651
                        }
652
                case 'pro':
16✔
653
                default:
654
                        $base_link = 'https://equalizedigital.com/accessibility-checker/pricing/';
16✔
655
                        break;
16✔
656
        }
657
        return add_query_arg( $query_args, $base_link );
58✔
658
}
659

660
/**
661
 * Echo or return a link with some utms.
662
 *
663
 * This is just a simplified wrapper around `edac_generate_link_type` to generate a link with UTM parameters.
664
 *
665
 * @param string $base_url the base URL to which UTM parameters will be added.
666
 * @param string $campaign the UTM campaign name, optional.
667
 * @param string $content the UTM content name, optional.
668
 * @param bool   $directly_echo whether to echo the link or return it. Default is true.
669
 *
670
 * @return void|string
671
 */
672
function edac_link_wrapper( $base_url, $campaign = '', $content = '', $directly_echo = true ) {
673
        if ( empty( $base_url ) || ! is_string( $base_url ) ) {
42✔
674
                return;
×
675
        }
676

677
        $params = [];
42✔
678
        if ( ! empty( $campaign ) ) {
42✔
679
                $params['utm_campaign'] = $campaign;
42✔
680
        }
681

682
        if ( ! empty( $content ) ) {
42✔
683
                $params['utm_content'] = $content;
42✔
684
        }
685

686
        $link = edac_generate_link_type(
42✔
687
                $params,
42✔
688
                'custom',
42✔
689
                [ 'base_link' => $base_url ]
42✔
690
        );
42✔
691

692
        if ( ! $directly_echo ) {
42✔
693
                return $link;
40✔
694
        }
695

696
        echo esc_url( $link );
2✔
697
}
698

699
/**
700
 * Check if WooCommerce is enabled.
701
 *
702
 * This just checks for existence of the main WooCommerce function and class.
703
 *
704
 * @return bool
705
 */
706
function edac_is_woocommerce_enabled() {
707
        return function_exists( 'WC' ) && class_exists( 'WooCommerce' );
×
708
}
709

710
/**
711
 * Check if a given post id is the WooCommerce checkout page.
712
 *
713
 * @param int $post_id The post ID to check.
714
 * @return bool
715
 */
716
function edac_check_if_post_id_is_woocommerce_checkout_page( $post_id ) {
717
        if ( ! edac_is_woocommerce_enabled() ) {
×
718
                return false;
×
719
        }
720

721
        return wc_get_page_id( 'checkout' ) === $post_id;
×
722
}
723

724
/**
725
 * Parse HTML content to extract image or SVG elements
726
 *
727
 * @param string $html The HTML content to parse.
728
 * @return array Array containing 'img' (string) and 'svg' (string) keys.
729
 */
730
function edac_parse_html_for_media( $html ) {
731
        if ( empty( $html ) ) {
32✔
732
                return [
2✔
733
                        'img' => null,
2✔
734
                        'svg' => null,
2✔
735
                ];
2✔
736
        }
737

738
        // Decode HTML entities before processing.
739
        $decoded_html = html_entity_decode( $html, ENT_QUOTES | ENT_HTML5 );
30✔
740

741
        // Early return if no media tags found.
742
        if ( stripos( $decoded_html, '<img' ) === false && stripos( $decoded_html, '<svg' ) === false ) {
30✔
743
                return [
2✔
744
                        'img' => null,
2✔
745
                        'svg' => null,
2✔
746
                ];
2✔
747
        }
748

749
        // More specific img tag regex pattern.
750
        if ( preg_match( '/<img[^>]+src=([\'"])(.*?)\1[^>]*>/i', $decoded_html, $matches ) ) {
28✔
751
                return [
18✔
752
                        'img' => $matches[2] ?? null,
18✔
753
                        'svg' => null,
18✔
754
                ];
18✔
755
        }
756

757
        // SVG pattern remains the same.
758
        if ( preg_match( '/<svg[^>]*>.*?<\/svg>/is', $decoded_html, $matches ) ) {
10✔
759
                return [
8✔
760
                        'img' => null,
8✔
761
                        'svg' => $matches[0],
8✔
762
                ];
8✔
763
        }
764

765
        return [
2✔
766
                'img' => null,
2✔
767
                'svg' => null,
2✔
768
        ];
2✔
769
}
770

771
/**
772
 * Remove corrected posts
773
 *
774
 * @param int    $post_ID The ID of the post.
775
 * @param string $type    The type of the post.
776
 * @param int    $pre     The flag indicating the removal stage (1 for before validation php based rules, 2 for after validation).
777
 * @param string $ruleset    The type of the ruleset to correct (php or js). For backwards compatibility, defaults to 'php'.
778
 *
779
 * @return void
780
 */
781
function edac_remove_corrected_posts( $post_ID, $type, $pre = 1, $ruleset = 'php' ) {  // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $ruleset is for backwards compatibility.
782
        global $wpdb;
4✔
783

784
        $rules = edac_register_rules();
4✔
785

786
        if ( 0 === count( $rules ) ) {
4✔
787
                return;
×
788
        }
789

790
        $sql = 1 === $pre
4✔
791
                ? "UPDATE {$wpdb->prefix}accessibility_checker SET recordcheck = %d WHERE siteid = %d AND postid = %d AND type = %s"
4✔
792
                : "DELETE FROM {$wpdb->prefix}accessibility_checker WHERE recordcheck = %d AND siteid = %d AND postid = %d AND type = %s";
4✔
793

794
        // 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.
795
        $wpdb->query(
4✔
796
                $wpdb->prepare(
4✔
797
                        $sql, // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
4✔
798
                        0,
4✔
799
                        get_current_blog_id(),
4✔
800
                        $post_ID,
4✔
801
                        $type
4✔
802
                )
4✔
803
        );
4✔
804
}
805

806
/**
807
 * Generate a landmark link with proper URL and ARIA label
808
 *
809
 * @param string $landmark The landmark type (e.g., "header", "navigation", "main").
810
 * @param string $landmark_selector The CSS selector for the landmark.
811
 * @param int    $post_id The post ID to link to.
812
 * @param string $css_class Optional CSS class for the link. Default 'edac-details-rule-records-record-landmark-link'.
813
 * @param bool   $target_blank Whether to open link in new window. Default true.
814
 *
815
 * @return string The HTML for the landmark link or just the landmark text if no selector.
816
 */
817
function edac_generate_landmark_link( $landmark, $landmark_selector, $post_id, $css_class = 'edac-details-rule-records-record-landmark-link', $target_blank = true ) {
818
        if ( empty( $landmark ) ) {
64✔
819
                return '';
6✔
820
        }
821
        $landmark = ucwords( $landmark );
60✔
822
        $landmark = esc_html( $landmark );
60✔
823

824
        // If we have both landmark and selector, create a link.
825
        if ( ! empty( $landmark_selector ) ) {
60✔
826
                $link = apply_filters(
52✔
827
                        'edac_get_origin_url_for_virtual_page',
52✔
828
                        get_the_permalink( $post_id ),
52✔
829
                        $post_id
52✔
830
                );
52✔
831

832
                $landmark_url = add_query_arg(
52✔
833
                        [
52✔
834
                                'edac_landmark' => base64_encode( $landmark_selector ),
52✔
835
                                'edac_nonce'    => wp_create_nonce( 'edac_highlight' ),
52✔
836
                        ],
52✔
837
                        $link
52✔
838
                );
52✔
839

840
                // translators: %s is the landmark type (e.g., "Header", "Navigation", "Main").
841
                $landmark_aria_label = sprintf( __( 'View %s landmark on website, opens a new window', 'accessibility-checker' ), $landmark );
52✔
842

843
                $target_attr = $target_blank ? ' target="_blank"' : '';
52✔
844

845
                return sprintf(
52✔
846
                        '<a href="%s" class="%s"%s aria-label="%s">%s</a>',
52✔
847
                        esc_url( $landmark_url ),
52✔
848
                        esc_attr( $css_class ),
52✔
849
                        $target_attr,
52✔
850
                        esc_attr( $landmark_aria_label ),
52✔
851
                        $landmark
52✔
852
                );
52✔
853
        }
854

855
        // If we only have landmark text, return it formatted.
856
        return $landmark;
8✔
857
}
858

859
/**
860
 * Check if a post is a virtual page.
861
 *
862
 * This function checks if a post is a virtual page using the pro plugin's
863
 * VirtualItemType:POST_TYPE constant.
864
 *
865
 * @param int $post_id The post ID to check.
866
 * @return bool True if the post is a virtual page, false otherwise.
867
 */
868
function edac_is_virtual_page( $post_id ) {
869
        if ( class_exists( '\EqualizeDigital\AccessibilityCheckerPro\VirtualContent\PostType\VirtualItemType' ) ) {
20✔
870
                $post_type     = get_post_type( $post_id );
16✔
871
                $pro_post_type = \EqualizeDigital\AccessibilityCheckerPro\VirtualContent\PostType\VirtualItemType::POST_TYPE;
16✔
872
                return $pro_post_type === $post_type;
16✔
873
        }
874

875
        return false;
4✔
876
}
877

878
/**
879
 * Check if the Pro version of the plugin is active.
880
 *
881
 * @return bool True if Pro version is active, false otherwise.
882
 */
883
function edac_is_pro() {
884
        return defined( 'EDACP_VERSION' ) && defined( 'EDAC_KEY_VALID' ) && EDAC_KEY_VALID;
×
885
}
886

887
/**
888
 * Get current timestamp in UTC for database storage.
889
 *
890
 * @since 1.35.0
891
 * @return string MySQL datetime string in UTC.
892
 */
893
function edac_get_current_utc_datetime(): string {
NEW
894
        return gmdate( 'Y-m-d H:i:s' );
×
895
}
896

897
/**
898
 * Format a UTC datetime string for display in WordPress timezone.
899
 *
900
 * Uses the WordPress configured date and time format settings.
901
 *
902
 * @since 1.35.0
903
 * @param string $utc_datetime MySQL datetime string in UTC format.
904
 * @return string Formatted datetime string in WordPress timezone, or empty string if invalid.
905
 */
906
function edac_format_datetime_from_utc( string $utc_datetime ): string {
NEW
907
        if ( ! $utc_datetime || '0000-00-00 00:00:00' === $utc_datetime ) {
×
NEW
908
                return '';
×
909
        }
910

NEW
911
        $timestamp = strtotime( $utc_datetime . ' UTC' );
×
NEW
912
        if ( false === $timestamp ) {
×
NEW
913
                return '';
×
914
        }
915

NEW
916
        $date_format = get_option( 'date_format' );
×
NEW
917
        $time_format = get_option( 'time_format' );
×
NEW
918
        $format      = $date_format . ' ' . $time_format;
×
919

NEW
920
        return wp_date( $format, $timestamp );
×
921
}
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