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

mihdan / cyr2lat / #1083

08 Feb 2026 04:31PM UTC coverage: 90.266% (+2.6%) from 87.636%
#1083

Pull #183

php-coveralls

web-flow
Merge f7e49f071 into dfdf472f4
Pull Request #183: V6.7.0

25 of 28 new or added lines in 4 files covered. (89.29%)

2578 of 2856 relevant lines covered (90.27%)

2.28 hits per line

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

97.05
/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
         * 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 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_' ) ) {
2✔
402
                        return false;
×
403
                }
404

405
                // phpcs:disable WordPress.Security.NonceVerification.Missing
406
                $action = isset( $_POST['action'] )
2✔
407
                        ? sanitize_text_field( wp_unslash( $_POST['action'] ) )
×
408
                        : '';
2✔
409
                // phpcs:enable WordPress.Security.NonceVerification.Missing
410

411
                // Not the 'save attributes' action.
412
                if ( 'woocommerce_save_attributes' !== $action ) {
2✔
413
                        return false;
2✔
414
                }
415

416
                // phpcs:disable WordPress.Security.NonceVerification.Missing
NEW
417
                $data = isset( $_POST['data'] )
×
418
                        ? filter_input( INPUT_POST, 'data', FILTER_SANITIZE_URL )
×
NEW
419
                        : '';
×
420
                // phpcs:enable WordPress.Security.NonceVerification.Missing
421

422
                wp_parse_str( urldecode( $data ), $attributes );
×
423

424
                $attribute_names = $attributes['attribute_names'] ?? [];
×
425

NEW
426
                return in_array( $title, $attribute_names, true );
×
427
        }
428

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

440
                global $product;
6✔
441

442
                if ( ! is_a( $product, 'WC_Product' ) ) {
6✔
443
                        return false;
3✔
444
                }
445

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

449
                foreach ( $attributes as $slug => $attribute ) {
3✔
450
                        $name = $attribute['name'] ?? '';
2✔
451

452
                        if ( $name === $title && sanitize_title_with_dashes( $title ) === $slug ) {
2✔
453
                                return true;
1✔
454
                        }
455
                }
456

457
                return false;
2✔
458
        }
459

460
        /**
461
         * Check if title is an attribute.
462
         *
463
         * @param string $title Title.
464
         *
465
         * @return bool
466
         * @noinspection PhpUndefinedFunctionInspection
467
         */
468
        protected function is_wc_attribute( string $title ): bool {
469
                if ( ! function_exists( 'WC' ) ) {
28✔
470
                        return false;
16✔
471
                }
472

473
                return (
12✔
474
                        $this->is_wc_attribute_taxonomy( $title ) ||
12✔
475
                        $this->is_local_attribute( $title ) ||
12✔
476
                        $this->is_wc_product_not_converted_attribute( $title )
12✔
477
                );
12✔
478
        }
479

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

494
                $pre = apply_filters( 'ctl_pre_sanitize_filename', false, $filename );
8✔
495

496
                if ( false !== $pre ) {
8✔
497
                        return (string) $pre;
1✔
498
                }
499

500
                $filename = (string) $filename;
7✔
501
                $is_utf8  = version_compare( (string) $wp_version, '6.9-RC1', '>=' ) ? 'wp_is_valid_utf8' : 'seems_utf8';
7✔
502

503
                if ( $is_utf8( $filename ) ) {
7✔
504
                        $filename = (string) Mbstring::mb_strtolower( $filename );
7✔
505
                }
506

507
                return $this->transliterate( $filename );
7✔
508
        }
509

510
        /**
511
         * Get min suffix.
512
         *
513
         * @return string
514
         */
515
        public function min_suffix(): string {
516
                return defined( 'SCRIPT_DEBUG' ) && constant( 'SCRIPT_DEBUG' ) ? '' : '.min';
4✔
517
        }
518

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

530
                $fix = [];
26✔
531
                foreach ( $fix_table as $key => $value ) {
26✔
532
                        if ( isset( $table[ $key ] ) ) {
26✔
533
                                $fix[ $value ] = $table[ $key ];
26✔
534
                        }
535
                }
536

537
                return strtr( $str, $fix );
26✔
538
        }
539

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

553
                $chars = Mbstring::mb_str_split( $str );
2✔
554
                $str   = '';
2✔
555

556
                foreach ( $chars as $char ) {
2✔
557
                        if ( isset( $table[ $char ] ) ) {
2✔
558
                                $str .= '-' . $char . '-';
2✔
559
                        } else {
560
                                $str .= $char;
1✔
561
                        }
562
                }
