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

mihdan / cyr2lat / #1132

05 May 2026 11:17AM UTC coverage: 89.835% (-0.2%) from 90.025%
#1132

push

php-coveralls

kagg-design
Update changelog from readme.txt

2510 of 2794 relevant lines covered (89.84%)

2.15 hits per line

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

95.88
/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 JsonException;
22
use Polylang;
23
use SitePress;
24
use stdClass;
25
use WP_CLI;
26
use WP_Error;
27
use WP_Post;
28
use wpdb;
29
use CyrToLat\Settings\Settings;
30
use CyrToLat\Symfony\Polyfill\Mbstring\Mbstring;
31

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

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

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

51
        /**
52
         * Process posts instance.
53
         *
54
         * @var PostConversionProcess|null
55
         */
56
        protected ?PostConversionProcess $process_all_posts = null;
57

58
        /**
59
         * Process terms instance.
60
         *
61
         * @var TermConversionProcess|null
62
         */
63
        protected ?TermConversionProcess $process_all_terms = null;
64

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

72
        /**
73
         * Converter instance.
74
         *
75
         * @var Converter|null
76
         */
77
        protected ?Converter $converter = null;
78

79
        /**
80
         * WP_CLI instance.
81
         *
82
         * @var WPCli|null
83
         */
84
        protected ?WPCli $cli = null;
85

86
        /**
87
         * ACF instance.
88
         *
89
         * @var ACF|null
90
         */
91
        protected ?ACF $acf = null;
92

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

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

107
        /**
108
         * Polylang locale.
109
         *
110
         * @var string|null
111
         */
112
        private ?string $pll_locale = null;
113

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

121
        /**
122
         * WPML languages.
123
         *
124
         * @var array
125
         */
126
        protected array $wpml_languages = [];
127

128
        /**
129
         * The current request is frontend.
130
         *
131
         * @var bool|null
132
         */
133
        protected ?bool $is_frontend = null;
134

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

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

152
                $this->init_multilingual();
1✔
153
                $this->init_classes();
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 hooks.
235
         */
236
        protected function init_hooks(): void {
237
                if ( $this->is_frontend ) {
9✔
238
                        add_action( 'woocommerce_before_template_part', [ $this, 'woocommerce_before_template_part_filter' ] );
4✔
239
                        add_action( 'woocommerce_after_template_part', [ $this, 'woocommerce_after_template_part_filter' ] );
4✔
240
                }
241

242
                if ( ! $this->request->is_allowed() ) {
9✔
243
                        return;
1✔
244
                }
245

246
                add_filter( 'sanitize_title', [ $this, 'sanitize_title' ], 9, 3 );
8✔
247
                add_filter( 'sanitize_file_name', [ $this, 'sanitize_filename' ], 10, 2 );
8✔
248
                add_filter( 'wp_insert_post_data', [ $this, 'sanitize_post_name' ], 10, 2 );
8✔
249
                add_filter( 'pre_insert_term', [ $this, 'pre_insert_term_filter' ], PHP_INT_MAX, 2 );
8✔
250
                add_filter( 'post_updated', [ $this, 'check_for_changed_slugs' ], 10, 3 );
8✔
251

252
                if ( ! $this->is_frontend || class_exists( SitePress::class ) ) {
8✔
253
                        add_filter( 'get_terms_args', [ $this, 'get_terms_args_filter' ], PHP_INT_MAX, 2 );
6✔
254
                }
255

256
                add_action( 'before_woocommerce_init', [ $this, 'declare_wc_compatibility' ] );
8✔
257

258
                if ( $this->request->is_cli() ) {
8✔
259
                        add_action( 'cli_init', [ $this, 'action_cli_init' ] );
4✔
260
                }
261
        }
262

263
        /**
264
         * Action cli init.
265
         *
266
         * @return void
267
         */
