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

mihdan / cyr2lat / #1044

08 Feb 2026 11:26AM UTC coverage: 62.72% (-24.9%) from 87.636%
#1044

push

php-coveralls

web-flow
Merge pull request #182 from mihdan/wc-local-attr

Wc local attr

12 of 20 new or added lines in 2 files covered. (60.0%)

4095 of 6529 relevant lines covered (62.72%)

1.28 hits per line

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

97.13
/src/php/Main.php
1
<?php
2
/**
3
 * Main class of the plugin.
4
 *
5
 * @package cyr-to-lat
6
 */
7

8
// phpcs:disable Generic.Commenting.DocComment.MissingShort
9
/** @noinspection PhpUndefinedNamespaceInspection */
10
/** @noinspection PhpUndefinedClassInspection */
11
// phpcs:enable Generic.Commenting.DocComment.MissingShort
12

13
namespace CyrToLat;
14

15
use Automattic\WooCommerce\Utilities\FeaturesUtil;
16
use CyrToLat\BackgroundProcesses\PostConversionProcess;
17
use CyrToLat\BackgroundProcesses\TermConversionProcess;
18
use CyrToLat\Settings\Converter as SettingsConverter;
19
use CyrToLat\Settings\SystemInfo as SettingsSystemInfo;
20
use CyrToLat\Settings\Tables as SettingsTables;
21
use Polylang;
22
use SitePress;
23
use WP_CLI;
24
use WP_Error;
25
use WP_Post;
26
use wpdb;
27
use Exception;
28
use CyrToLat\Settings\Settings;
29
use CyrToLat\Symfony\Polyfill\Mbstring\Mbstring;
30

31
/**
32
 * Class Main
33
 */