563

564
                return $str;
2✔
565
        }
566

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

577
                $str = $this->fix_mac_string( $str, $table );
26✔
578
                $str = $this->split_chinese_string( $str, $table );
26✔
579

580
                return strtr( $str, $table );
26✔
581
        }
582

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

597
                if ( ! function_exists( 'is_plugin_active' ) ) {
3✔
598
                        // @codeCoverageIgnoreStart
599
                        include_once ABSPATH . 'wp-admin/includes/plugin.php';
600
                        // @codeCoverageIgnoreEnd
601
                }
602

603
                if ( is_plugin_active( 'classic-editor/classic-editor.php' ) ) {
3✔
604
                        return in_array( get_option( 'classic-editor-replace' ), [ 'no-replace', 'block' ], true );
1✔
605
                }
606

607
                if ( is_plugin_active( 'disable-gutenberg/disable-gutenberg.php' ) ) {
2✔
608
                        return ! disable_gutenberg();
1✔
609
                }
610

611
                return true;
1✔
612
        }
613

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

626
                if ( ! $this->is_gutenberg_editor_active() ) {
5✔
627
                        return $data;
2✔
628
                }
629

630
                // Run code only on post edit screen.
631
                if ( ! ( $current_screen && 'post' === $current_screen->base ) ) {
3✔
632
                        return $data;
1✔
633
                }
634

635
                if (
636
                        ! $data['post_name'] && $data['post_title'] &&
2✔
637
                        ! in_array( $data['post_status'], [ 'auto-draft', 'revision' ], true )
2✔
638
                ) {
639
                        $data['post_name'] = sanitize_title( $data['post_title'] );
1✔
640
                }
641

642
                return $data;
2✔
643
        }
644

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

662
                $this->is_term    = true;
2✔
663
                $this->taxonomies = [ $taxonomy ];
2✔
664

665
                return $term;
2✔
666
        }
667

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

680
                return $args;
3✔
681
        }
682

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

695
                $rest_locale = $this->pll_locale_filter_with_rest();
7✔
696

697
                if ( false === $rest_locale ) {
7✔
698
                        return $locale;
1✔
699
                }
700

701
                if ( $rest_locale ) {
7✔
702
                        $this->pll_locale = $rest_locale;
1✔
703

704
                        return $this->pll_locale;
1✔
705
                }
706

707
                if ( ! is_admin() ) {
6✔
708
                        return $locale;
1✔
709
                }
710

711
                if ( ! $this->request->is_post() ) {
5✔
712
                        return $locale;
1✔
713
                }
714

715
                $pll_get_post_language = $this->pll_locale_filter_with_classic_editor();
4✔
716

717
                if ( $pll_get_post_language ) {
4✔
718
                        $this->pll_locale = $pll_get_post_language;
3✔
719

720
                        return $this->pll_locale;
3✔
721
                }
722

723
                $pll_get_term_language = $this->pll_locale_filter_with_term();
4✔
724

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

728
                        return $this->pll_locale;
1✔
729
                }
730

731
                return $locale;
4✔
732
        }
733

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

744
                /**
745
                 * REST Server.
746
                 *
747
                 * @var WP_REST_Server $rest_server
748
                 */
749
                $rest_server = rest_get_server();
1✔
750

751
                try {
752
                        $data = json_decode( $rest_server::get_raw_data(), false, 512, JSON_THROW_ON_ERROR );
1✔
753
                } catch ( JsonException $e ) {
1✔
754
                        $data = new stdClass();
1✔
755
                }
756

757
                return $data->lang ?? false;
1✔
758
        }
759

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

771
                $pll_get_post_language = false;
4✔
772

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

796
                return $pll_get_post_language;
4✔
797
        }
798

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

810
                $pll_get_term_language = false;
1✔
811

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

818
                        if ( $pll_get_language ) {
1✔
819
                                $pll_get_term_language = $pll_get_language->locale;
1✔
820
                        }
821
                }
822

823
                // phpcs:enable WordPress.Security.NonceVerification.Missing
824

825
                return $pll_get_term_language;
1✔
826
        }
827

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

840
                return $locale;
1✔
841
        }
842

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

853
                return $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
2✔
854
        }
855

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

870
                $this->wpml_locale = $this->wpml_languages[ $language_code ]['default_locale'] ?? null;
3✔
871
        }
872

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

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

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

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

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

932
                $prepared_in = '';
8✔
933
                $items       = (array) $items;
8✔
934
                $how_many    = count( $items );
8✔
935

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

943
                return $prepared_in;
8✔
944
        }
945

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

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

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