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

mihdan / cyr2lat / #1114

01 Apr 2026 09:00PM UTC coverage: 90.025% (+2.4%) from 87.636%
#1114

Pull #183

php-coveralls

kagg-design
Fix tests.
Pull Request #183: V6.7.0

28 of 35 new or added lines in 4 files covered. (80.0%)

2500 of 2777 relevant lines covered (90.03%)

2.15 hits per line

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

96.35
/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
                // The `save attributes` action.
410
                if ( 'woocommerce_save_attributes' === $action ) {
5✔
411
                        $data            = (string) filter_input( INPUT_POST, 'data', FILTER_SANITIZE_URL );
2✔
412
                        $attributes      = $this->wp_parse_str( urldecode( $data ) );
2✔
413
                        $attribute_names = $attributes['attribute_names'] ?? [];
2✔
414

415
                        return in_array( $title, $attribute_names, true );
2✔
416
                }
417

418
                // The `edit post` action.
419
                if ( 'editpost' === $action ) {
3✔
NEW
420
                        $attribute_names = array_map(
×
NEW
421
                                'sanitize_text_field',
×
NEW
422
                                (array) wp_unslash( $_POST['attribute_names'] ?? [] )
×
NEW
423
                        );
×
424

425
                        return in_array( $title, $attribute_names, true );
426
                }
427

NEW
428
                if ( doing_action( 'woocommerce_variable_add_to_cart' ) ) {
×
429
                        $attributes = $GLOBALS['product']->get_attributes();
430

431
                        $encoded_attr_name = strtolower( rawurlencode( mb_strtolower( $title ) ) );
432

NEW
433
                        if ( isset( $attributes[ $encoded_attr_name ] ) ) {
×
434
                                return true;
435
                        }
436

437
                        return false;
438
                }
439

NEW
440
                if ( did_action( 'woocommerce_load_cart_from_session' ) ) {
×
441
                        return true;
442
                }
443

444
                $attr_name = str_replace( 'attribute_', '', mb_strtolower( $title ) );
3✔
445
                $attr_name = 'attribute_' . $attr_name;
446

447
                $encoded_attr_name = rawurlencode( $attr_name );
448

449
                return isset( $_POST[ $encoded_attr_name ] ) || isset( $_POST[ strtolower( $encoded_attr_name ) ] );
450

451
                // phpcs:enable WordPress.Security.NonceVerification.Missing
452
        }
453

454
        // @codeCoverageIgnoreStart
455

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

467
                return $result;
468
        }
469

470
        // @codeCoverageIgnoreEnd
471

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

483
                global $product;
484

485
                if ( ! is_a( $product, 'WC_Product' ) ) {
3✔
486
                        return false;
487
                }
488

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

492
                foreach ( $attributes as $slug => $attribute ) {
2✔
493
                        $name = $attribute['name'] ?? '';
494

495
                        if ( $name === $title && sanitize_title_with_dashes( $title ) === $slug ) {
1✔
496
                                return true;
497
                        }
498
                }
499

500
                return false;
501
        }
502

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

516
                return (
12✔
517
                        $this->is_wc_attribute_taxonomy( $title ) ||
12✔
518
                        $this->is_local_attribute( $title ) ||
12✔
519
                        $this->is_wc_product_not_converted_attribute( $title )
12✔
520
                );
12✔
521
        }
522

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

537
                $pre = apply_filters( 'ctl_pre_sanitize_filename', false, $filename );
538

539
                if ( false !== $pre ) {
1✔
540
                        return (string) $pre;
541
                }
542

543
                $filename = (string) $filename;
7✔
544
                $is_utf8  = version_compare( (string) $wp_version, '6.9-RC1', '>=' ) ? 'wp_is_valid_utf8' : 'seems_utf8';
545

546
                if ( $is_utf8( $filename ) ) {
7✔
547
                        $filename = (string) Mbstring::mb_strtolower( $filename );
548
                }
549

550
                return $this->transliterate( $filename );
551
        }
552

553
        /**
554
         * Get min suffix.
555
         *
556
         * @return string
557
         */
558
        public function min_suffix(): string {
559
                return defined( 'SCRIPT_DEBUG' ) && constant( 'SCRIPT_DEBUG' ) ? '' : '.min';
560
        }
561

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

573
                $fix = [];
26✔
574
                foreach ( $fix_table as $key => $value ) {
26✔
575
                        if ( isset( $table[ $key ] ) ) {
26✔
576
                                $fix[ $value ] = $table[ $key ];
577
                        }
578
                }
579

580
                return strtr( $str, $fix );
581
        }
582

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