268
        public function action_cli_init(): void {
269
                $this->cli = new WPCli( $this->converter );
1✔
270

271
                /**
272
                 * Method WP_CLI::add_command() accepts a class as callable.
273
                 *
274
                 * @noinspection PhpParamsInspection
275
                 */
276
                WP_CLI::add_command( 'cyr2lat', $this->cli );
1✔
277
        }
278

279
        /**
280
         * Get a Settings instance.
281
         *
282
         * @return Settings
283
         */
284
        public function settings(): Settings {
285
                return $this->settings;
1✔
286
        }
287

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

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

312
                $title = urldecode( (string) $title );
21✔
313
                $pre   = apply_filters( 'ctl_pre_sanitize_title', false, $title );
21✔
314

315
                if ( false !== $pre ) {
21✔
316
                        return $pre;
1✔
317
                }
318

319
                if ( $this->is_term ) {
20✔
320
                        // Make sure we search in the db only once being called from wp_insert_term().
321
                        $this->is_term = false;
5✔
322

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

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

335
                        if ( $this->taxonomies ) {
4✔
336
                                $sql .= ' AND tt.taxonomy IN (' . $this->prepare_in( $this->taxonomies ) . ')';
3✔
337
                        }
338

339
                        // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
340
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
341
                        $term = $wpdb->get_var( $sql );
4✔
342
                        // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
343

344
                        if ( ! empty( $term ) ) {
4✔
345
                                return $term;
4✔
346
                        }
347
                }
348

349
                return $this->is_wc_attribute( $title ) ? $title : $this->transliterate( $title );
19✔
350
        }
351

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

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

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

383
                foreach ( wc_get_attribute_taxonomies() as $attribute_taxonomy ) {
4✔
384
                        if ( $title === $attribute_taxonomy->attribute_name ) {
3✔
385
                                return true;
2✔
386
                        }
387
                }
388

389
                return false;
2✔
390
        }
391

392
        /**
393
         * Check if the title is a local attribute.
394
         *
395
         * @param string $title Title.
396
         *
397
         * @return bool
398
         */