34
class Main {
35

36
        /**
37
         * Request type.
38
         *
39
         * @var Request
40
         */
41
        protected $request;
42

43
        /**
44
         * Plugin settings.
45
         *
46
         * @var Settings
47
         */
48
        protected $settings;
49

50
        /**
51
         * Process posts instance.
52
         *
53
         * @var PostConversionProcess
54
         */
55
        protected $process_all_posts;
56

57
        /**
58
         * Process terms instance.
59
         *
60
         * @var TermConversionProcess
61
         */
62
        protected $process_all_terms;
63

64
        /**
65
         * Admin Notices instance.
66
         *
67
         * @var AdminNotices
68
         */
69
        protected $admin_notices;
70

71
        /**
72
         * Converter instance.
73
         *
74
         * @var Converter
75
         */
76
        protected $converter;
77

78
        /**
79
         * WP_CLI instance.
80
         *
81
         * @var WPCli
82
         */
83
        protected $cli;
84

85
        /**
86
         * ACF instance.
87
         *
88
         * @var ACF
89
         */
90
        protected $acf;
91

92
        /**
93
         * Flag showing that we are processing a term.
94
         *
95
         * @var bool
96
         */
97
        private $is_term = false;
98

99
        /**
100
         * Taxonomies saved in pre_insert_term or get_terms_args filter.
101
         *
102
         * @var string[]|null
103
         */
104
        private $taxonomies;
105

106
        /**
107
         * Polylang locale.
108
         *
109
         * @var string
110
         */
111
        private $pll_locale;
112

113
        /**
114
         * WPML locale.
115
         *
116
         * @var string|null
117
         */
118
        protected $wpml_locale;
119

120
        /**
121
         * WPML languages.
122
         *
123
         * @var array
124
         */
125
        protected $wpml_languages;
126

127
        /**
128
         * Current request is frontend.
129
         *
130
         * @var bool|null
131
         */
132
        protected $is_frontend;
133

134
        /**
135
         * Init plugin.
136
         *
137
         * @return void
138
         */
139
        public function init(): void {
140
                add_action( 'plugins_loaded', [ $this, 'init_all' ], - PHP_INT_MAX );
1✔
141
        }
142

143
        /**
144
         * Init all plugin stuffs.
145
         *
146
         * @return void
147
         */
148
        public function init_all(): void {
149
                $this->load_textdomain();
1✔
150

151
                $this->init_multilingual();
1✔
152
                $this->init_classes();
1✔
153
                $this->init_cli();
1✔
154
                $this->init_hooks();
1✔
155
        }
156

157
        /**
158
         * Load plugin text domain.
159
         *
160
         * @return void
161
         */
162
        public function load_textdomain(): void {
163
                load_default_textdomain();
1✔
164
                load_plugin_textdomain(
1✔
165
                        'cyr2lat',
1✔
166
                        false,
1✔
167
                        dirname( plugin_basename( constant( 'CYR_TO_LAT_FILE' ) ) ) . '/languages/'
1✔
168
                );
1✔
169
        }
170

171
        /**
172
         * Init multilingual features.
173
         * It must be first in the init sequence, as we use defined filters internally in our classes.
174
         *
175
         * @return void
176
         */
177
        protected function init_multilingual(): void {
178
                if ( class_exists( Polylang::class ) ) {
4✔
179
                        add_filter( 'locale', [ $this, 'pll_locale_filter' ] );
2✔
180
                }
181

182
                if ( class_exists( SitePress::class ) ) {
4✔
183
                        $this->wpml_locale = $this->get_wpml_locale();
2✔
184

185
                        // We cannot use locale filter here
186
                        // as WPML reverts locale at PHP_INT_MAX in \WPML\ST\MO\Hooks\LanguageSwitch::filterLocale.
187
                        add_filter( 'ctl_locale', [ $this, 'wpml_locale_filter' ], - PHP_INT_MAX );
2✔
188

189
                        add_action( 'wpml_language_has_switched', [ $this, 'wpml_language_has_switched' ], 10, 3 );
2✔
190
                }
191
        }
192

193
        /**
194
         * Init other classes.
195
         *
196
         * @return void
197
         */
198
        protected function init_classes(): void {
199
                ( new ErrorHandler() )->init();
1✔
200

201
                $this->request  = new Request();
1✔
202
                $this->settings = new Settings(
1✔
203
                        [
1✔
204
                                'Cyr To Lat' => [
1✔
205
                                        SettingsTables::class,
1✔
206
                                        SettingsConverter::class,
1✔
207
                                        SettingsSystemInfo::class,
1✔
208
                                ],
1✔
209
                        ]
1✔
210
                );
1✔
211

212
                $this->admin_notices = new AdminNotices();
1✔
213
                $requirements        = new Requirements( $this->settings, $this->admin_notices );
1✔
214

215
                if ( ! $requirements->are_requirements_met() ) {
1✔
216
                        return;
1✔
217
                }
218

219
                $this->process_all_posts = new PostConversionProcess( $this );
1✔
220
                $this->process_all_terms = new TermConversionProcess( $this );
1✔
221
                $this->converter         = new Converter(
1✔
222
                        $this,
1✔
223
                        $this->settings,
1✔
224
                        $this->process_all_posts,
1✔
225
                        $this->process_all_terms,
1✔
226
                        $this->admin_notices
1✔
227
                );
1✔
228

229
                $this->acf         = new ACF( $this->settings );
1✔
230
                $this->is_frontend = $this->request->is_frontend();
1✔
231
        }
232

233
        /**
234
         * Init in CLI mode.
235
         *
236
         * @return void
237
         */
238
        protected function init_cli(): void {
239
                if ( ! $this->request->is_cli() ) {
3✔
240
                        return;
1✔
241
                }
242

243
                $this->cli = new WPCli( $this->converter );
2✔
244

245
                try {
246
                        /**
247
                         * Method WP_CLI::add_command() accepts a class as callable.
248
                         *
249
                         * @noinspection PhpParamsInspection
250
                         */
251
                        WP_CLI::add_command( 'cyr2lat', $this->cli );
2✔
252
                } catch ( Exception $ex ) {
1✔
253
                        return;
1✔
254
                }
255
        }
256

257
        /**
258
         * Init hooks.
259
         */
260
        protected function init_hooks(): void {
261
                if ( $this->is_frontend ) {
5✔
262
                        add_action( 'woocommerce_before_template_part', [ $this, 'woocommerce_before_template_part_filter' ] );
2✔
263
                        add_action( 'woocommerce_after_template_part', [ $this, 'woocommerce_after_template_part_filter' ] );
2✔
264
                }
265

266
                if ( ! $this->request->is_allowed() ) {
5✔
267
                        return;
1✔
268
                }
269

270
                add_filter( 'sanitize_title', [ $this, 'sanitize_title' ], 9, 3 );
4✔
271
                add_filter( 'sanitize_file_name', [ $this, 'sanitize_filename' ], 10, 2 );
4✔
272
                add_filter( 'wp_insert_post_data', [ $this, 'sanitize_post_name' ], 10, 2 );
4✔
273
                add_filter( 'pre_insert_term', [ $this, 'pre_insert_term_filter' ], PHP_INT_MAX, 2 );
4✔
274
                add_filter( 'post_updated', [ $this, 'check_for_changed_slugs' ], 10, 3 );
4✔
275

276
                if ( ! $this->is_frontend || class_exists( SitePress::class ) ) {
4✔
277
                        add_filter( 'get_terms_args', [ $this, 'get_terms_args_filter' ], PHP_INT_MAX, 2 );
3✔
278
                }
279

280
                add_action( 'before_woocommerce_init', [ $this, 'declare_wc_compatibility' ] );
4✔
281
        }
282

283
        /**
284
         * Get Settings instance.
285
         *
286
         * @return Settings
287
         */
288
        public function settings(): Settings {
289
                return $this->settings;
1✔
290
        }
291

292
        /**
293
         * Sanitize title.
294
         *
295
         * @param string|mixed $title     Sanitized title.
296
         * @param string|mixed $raw_title The title prior to sanitization.
297
         * @param string|mixed $context   The context for which the title is being sanitized.
298
         *
299
         * @return string|mixed
300
         * @noinspection PhpUnusedParameterInspection
301
         * @noinspection PhpMissingReturnTypeInspection
302
         * @noinspection ReturnTypeCanBeDeclaredInspection
303
         */
304
        public function sanitize_title( $title, $raw_title = '', $context = '' ) {
305
                global $wpdb;
25✔
306

307
                if (
308
                        ! $title ||
25✔
309
                        // Fix the bug with `_wp_old_slug` redirect.
310
                        'query' === $context ||
23✔
311
                        ! $this->transliterate_on_pre_term_slug_filter( (string) $title )
25✔
312
                ) {
313
                        return $title;
4✔
314
                }
315

316
                $title = urldecode( (string) $title );
21✔
317
                $pre   = apply_filters( 'ctl_pre_sanitize_title', false, $title );
21✔
318

319
                if ( false !== $pre ) {
21✔
320
                        return $pre;
1✔
321
                }
322

323
                if ( $this->is_term ) {
20✔
324
                        // Make sure we search in the db only once being called from wp_insert_term().
325
                        $this->is_term = false;
5✔
326

327
                        // Fix a case when showing previously created categories in cyrillic with WPML.
328
                        if ( $this->is_frontend && class_exists( SitePress::class ) ) {
5✔
329
                                return $title;
1✔
330
                        }
331

332
                        $sql = $wpdb->prepare(
4✔
333
                                "SELECT slug FROM $wpdb->terms t LEFT JOIN $wpdb->term_taxonomy tt
4✔
334
                                                        ON t.term_id = tt.term_id
335
                                                        WHERE t.slug = %s",
4✔
336
                                rawurlencode( $title )
4✔
337
                        );
4✔
338

339
                        if ( $this->taxonomies ) {
4✔
340
                                $sql .= ' AND tt.taxonomy IN (' . $this->prepare_in( $this->taxonomies ) . ')';
3✔
341
                        }
342

343
                        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
344
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
345
                        $term = $wpdb->get_var( $sql );
4✔
346
                        // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
347

348
                        if ( ! empty( $term ) ) {
4✔
349
                                return $term;
4✔
350
                        }
351
                }
352

353
                return $this->is_wc_attribute( $title ) ? $title : $this->transliterate( $title );
19✔
354
        }
355

356
        /**
357
         * WC before template part filter.
358
         * Add the sanitize_title filter to support transliteration of WC attributes on the frontend.
359
         *
360
         * @return void
361
         */
362
        public function woocommerce_before_template_part_filter(): void {
363
                add_filter( 'sanitize_title', [ $this, 'sanitize_title' ], 9, 3 );
1✔
364
        }
365

366
        /**
367
         * WC after the template part filter.
368
         * Remove the sanitize_title filter after supporting transliteration of WC attributes on the frontend.
369
         *
370
         * @return void
371
         */
372
        public function woocommerce_after_template_part_filter(): void {
373
                remove_filter( 'sanitize_title', [ $this, 'sanitize_title' ], 9 );
1✔
374
        }
375

376
        /**
377
         * Check if title is an attribute taxonomy.
378
         *
379
         * @param string $title Title.
380
         *
381
         * @return bool
382
         * @noinspection PhpUndefinedFunctionInspection
383
         */
384
        protected function is_wc_attribute_taxonomy( string $title ): bool {
385
                $title = preg_replace( '/^pa_/', '', $title );
4✔
386

387
                foreach ( wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
4✔
388
                        if ( $title === $attribute_taxonomy->attribute_name ) {
3✔
389
                                return true;
2✔
390
                        }
391
                }
392

393
                return false;
2✔
394
        }
395

396
        /**
397
         * Check if the title is a local attribute.
398
         *
399
         * @param string $title Title.
400
         *
401
         * @return bool
402
         */
403
        protected function is_local_attribute( string $title ): bool {
404
                // Global attribute.
405
                if ( 0 === strpos( $title, 'pa_' ) ) {
2✔
NEW
406
                        return false;
×
407
                }
408

409
                // phpcs:disable WordPress.Security.NonceVerification.Missing
410
                $action = isset( $_POST['action'] )
2✔
NEW
411
                        ? sanitize_text_field( wp_unslash( $_POST['action'] ) )
×
412
                        : '';
2✔
413
                // phpcs:enable WordPress.Security.NonceVerification.Missing
414

415
                // Not the 'save attributes' action.
416
                if ( 'woocommerce_save_attributes' !== $action ) {
2✔
417
                        return false;
2✔
418
                }
419

420
                // phpcs:disable WordPress.Security.NonceVerification.Missing
NEW
421
                $data = isset( $_POST['data'] )
×
NEW
422
                        ? filter_input( INPUT_POST, 'data', FILTER_SANITIZE_URL )
×
NEW
423
                        : '';
×
424
                // phpcs:enable WordPress.Security.NonceVerification.Missing
425

NEW
426
                wp_parse_str( urldecode( $data ), $attributes );
×
427

NEW
428
                $attribute_names = $attributes['attribute_names'] ?? [];
×
429

NEW
430
                return in_array( $title, $attribute_names, true );
×
431
        }
432

433
        /**
434
         * Check if title is a product not converted attribute.
435
         *
436
         * @param string $title Title.
437
         *
438
         * @return bool
439
         * @noinspection PhpUndefinedFunctionInspection
440
         * @noinspection PhpUndefinedMethodInspection
441
         */
442
        protected function is_wc_product_not_converted_attribute( string $title ): bool {
443

444
                global $product;
6✔
445

446
                if ( ! is_a( $product, 'WC_Product' ) ) {
6✔
447
                        return false;
3✔
448
                }
449

450
                // We have to get attributes from postmeta here to see the converted slug.
451
                $attributes = (array) get_post_meta( $product->get_id(), '_product_attributes', true );
3✔
452

453
                foreach ( $attributes as $slug => $attribute ) {
3✔
454
                        $name = $attribute['name'] ?? '';
2✔
455

456
                        if ( $name === $title && sanitize_title_with_dashes( $title ) === $slug ) {
2✔
457
                                return true;
1✔
458
                        }
459
                }
460

461
                return false;
2✔
462
        }
463

464
        /**
465
         * Check if title is an attribute.
466
         *
467
         * @param string $title Title.
468
         *
469
         * @return bool
470
         * @noinspection PhpUndefinedFunctionInspection
471
         */
472
        protected function is_wc_attribute( string $title ): bool {
473
                if ( ! function_exists( 'WC' ) ) {
19✔
474
                        return false;
15✔
475
                }
476

477
                return (
4✔
478
                        $this->is_wc_attribute_taxonomy( $title ) ||
4✔
479
                        $this->is_local_attribute( $title ) ||
4✔
480
                        $this->is_wc_product_not_converted_attribute( $title )
4✔
481
                );
4✔
482
        }
483

484
        /**
485
         * Sanitize filename.
486
         *
487
         * @param string|mixed $filename     Sanitized filename.
488
         * @param string|mixed $filename_raw The filename prior to sanitization.
489
         *
490
         * @return string
491
         * @noinspection PhpUnusedParameterInspection
492
         * @noinspection PhpMissingReturnTypeInspection
493
         * @noinspection ReturnTypeCanBeDeclaredInspection
494
         */
495
        public function sanitize_filename( $filename, $filename_raw ) {
496
                global $wp_version;
8✔
497

498
                $pre = apply_filters( 'ctl_pre_sanitize_filename', false, $filename );
8✔
499

500
                if ( false !== $pre ) {
8✔
501
                        return (string) $pre;
1✔
502
                }
503

504
                $filename = (string) $filename;
7✔
505
                $is_utf8  = version_compare( (string) $wp_version, '6.9-RC1', '>=' ) ? 'wp_is_valid_utf8' : 'seems_utf8';
7✔
506

507
                if ( $is_utf8( $filename ) ) {
7✔
508
                        $filename = (string) Mbstring::mb_strtolower( $filename );
7✔
509
                }
510

511
                return $this->transliterate( $filename );
7✔
512
        }
513

514
        /**
515
         * Get min suffix.
516
         *
517
         * @return string
518
         */
519
        public function min_suffix(): string {
520
                return defined( 'SCRIPT_DEBUG' ) && constant( 'SCRIPT_DEBUG' ) ? '' : '.min';
4✔
521
        }
522

523
        /**
524
         * Fix string encoding on macOS.
525
         *
526
         * @param string $str   String.
527
         * @param array  $table Conversion table.
528
         *
529
         * @return string
530
         */
531
        private function fix_mac_string( string $str, array $table ): string {
532
                $fix_table = ConversionTables::get_fix_table_for_mac();
26✔
533

534
                $fix = [];
26✔
535
                foreach ( $fix_table as $key => $value ) {
26✔
536
                        if ( isset( $table[ $key ] ) ) {
26✔
537
                                $fix[ $value ] = $table[ $key ];
26✔
538
                        }
539
                }
540

541
                return strtr( $str, $fix );
26✔
542
        }
543

544
        /**
545
         * Split Chinese string by hyphens.
546
         *
547
         * @param string $str   String.
548
         * @param array  $table Conversion table.
549
         *
550
         * @return string
551
         */
552
        protected function split_chinese_string( string $str, array $table ): string {
553
                if ( ! $this->settings->is_chinese_locale() || mb_strlen( $str ) < 4 ) {
29✔
554
                        return $str;
27✔
555
                }
556

557
                $chars = Mbstring::mb_str_split( $str );
2✔
558
                $str   = '';
2✔
559

560
                foreach ( $chars as $char ) {
2✔
561
                        if ( isset( $table[ $char ] ) ) {
2✔
562
                                $str .= '-' . $char . '-';
2✔
563
                        } else {
564
                                $str .= $char;
1✔
565
                        }
566
                }
567

568
                return $str;
2✔
569
        }
570

571
        /**
572
         * Transliterate string using a table.
573
         *
574
         * @param string $str String.
575
         *
576
         * @return string
577
         */
578
        public function transliterate( string $str ): string {
579
                $table = (array) apply_filters( 'ctl_table', $this->settings->get_table() );
26✔
580

581
                $str = $this->fix_mac_string( $str, $table );
26✔
582
                $str = $this->split_chinese_string( $str, $table );
26✔
583

584
                return strtr( $str, $table );
26✔
585
        }
586

587
        /**
588
         * Check if the Block Editor is active.
589
         * Must only be used after the plugins_loaded action is fired.
590
         *
591
         * @return bool
592
         * @noinspection PhpUndefinedFunctionInspection
593
         */
594
        private function is_gutenberg_editor_active(): bool {
595
                // Gutenberg plugin is installed and activated.
596
                // This filter was removed in WP 5.5.
597
                if ( has_filter( 'replace_editor', 'gutenberg_init' ) ) {
5✔
598
                        return true;
2✔
599
                }
600

601
                if ( ! function_exists( 'is_plugin_active' ) ) {
3✔
602
                        // @codeCoverageIgnoreStart
603
                        include_once ABSPATH . 'wp-admin/includes/plugin.php';
604
                        // @codeCoverageIgnoreEnd
605
                }
606

607
                if ( is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
3✔
608
                        return in_array( get_option( 'classic-editor-replace' ), [ 'no-replace', 'block' ], true );
1✔
609
                }
610

611
                if ( is_plugin_active( 'disable-gutenberg/disable-gutenberg.php' ) ) {
2✔
612
                        return ! disable_gutenberg();
1✔
613
                }
614

615
                return true;
1✔
616
        }
617

618
        /**
619
         * Gutenberg support
620
         *
621
         * @param array|mixed $data    An array of slashed post data.
622
         * @param array|mixed $postarr An array of sanitized, but otherwise unmodified post data.
623
         *
624
         * @return array|mixed
625
         * @noinspection PhpUnusedParameterInspection
626
         */
627
        public function sanitize_post_name( $data, $postarr = [] ) {
628
                global $current_screen;
5✔
629

630
                if ( ! $this->is_gutenberg_editor_active() ) {
5✔
631
                        return $data;
2✔
632
                }
633

634
                // Run code only on post edit screen.
635
                if ( ! ( $current_screen && 'post' === $current_screen->base ) ) {
3✔
636
                        return $data;
1✔
637
                }
638

639
                if (
640
                        ! $data['post_name'] && $data['post_title'] &&
2✔
641
                        ! in_array( $data['post_status'], [ 'auto-draft', 'revision' ], true )
2✔
642
                ) {
643
                        $data['post_name'] = sanitize_title( $data['post_title'] );
1✔
644
                }
645

646
                return $data;
2✔
647
        }
648

649
        /**
650
         * Filters a term before it is sanitized and inserted into the database.
651
         *
652
         * @param string|int|WP_Error $term     The term name to add, or a WP_Error object if there's an error.
653
         * @param string              $taxonomy Taxonomy slug.
654
         *
655
         * @return string|int|WP_Error
656
         */
657
        public function pre_insert_term_filter( $term, string $taxonomy ) {
658
                if (
659
                        0 === $term ||
5✔
660
                        is_wp_error( $term ) ||
4✔
661
                        '' === trim( $term )
5✔
662
                ) {
663
                        return $term;
3✔
664
                }
665

666
                $this->is_term    = true;
2✔
667
                $this->taxonomies = [ $taxonomy ];
2✔
668

669
                return $term;
2✔
670
        }
671

672
        /**
673
         * Filters the terms query arguments.
674
         *
675
         * @param array|mixed $args       An array of get_terms() arguments.
676
         * @param string[]    $taxonomies An array of taxonomy names.
677
         *
678
         * @return array|mixed
679
         */
680
        public function get_terms_args_filter( $args, array $taxonomies ) {
681
                $this->is_term    = true;
3✔
682
                $this->taxonomies = $taxonomies;
3✔
683

684
                return $args;
3✔
685
        }
686

687
        /**
688
         * Locale filter for Polylang.
689
         *
690
         * @param string|mixed $locale Locale.
691
         *
692
         * @return string|mixed
693
         */
694
        public function pll_locale_filter( $locale ) {
695
                if ( $this->pll_locale ) {
7✔
696
                        return $this->pll_locale;
5✔
697
                }
698

699
                $rest_locale = $this->pll_locale_filter_with_rest();
7✔
700

701
                if ( false === $rest_locale ) {
7✔
702
                        return $locale;
1✔
703
                }
704

705
                if ( $rest_locale ) {
7✔
706
                        $this->pll_locale = $rest_locale;
1✔
707

708
                        return $this->pll_locale;
1✔
709
                }
710

711
                if ( ! is_admin() ) {
6✔
712
                        return $locale;
1✔
713
                }
714

715
                if ( ! $this->request->is_post() ) {
5✔
716
                        return $locale;
1✔
717
                }
718

719
                $pll_get_post_language = $this->pll_locale_filter_with_classic_editor();
4✔
720
                if ( $pll_get_post_language ) {
4✔
721
                        $this->pll_locale = $pll_get_post_language;
3✔
722

723
                        return $this->pll_locale;
3✔
724
                }
725

726
                $pll_get_term_language = $this->pll_locale_filter_with_term();
4✔
727
                if ( $pll_get_term_language ) {
4✔
728
                        $this->pll_locale = $pll_get_term_language;
1✔
729

730
                        return $this->pll_locale;
1✔
731
                }
732

733
                return $locale;
4✔
734
        }
735

736
        /**
737
         * Locale filter for Polylang with REST request.
738
         *
739
         * @return false|null|string
740
         */
741
        private function pll_locale_filter_with_rest() {
742
                if ( ! defined( 'REST_REQUEST' ) || ! constant( 'REST_REQUEST' ) ) {
7✔
743
                        return null;
6✔
744
                }
745

746
                /**
747
                 * REST Server.
748
                 *
749
                 * @var WP_REST_Server $rest_server
750
                 */
751
                $rest_server = rest_get_server();
1✔
752
                $data        = json_decode( $rest_server::get_raw_data(), false );
1✔
753

754
                return $data->lang ?? false;
1✔
755
        }
756

757
        /**
758
         * Locale filter for Polylang with the classic editor.
759
         *
760
         * @return bool|string
761
         * @noinspection PhpUndefinedFunctionInspection
762
         */
763
        private function pll_locale_filter_with_classic_editor() {
764
                if ( ! function_exists( 'pll_get_post_language' ) ) {
4✔
765
                        return false;
1✔
766
                }
767

768
                $pll_get_post_language = false;
4✔
769

770
                // phpcs:disable WordPress.Security.NonceVerification.Missing
771
                // phpcs:disable WordPress.Security.NonceVerification.Recommended
772
                if ( isset( $_POST['post_ID'] ) ) {
4✔
773
                        $pll_get_post_language = pll_get_post_language(
1✔
774
                                (int) filter_input( INPUT_POST, 'post_ID', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
775
                                'locale'
1✔
776
                        );
1✔
777
                }
778
                if ( isset( $_POST['pll_post_id'] ) ) {
4✔
779
                        $pll_get_post_language = pll_get_post_language(
1✔
780
                                (int) filter_input( INPUT_POST, 'pll_post_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
781
                                'locale'
1✔
782
                        );
1✔
783
                }
784
                if ( isset( $_GET['post'] ) ) {
4✔
785
                        $pll_get_post_language = pll_get_post_language(
1✔
786
                                (int) filter_input( INPUT_GET, 'post', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
787
                                'locale'
1✔
788
                        );
1✔
789
                }
790
                // phpcs:enable WordPress.Security.NonceVerification.Recommended
791
                // phpcs:enable WordPress.Security.NonceVerification.Missing
792

793
                return $pll_get_post_language;
4✔
794
        }
795

796
        /**
797
         * Locale filter for Polylang with term.
798
         *
799
         * @return false|string
800
         * @noinspection PhpUndefinedFunctionInspection
801
         */
802
        private function pll_locale_filter_with_term() {
803
                if ( ! function_exists( 'PLL' ) ) {
4✔
804
                        return false;
4✔
805
                }
806

807
                $pll_get_term_language = false;
1✔
808

809
                // phpcs:disable WordPress.Security.NonceVerification.Missing
810
                if ( isset( $_POST['term_lang_choice'] ) ) {
1✔
811
                        $pll_get_language = PLL()->model->get_language(
1✔
812
                                filter_input( INPUT_POST, 'term_lang_choice', FILTER_SANITIZE_FULL_SPECIAL_CHARS )
1✔
813
                        );
1✔
814

815
                        if ( $pll_get_language ) {
1✔
816
                                $pll_get_term_language = $pll_get_language->locale;
1✔
817
                        }
818
                }
819

820
                // phpcs:enable WordPress.Security.NonceVerification.Missing
821

822
                return $pll_get_term_language;
1✔
823
        }
824

825
        /**
826
         * Locale filter for WPML.
827
         *
828
         * @param string|mixed $locale Locale.
829
         *
830
         * @return string|null|mixed
831
         */
832
        public function wpml_locale_filter( $locale ) {
833
                if ( $this->wpml_locale ) {
1✔
834
                        return $this->wpml_locale;
1✔
835
                }
836

837
                return $locale;
1✔
838
        }
839

840
        /**
841
         * Get wpml locale.
842
         *
843
         * @return string|null
844
         * @noinspection PhpUndefinedFunctionInspection
845
         */
846
        protected function get_wpml_locale(): ?string {
847
                $language_code        = wpml_get_current_language();
2✔
848
                $this->wpml_languages = (array) apply_filters( 'wpml_active_languages', [] );
2✔
849

850
                return (
2✔
851
                isset( $this->wpml_languages[ $language_code ] ) ?
2✔
852
                        $this->wpml_languages[ $language_code ]['default_locale'] :
1✔
853
                        null
2✔
854
                );
2✔
855
        }
856

857
        /**
858
         * Save switched locale.
859
         *
860
         * @param null|string $language_code     Language code to switch into.
861
         * @param bool|string $cookie_lang       Optionally also switch the cookie language to the value given.
862
         * @param string      $original_language Original language.
863
         *
864
         * @return void
865
         * @noinspection PhpUnusedParameterInspection
866
         * @noinspection PhpMissingParamTypeInspection
867
         */
868
        public function wpml_language_has_switched( $language_code, $cookie_lang, string $original_language ): void {
869
                $language_code = (string) $language_code;
3✔
870

871
                $this->wpml_locale =
3✔
872
                        isset( $this->wpml_languages[ $language_code ] ) ?
3✔
873
                                $this->wpml_languages[ $language_code ]['default_locale'] :
1✔
874
                                null;
2✔
875
        }
876

877
        /**
878
         * Checks for changed slugs for published post objects to save the old slug.
879
         *
880
         * @param int     $post_id     Post ID.
881
         * @param WP_Post $post        The post object.
882
         * @param WP_Post $post_before The previous post object.
883
         *
884
         * @noinspection PhpMissingParamTypeInspection
885
         * @noinspection PhpUnusedParameterInspection
886
         */
887
        public function check_for_changed_slugs( $post_id, $post, $post_before ): void {
888
                // Don't bother if it hasn't changed.
889
                if ( $post->post_name === $post_before->post_name ) {
6✔
890
                        return;
1✔
891
                }
892

893
                // We're only concerned with published, non-hierarchical objects.
894
                if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) {
5✔
895
                        return;
2✔
896
                }
897

898
                // Modify $post_before->post_name when cyr2lat converted the title.
899
                if (
900
                        empty( $post_before->post_name ) &&
3✔
901
                        $post->post_title !== $post->post_name &&
3✔
902
                        $post->post_name === $this->transliterate( $post->post_title )
3✔
903
                ) {
904
                        $post_before->post_name = rawurlencode( $post->post_title );
1✔
905
                }
906
        }
907

908
        /**
909
         * Declare compatibility with custom order tables for WooCommerce.
910
         *
911
         * @return void
912
         */
913
        public function declare_wc_compatibility(): void {
914
                if ( class_exists( FeaturesUtil::class ) ) {
2✔
915
                        FeaturesUtil::declare_compatibility(
1✔
916
                                'custom_order_tables',
1✔
917
                                constant( 'CYR_TO_LAT_FILE' )
1✔
918
                        );
1✔
919
                }
920
        }
921

922
        /**
923
         * Changes an array of items into a string of items, separated by comma and sql-escaped.
924
         *
925
         * @see https://coderwall.com/p/zepnaw
926
         * @global wpdb       $wpdb
927
         *
928
         * @param mixed|array $items  item(s) to be joined into string.
929
         * @param string      $format %s or %d.
930
         *
931
         * @return string Items separated by comma and sql-escaped.
932
         */
933
        public function prepare_in( $items, string $format = '%s' ): string {
934
                global $wpdb;
8✔
935

936
                $prepared_in = '';
8✔
937
                $items       = (array) $items;
8✔
938
                $how_many    = count( $items );
8✔
939

940
                if ( $how_many > 0 ) {
8✔
941
                        $placeholders    = array_fill( 0, $how_many, $format );
6✔
942
                        $prepared_format = implode( ',', $placeholders );
6✔
943
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
944
                        $prepared_in = $wpdb->prepare( $prepared_format, $items );
6✔
945
                }
946

947
                return $prepared_in;
8✔
948
        }
949

950
        /**
951
         * Check if we should transliterate the tag on pre_term_slug filter.
952
         *
953
         * @param string $title Title.
954
         *
955
         * @return bool
956
         */
957
        protected function transliterate_on_pre_term_slug_filter( string $title ): bool {
958
                global $wp_query;
22✔
959

960
                $tag_var = $wp_query->query_vars['tag'] ?? null;
22✔
961

962
                return ! (
22✔
963
                        $tag_var === $title &&
22✔
964
                        doing_filter( 'pre_term_slug' ) &&
22✔
965
                        // Transliterate on pre_term_slug with Polylang and WPML only.
966
                        ! ( class_exists( 'Polylang' ) || class_exists( 'SitePress' ) )
22✔
967
                );
22✔
968
        }
969
}
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