596
                $chars = Mbstring::mb_str_split( $str );
2✔
597
                $str   = '';
598

599
                foreach ( $chars as $char ) {
2✔
600
                        if ( isset( $table[ $char ] ) ) {
2✔
601
                                $str .= '-' . $char . '-';
602
                        } else {
603
                                $str .= $char;
604
                        }
605
                }
606

607
                return $str;
608
        }
609

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

620
                $str = $this->fix_mac_string( $str, $table );
26✔
621
                $str = $this->split_chinese_string( $str, $table );
622

623
                return strtr( $str, $table );
624
        }
625

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

640
                if ( ! function_exists( 'is_plugin_active' ) ) {
641
                        // @codeCoverageIgnoreStart
642
                        include_once ABSPATH . 'wp-admin/includes/plugin.php';
643
                        // @codeCoverageIgnoreEnd
644
                }
645

646
                if ( is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
1✔
647
                        return in_array( get_option( 'classic-editor-replace' ), [ 'no-replace', 'block' ], true );
648
                }
649

650
                if ( is_plugin_active( 'disable-gutenberg/disable-gutenberg.php' ) ) {
1✔
651
                        return ! disable_gutenberg();
652
                }
653

654
                return true;
655
        }
656

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

669
                if ( ! $this->is_gutenberg_editor_active() ) {
2✔
670
                        return $data;
671
                }
672

673
                // Run code only on post-edit screen.
674
                if ( ! ( $current_screen && 'post' === $current_screen->base ) ) {
1✔
675
                        return $data;
676
                }
677

678
                if (
679
                        ! $data['post_name'] && $data['post_title'] &&
2✔
680
                        ! in_array( $data['post_status'], [ 'auto-draft', 'revision' ], true )
681
                ) {
682
                        $data['post_name'] = sanitize_title( $data['post_title'] );
683
                }
684

685
                return $data;
686
        }
687

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

705
                $this->is_term    = true;
2✔
706
                $this->taxonomies = [ $taxonomy ];
707

708
                return $term;
709
        }
710

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

723
                return $args;
724
        }
725

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

738
                $rest_locale = $this->pll_locale_filter_with_rest();
739

740
                if ( false === $rest_locale ) {
1✔
741
                        return $locale;
742
                }
743

744
                if ( $rest_locale ) {
1✔
745
                        $this->pll_locale = $rest_locale;
746

747
                        return $this->pll_locale;
748
                }
749

750
                if ( ! is_admin() ) {
1✔
751
                        return $locale;
752
                }
753

754
                if ( ! $this->request->is_post() ) {
1✔
755
                        return $locale;
756
                }
757

758
                $pll_get_post_language = $this->pll_locale_filter_with_classic_editor();
759

760
                if ( $pll_get_post_language ) {
3✔
761
                        $this->pll_locale = $pll_get_post_language;
762

763
                        return $this->pll_locale;
764
                }
765

766
                $pll_get_term_language = $this->pll_locale_filter_with_term();
767

768
                if ( $pll_get_term_language ) {
1✔
769
                        $this->pll_locale = $pll_get_term_language;
770

771
                        return $this->pll_locale;
772
                }
773

774
                return $locale;
775
        }
776

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

787
                /**
788
                 * REST Server.
789
                 *
790
                 * @var WP_REST_Server $rest_server
791
                 */
792
                $rest_server = rest_get_server();
793

794
                try {
795
                        $data = json_decode( $rest_server::get_raw_data(), false, 512, JSON_THROW_ON_ERROR );
1✔
796
                } catch ( JsonException $e ) {
1✔
797
                        $data = new stdClass();
798
                }
799

800
                return $data->lang ?? false;
801
        }
802

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

814
                $pll_get_post_language = false;
815

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

839
                return $pll_get_post_language;
840
        }
841

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

853
                $pll_get_term_language = false;
854

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

861
                        if ( $pll_get_language ) {
1✔
862
                                $pll_get_term_language = $pll_get_language->locale;
863
                        }
864
                }
865

866
                // phpcs:enable WordPress.Security.NonceVerification.Missing
867

868
                return $pll_get_term_language;
869
        }
870

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

883
                return $locale;
884
        }
885

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

896
                return $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
897
        }
898

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

913
                $this->wpml_locale = $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
3✔
914
        }
915

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

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

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

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

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

975
                $prepared_in = '';
8✔
976
                $items       = (array) $items;
8✔
977
                $how_many    = count( $items );
978

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

986
                return $prepared_in;
987
        }
988

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

999
                $tag_var = $wp_query->query_vars['tag'] ?? null;
1000

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