399
        protected function is_local_attribute( string $title ): bool {
400
                // Global attribute.
401
                if ( 0 === strpos( $title, 'pa_' ) ) {
6✔
402
                        return false;
1✔
403
                }
404

405
                // phpcs:disable WordPress.Security.NonceVerification.Missing
406

407
                $action = (string) filter_input( INPUT_POST, 'action', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
5✔
408

409
                if ( 'woocommerce_do_ajax_product_import' === $action ) {
5✔
410
                        return false;
×
411
                }
412

413
                // The `save attributes` action.
414
                if ( 'woocommerce_save_attributes' === $action ) {
5✔
415
                        $data            = (string) filter_input( INPUT_POST, 'data', FILTER_SANITIZE_URL );
2✔
416
                        $attributes      = $this->wp_parse_str( urldecode( $data ) );
2✔
417
                        $attribute_names = $attributes['attribute_names'] ?? [];
2✔
418

419
                        return in_array( $title, $attribute_names, true );
2✔
420
                }
421

422
                // The `edit post` action.
423
                if ( 'editpost' === $action ) {
3✔
424
                        $attribute_names = array_map(
×
425
                                'sanitize_text_field',
×
426
                                (array) wp_unslash( $_POST['attribute_names'] ?? [] )
×
427
                        );
×
428

429
                        return in_array( $title, $attribute_names, true );
430
                }
431

432
                if ( doing_action( 'woocommerce_variable_add_to_cart' ) ) {
×
433
                        $attributes = $GLOBALS['product']->get_attributes();
434

435
                        $encoded_attr_name = strtolower( rawurlencode( mb_strtolower( $title ) ) );
436

437
                        if ( isset( $attributes[ $encoded_attr_name ] ) ) {
×
438
                                return true;
439
                        }
440

441
                        return false;
442
                }
443

444
                if ( did_action( 'woocommerce_load_cart_from_session' ) ) {
×
445
                        return true;
446
                }
447

448
                $attr_name = str_replace( 'attribute_', '', mb_strtolower( $title ) );
3✔
449
                $attr_name = 'attribute_' . $attr_name;
450

451
                $encoded_attr_name = rawurlencode( $attr_name );
452

453
                return isset( $_POST[ $encoded_attr_name ] ) || isset( $_POST[ strtolower( $encoded_attr_name ) ] );
454

455
                // phpcs:enable WordPress.Security.NonceVerification.Missing
456
        }
457

458
        // @codeCoverageIgnoreStart
459

460
        /**
461
         * Polyfill of the wp_parse_str().
462
         * Added for test reasons.
463
         *
464
         * @param string $input_string Input string.
465
         *
466
         * @return array
467
         */
468
        protected function wp_parse_str( string $input_string ): array {
469
                wp_parse_str( $input_string, $result );
470

471
                return $result;
472
        }
473

474
        // @codeCoverageIgnoreEnd
475

476
        /**
477
         * Check if title is a product not converted attribute.
478
         *
479
         * @param string $title Title.
480
         *
481
         * @return bool
482
         * @noinspection PhpUndefinedFunctionInspection
483
         * @noinspection PhpUndefinedMethodInspection
484
         */
485
        protected function is_wc_product_not_converted_attribute( string $title ): bool {
486

487
                global $product;
488

489
                if ( ! is_a( $product, 'WC_Product' ) ) {
3✔
490
                        return false;
491
                }
492

493
                // We have to get attributes from postmeta here to see the converted slug.
494
                $attributes = (array) get_post_meta( $product->get_id(), '_product_attributes', true );
495

496
                foreach ( $attributes as $slug => $attribute ) {
2✔
497
                        $name = $attribute['name'] ?? '';
498

499
                        if ( $name === $title && sanitize_title_with_dashes( $title ) === $slug ) {
1✔
500
                                return true;
501
                        }
502
                }
503

504
                return false;
505
        }
506

507
        /**
508
         * Check if title is an attribute.
509
         *
510
         * @param string $title Title.
511
         *
512
         * @return bool
513
         * @noinspection PhpUndefinedFunctionInspection
514
         */
515
        protected function is_wc_attribute( string $title ): bool {
516
                if ( ! function_exists( 'WC' ) ) {
16✔
517
                        return false;
518
                }
519

520
                return (
12✔
521
                        $this->is_wc_attribute_taxonomy( $title ) ||
12✔
522
                        $this->is_local_attribute( $title ) ||
12✔
523
                        $this->is_wc_product_not_converted_attribute( $title )
12✔
524
                );
12✔
525
        }
526

527
        /**
528
         * Sanitize filename.
529
         *
530
         * @param string|mixed $filename     Sanitized filename.
531
         * @param string|mixed $filename_raw The filename prior to sanitization.
532
         *
533
         * @return string
534
         * @noinspection PhpUnusedParameterInspection
535
         * @noinspection PhpMissingReturnTypeInspection
536
         * @noinspection ReturnTypeCanBeDeclaredInspection
537
         */
538
        public function sanitize_filename( $filename, $filename_raw ) {
539
                global $wp_version;
540

541
                $pre = apply_filters( 'ctl_pre_sanitize_filename', false, $filename );
542

543
                if ( false !== $pre ) {
1✔
544
                        return (string) $pre;
545
                }
546

547
                $filename = (string) $filename;
7✔
548
                $is_utf8  = version_compare( (string) $wp_version, '6.9-RC1', '>=' ) ? 'wp_is_valid_utf8' : 'seems_utf8';
549

550
                if ( $is_utf8( $filename ) ) {
7✔
551
                        $filename = (string) Mbstring::mb_strtolower( $filename );
552
                }
553

554
                return $this->transliterate( $filename );
555
        }
556

557
        /**
558
         * Get min suffix.
559
         *
560
         * @return string
561
         */
562
        public function min_suffix(): string {
563
                return defined( 'SCRIPT_DEBUG' ) && constant( 'SCRIPT_DEBUG' ) ? '' : '.min';
564
        }
565

566
        /**
567
         * Fix string encoding on macOS.
568
         *
569
         * @param string $str   String.
570
         * @param array  $table Conversion table.
571
         *
572
         * @return string
573
         */
574
        private function fix_mac_string( string $str, array $table ): string {
575
                $fix_table = ConversionTables::get_fix_table_for_mac();
576

577
                $fix = [];
26✔
578
                foreach ( $fix_table as $key => $value ) {
26✔
579
                        if ( isset( $table[ $key ] ) ) {
26✔
580
                                $fix[ $value ] = $table[ $key ];
581
                        }
582
                }
583

584
                return strtr( $str, $fix );
585
        }
586

587
        /**
588
         * Split Chinese string by hyphens.
589
         *
590
         * @param string $str   String.
591
         * @param array  $table Conversion table.
592
         *
593
         * @return string
594
         */
595
        protected function split_chinese_string( string $str, array $table ): string {
596
                if ( ! $this->settings->is_chinese_locale() || mb_strlen( $str ) < 4 ) {
27✔
597
                        return $str;
598
                }
599

600
                $chars = Mbstring::mb_str_split( $str );
2✔
601
                $str   = '';
602

603
                foreach ( $chars as $char ) {
2✔
604
                        if ( isset( $table[ $char ] ) ) {
2✔
605
                                $str .= '-' . $char . '-';
606
                        } else {
607
                                $str .= $char;
608
                        }
609
                }
610

611
                return $str;
612
        }
613

614
        /**
615
         * Transliterate string using a table.
616
         *
617
         * @param string $str String.
618
         *
619
         * @return string
620
         */
621
        public function transliterate( string $str ): string {
622
                $table = (array) apply_filters( 'ctl_table', $this->settings->get_table() );
623

624
                $str = $this->fix_mac_string( $str, $table );
26✔
625
                $str = $this->split_chinese_string( $str, $table );
626

627
                return strtr( $str, $table );
628
        }
629

630
        /**
631
         * Check if the Block Editor is active.
632
         * Must only be used after the plugins_loaded action is fired.
633
         *
634
         * @return bool
635
         * @noinspection PhpUndefinedFunctionInspection
636
         */
637
        private function is_gutenberg_editor_active(): bool {
638
                // Gutenberg plugin is installed and activated.
639
                // This filter was removed in WP 5.5.
640
                if ( has_filter( 'replace_editor', 'gutenberg_init' ) ) {
2✔
641
                        return true;
642
                }
643

644
                if ( ! function_exists( 'is_plugin_active' ) ) {
645
                        // @codeCoverageIgnoreStart
646
                        include_once ABSPATH . 'wp-admin/includes/plugin.php';
647
                        // @codeCoverageIgnoreEnd
648
                }
649

650
                if ( is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
1✔
651
                        return in_array( get_option( 'classic-editor-replace' ), [ 'no-replace', 'block' ], true );
652
                }
653

654
                if ( is_plugin_active( 'disable-gutenberg/disable-gutenberg.php' ) ) {
1✔
655
                        return ! disable_gutenberg();
656
                }
657

658
                return true;
659
        }
660

661
        /**
662
         * Gutenberg support
663
         *
664
         * @param array|mixed $data    An array of slashed post data.
665
         * @param array|mixed $postarr An array of sanitized, but otherwise unmodified post data.
666
         *
667
         * @return array|mixed
668
         * @noinspection PhpUnusedParameterInspection
669
         */
670
        public function sanitize_post_name( $data, $postarr = [] ) {
671
                global $current_screen;
672

673
                if ( ! $this->is_gutenberg_editor_active() ) {
2✔
674
                        return $data;
675
                }
676

677
                // Run code only on post-edit screen.
678
                if ( ! ( $current_screen && 'post' === $current_screen->base ) ) {
1✔
679
                        return $data;
680
                }
681

682
                if (
683
                        ! $data['post_name'] && $data['post_title'] &&
2✔
684
                        ! in_array( $data['post_status'], [ 'auto-draft', 'revision' ], true )
685
                ) {
686
                        $data['post_name'] = sanitize_title( $data['post_title'] );
687
                }
688

689
                return $data;
690
        }
691

692
        /**
693
         * Filters a term before it is sanitized and inserted into the database.
694
         *
695
         * @param string|int|WP_Error $term     The term name to add, or a WP_Error object if there's an error.
696
         * @param string              $taxonomy Taxonomy slug.
697
         *
698
         * @return string|int|WP_Error
699
         */
700
        public function pre_insert_term_filter( $term, string $taxonomy ) {
701
                if (
702
                        0 === $term ||
4✔
703
                        is_wp_error( $term ) ||
5✔
704
                        '' === trim( $term )
705
                ) {
706
                        return $term;
707
                }
708

709
                $this->is_term    = true;
2✔
710
                $this->taxonomies = [ $taxonomy ];
711

712
                return $term;
713
        }
714

715
        /**
716
         * Filters the term query arguments.
717
         *
718
         * @param array|mixed $args       An array of get_terms() arguments.
719
         * @param string[]    $taxonomies An array of taxonomy names.
720
         *
721
         * @return array|mixed
722
         */
723
        public function get_terms_args_filter( $args, array $taxonomies ) {
724
                $this->is_term    = true;
3✔
725
                $this->taxonomies = $taxonomies;
726

727
                return $args;
728
        }
729

730
        /**
731
         * Locale filter for Polylang.
732
         *
733
         * @param string|mixed $locale Locale.
734
         *
735
         * @return string|mixed
736
         */
737
        public function pll_locale_filter( $locale ) {
738
                if ( $this->pll_locale ) {
5✔
739
                        return $this->pll_locale;
740
                }
741

742
                $rest_locale = $this->pll_locale_filter_with_rest();
743

744
                if ( false === $rest_locale ) {
1✔
745
                        return $locale;
746
                }
747

748
                if ( $rest_locale ) {
1✔
749
                        $this->pll_locale = $rest_locale;
750

751
                        return $this->pll_locale;
752
                }
753

754
                if ( ! is_admin() ) {
1✔
755
                        return $locale;
756
                }
757

758
                if ( ! $this->request->is_post() ) {
1✔
759
                        return $locale;
760
                }
761

762
                $pll_get_post_language = $this->pll_locale_filter_with_classic_editor();
763

764
                if ( $pll_get_post_language ) {
3✔
765
                        $this->pll_locale = $pll_get_post_language;
766

767
                        return $this->pll_locale;
768
                }
769

770
                $pll_get_term_language = $this->pll_locale_filter_with_term();
771

772
                if ( $pll_get_term_language ) {
1✔
773
                        $this->pll_locale = $pll_get_term_language;
774

775
                        return $this->pll_locale;
776
                }
777

778
                return $locale;
779
        }
780

781
        /**
782
         * Locale filter for Polylang with REST request.
783
         *
784
         * @return false|null|string
785
         */
786
        private function pll_locale_filter_with_rest() {
787
                if ( ! defined( 'REST_REQUEST' ) || ! constant( 'REST_REQUEST' ) ) {
6✔
788
                        return null;
789
                }
790

791
                /**
792
                 * REST Server.
793
                 *
794
                 * @var WP_REST_Server $rest_server
795
                 */
796
                $rest_server = rest_get_server();
797

798
                try {
799
                        $data = json_decode( $rest_server::get_raw_data(), false, 512, JSON_THROW_ON_ERROR );
1✔
800
                } catch ( JsonException $e ) {
1✔
801
                        $data = new stdClass();
802
                }
803

804
                return $data->lang ?? false;
805
        }
806

807
        /**
808
         * Locale filter for Polylang with the classic editor.
809
         *
810
         * @return bool|string
811
         * @noinspection PhpUndefinedFunctionInspection
812
         */
813
        private function pll_locale_filter_with_classic_editor() {
814
                if ( ! function_exists( 'pll_get_post_language' ) ) {
1✔
815
                        return false;
816
                }
817

818
                $pll_get_post_language = false;
819

820
                // phpcs:disable WordPress.Security.NonceVerification.Missing
821
                // phpcs:disable WordPress.Security.NonceVerification.Recommended
822
                if ( isset( $_POST['post_ID'] ) ) {
1✔
823
                        $pll_get_post_language = pll_get_post_language(
1✔
824
                                (int) filter_input( INPUT_POST, 'post_ID', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
825
                                'locale'
1✔
826
                        );
1✔
827
                }
828
                if ( isset( $_POST['pll_post_id'] ) ) {
1✔
829
                        $pll_get_post_language = pll_get_post_language(
1✔
830
                                (int) filter_input( INPUT_POST, 'pll_post_id', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
831
                                'locale'
1✔
832
                        );
1✔
833
                }
834
                if ( isset( $_GET['post'] ) ) {
1✔
835
                        $pll_get_post_language = pll_get_post_language(
1✔
836
                                (int) filter_input( INPUT_GET, 'post', FILTER_SANITIZE_FULL_SPECIAL_CHARS ),
1✔
837
                                'locale'
1✔
838
                        );
1✔
839
                }
840
                // phpcs:enable WordPress.Security.NonceVerification.Recommended
841
                // phpcs:enable WordPress.Security.NonceVerification.Missing
842

843
                return $pll_get_post_language;
844
        }
845

846
        /**
847
         * Locale filter for Polylang with term.
848
         *
849
         * @return false|string
850
         * @noinspection PhpUndefinedFunctionInspection
851
         */
852
        private function pll_locale_filter_with_term() {
853
                if ( ! function_exists( 'PLL' ) ) {
4✔
854
                        return false;
855
                }
856

857
                $pll_get_term_language = false;
858

859
                // phpcs:disable WordPress.Security.NonceVerification.Missing
860
                if ( isset( $_POST['term_lang_choice'] ) ) {
1✔
861
                        $pll_get_language = PLL()->model->get_language(
1✔
862
                                filter_input( INPUT_POST, 'term_lang_choice', FILTER_SANITIZE_FULL_SPECIAL_CHARS )
1✔
863
                        );
1✔
864

865
                        if ( $pll_get_language ) {
1✔
866
                                $pll_get_term_language = $pll_get_language->locale;
867
                        }
868
                }
869

870
                // phpcs:enable WordPress.Security.NonceVerification.Missing
871

872
                return $pll_get_term_language;
873
        }
874

875
        /**
876
         * Locale filter for WPML.
877
         *
878
         * @param string|mixed $locale Locale.
879
         *
880
         * @return string|null|mixed
881
         */
882
        public function wpml_locale_filter( $locale ) {
883
                if ( $this->wpml_locale ) {
1✔
884
                        return $this->wpml_locale;
885
                }
886

887
                return $locale;
888
        }
889

890
        /**
891
         * Get wpml locale.
892
         *
893
         * @return string|null
894
         * @noinspection PhpUndefinedFunctionInspection
895
         */
896
        protected function get_wpml_locale(): ?string {
897
                $language_code        = wpml_get_current_language();
2✔
898
                $this->wpml_languages = (array) apply_filters( 'wpml_active_languages', [] );
899

900
                return $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
901
        }
902

903
        /**
904
         * Save switched locale.
905
         *
906
         * @param null|string $language_code     Language code to switch into.
907
         * @param bool|string $cookie_lang       Optionally also switch the cookie language to the value given.
908
         * @param string      $original_language Original language.
909
         *
910
         * @return void
911
         * @noinspection PhpUnusedParameterInspection
912
         * @noinspection PhpMissingParamTypeInspection
913
         */
914
        public function wpml_language_has_switched( $language_code, $cookie_lang, string $original_language ): void {
915
                $language_code = (string) $language_code;
916

917
                $this->wpml_locale = $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
3✔
918
        }
919

920
        /**
921
         * Checks for changed slugs for published post objects to save the old slug.
922
         *
923
         * @param int     $post_id     Post ID.
924
         * @param WP_Post $post        The post object.
925
         * @param WP_Post $post_before The previous post object.
926
         *
927
         * @noinspection PhpMissingParamTypeInspection
928
         * @noinspection PhpUnusedParameterInspection
929
         */
930
        public function check_for_changed_slugs( $post_id, $post, $post_before ): void {
931
                // Don't bother if it hasn't changed.
932
                if ( $post->post_name === $post_before->post_name ) {
1✔
933
                        return;
934
                }
935

936
                // We're only concerned with published, non-hierarchical objects.
937
                if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) {
2✔
938
                        return;
939
                }
940

941
                // Modify $post_before->post_name when cyr2lat converted the title.
942
                if (
943
                        empty( $post_before->post_name ) &&
3✔
944
                        $post->post_title !== $post->post_name &&
3✔
945
                        $post->post_name === $this->transliterate( $post->post_title )
946
                ) {
947
                        $post_before->post_name = rawurlencode( $post->post_title );
948
                }
949
        }
950

951
        /**
952
         * Declare compatibility with custom order tables for WooCommerce.
953
         *
954
         * @return void
955
         */
956
        public function declare_wc_compatibility(): void {
957
                if ( class_exists( FeaturesUtil::class ) ) {
1✔
958
                        FeaturesUtil::declare_compatibility(
1✔
959
                                'custom_order_tables',
1✔
960
                                constant( 'CYR_TO_LAT_FILE' )
1✔
961
                        );
1✔
962
                }
963
        }
964

965
        /**
966
         * Changes an array of items into a string of items, separated by comma and sql-escaped.
967
         *
968
         * @see https://coderwall.com/p/zepnaw
969
         * @global wpdb       $wpdb
970
         *
971
         * @param mixed|array $items  item(s) to be joined into string.
972
         * @param string      $format %s or %d.
973
         *
974
         * @return string Items separated by comma and sql-escaped.
975
         */
976
        public function prepare_in( $items, string $format = '%s' ): string {
977
                global $wpdb;
978

979
                $prepared_in = '';
8✔
980
                $items       = (array) $items;
8✔
981
                $how_many    = count( $items );
982

983
                if ( $how_many > 0 ) {
6✔
984
                        $placeholders    = array_fill( 0, $how_many, $format );
6✔
985
                        $prepared_format = implode( ',', $placeholders );
986
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
987
                        $prepared_in = $wpdb->prepare( $prepared_format, $items );
988
                }
989

990
                return $prepared_in;
991
        }
992

993
        /**
994
         * Check if we should transliterate the tag on pre_term_slug filter.
995
         *
996
         * @param string $title Title.
997
         *
998
         * @return bool
999
         */
1000
        protected function transliterate_on_pre_term_slug_filter( string $title ): bool {
1001
                global $wp_query;
1002

1003
                $tag_var = $wp_query->query_vars['tag'] ?? null;
1004

1005
                return ! (
22✔
1006
                        $tag_var === $title &&
22✔
1007
                        doing_filter( 'pre_term_slug' ) &&
22✔
1008
                        // Transliterate on pre_term_slug with Polylang and WPML only.
1009
                        ! ( class_exists( 'Polylang' ) || class_exists( 'SitePress' ) )
22✔
1010
                );
22✔
1011
        }
1012
}
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