Plugin Directory

source: polylang/trunk/src/Model/Languages.php

Last change on this file was 3467776, checked in by Chouby, 5 weeks ago

Version 3.8-beta2 in trunk

File size: 45.9 KB
Line 
1<?php
2/**
3 * @package Polylang
4 */
5
6namespace WP_Syntex\Polylang\Model;
7
8use PLL_Cache;
9use PLL_Language;
10use PLL_Language_Factory;
11use PLL_Translatable_Objects;
12use WP_Error;
13use WP_Term;
14use WP_Syntex\Polylang\Options\Options;
15
16defined( 'ABSPATH' ) || exit;
17
18/**
19 * Model for the languages.
20 *
21 * @since 3.7
22 */
23class Languages {
24        public const INNER_LOCALE_PATTERN = '[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?';
25        public const INNER_SLUG_PATTERN   = '[a-z][a-z0-9_-]*';
26
27        public const LOCALE_PATTERN = '^' . self::INNER_LOCALE_PATTERN . '$';
28        public const SLUG_PATTERN   = '^' . self::INNER_SLUG_PATTERN . '$';
29
30        public const TRANSIENT_NAME = 'pll_languages_list';
31        private const CACHE_KEY     = 'languages';
32
33        /**
34         * Polylang's options.
35         *
36         * @var Options
37         */
38        private $options;
39
40        /**
41         * Translatable objects registry.
42         *
43         * @var PLL_Translatable_Objects
44         */
45        private $translatable_objects;
46
47        /**
48         * Internal non persistent cache object.
49         *
50         * @var PLL_Cache<mixed>
51         */
52        private $cache;
53
54        /**
55         * Flag set to true during the language objects creation.
56         *
57         * @var bool
58         */
59        private $is_creating_list = false;
60
61        /**
62         * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
63         *
64         * @var bool
65         */
66        private $languages_ready = false;
67
68        /**
69         * List of automatic language proxies.
70         *
71         * @var Languages_Proxy_Interface[]
72         *
73         * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
74         */
75        private $automatic_proxies = array();
76
77        /**
78         * List of language proxies.
79         *
80         * @var Languages_Proxy_Interface[]
81         *
82         * @phpstan-var array<non-falsy-string, Languages_Proxy_Interface>
83         */
84        private $proxies = array();
85
86        /**
87         * Constructor.
88         *
89         * @since 3.7
90         *
91         * @param Options                  $options              Polylang's options.
92         * @param PLL_Translatable_Objects $translatable_objects Translatable objects registry.
93         * @param PLL_Cache                $cache                Internal non persistent cache object.
94         *
95         * @phpstan-param PLL_Cache<mixed> $cache
96         */
97        public function __construct( Options $options, PLL_Translatable_Objects $translatable_objects, PLL_Cache $cache ) {
98                $this->options              = $options;
99                $this->translatable_objects = $translatable_objects;
100                $this->cache                = $cache;
101        }
102
103        /**
104         * Returns the language by its term_id, tl_term_id, slug or locale.
105         *
106         * @since 0.1
107         * @since 3.4 Allow to get a language by `term_taxonomy_id`.
108         * @since 3.7 Moved from `PLL_Model::get_language()` to `WP_Syntex\Polylang\Model\Languages::get()`.
109         *
110         * @param mixed $value `term_id`, `term_taxonomy_id`, `slug`, `locale`, or `w3c` of the queried language.
111         *                     `term_id` and `term_taxonomy_id` can be fetched for any language taxonomy.
112         *                     /!\ For the `term_taxonomy_id`, prefix the ID by `tt:` (ex: `"tt:{$tt_id}"`),
113         *                     this is to prevent confusion between `term_id` and `term_taxonomy_id`.
114         * @return PLL_Language|false Language object, false if no language found.
115         *
116         * @phpstan-param PLL_Language|WP_Term|int|string $value
117         */
118        public function get( $value ) {
119                if ( $value instanceof PLL_Language ) {
120                        return $value;
121                }
122
123                // Cast WP_Term to PLL_Language.
124                if ( $value instanceof WP_Term ) {
125                        return $this->get( $value->term_id );
126                }
127
128                $return = $this->cache->get( 'language:' . $value );
129
130                if ( $return instanceof PLL_Language ) {
131                        return $return;
132                }
133
134                foreach ( $this->get_list() as $lang ) {
135                        foreach ( $lang->get_tax_props() as $props ) {
136                                $this->cache->set( 'language:' . $props['term_id'], $lang );
137                                $this->cache->set( 'language:tt:' . $props['term_taxonomy_id'], $lang );
138                        }
139                        $this->cache->set( 'language:' . $lang->slug, $lang );
140                        $this->cache->set( 'language:' . $lang->locale, $lang );
141                        $this->cache->set( 'language:' . $lang->w3c, $lang );
142                }
143
144                /** @var PLL_Language|false */
145                return $this->cache->get( 'language:' . $value );
146        }
147
148        /**
149         * Adds a new language and creates a default category for this language.
150         *
151         * @since 1.2
152         * @since 3.7 Moved from `PLL_Admin_Model::add_language()` to `WP_Syntex\Polylang\Model\Languages::add()`.
153         * @since 3.8 Returns the new object language.
154         *
155         * @param array $args {
156         *   Arguments used to create the language.
157         *
158         *   @type string $locale         WordPress locale. If something wrong is used for the locale, the .mo files will
159         *                                not be loaded...
160         *   @type string $name           Optional. Language name (used only for display). Default to the language name from {@see src/settings/languages.php}.
161         *   @type string $slug           Optional. Language code (ideally 2-letters ISO 639-1 language code). Default to the language code from {@see src/settings/languages.php}.
162         *   @type bool   $rtl            Optional. True if rtl language, false otherwise. Default is false.
163         *   @type bool   $is_rtl         Optional. True if rtl language, false otherwise. Will be converted to rtl. Default is false.
164         *   @type int    $term_group     Optional. Language order when displayed. Default is 0.
165         *   @type string $flag           Optional. Country code, {@see src/settings/flags.php}.
166         *   @type string $flag_code      Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
167         *   @type bool   $no_default_cat Optional. If set, no default category will be created for this language. Default is false.
168         * }
169         * @return PLL_Language|WP_Error The object language on success, a `WP_Error` otherwise.
170         *
171         * @phpstan-param array{
172         *     name?: string,
173         *     slug?: string,
174         *     locale?: string,
175         *     rtl?: bool,
176         *     is_rtl?: bool,
177         *     term_group?: int|numeric-string,
178         *     flag?: string,
179         *     flag_code?: string,
180         *     no_default_cat?: bool
181         * } $args
182         */
183        public function add( $args ) {
184                $args['rtl']        = $args['rtl'] ?? $args['is_rtl'] ?? null;
185                $args['flag']       = $args['flag'] ?? $args['flag_code'] ?? null;
186                $args['term_group'] = $args['term_group'] ?? 0;
187
188                if ( ! empty( $args['locale'] ) && ( ! isset( $args['name'] ) || ! isset( $args['slug'] ) ) ) {
189                        $languages = include POLYLANG_DIR . '/src/settings/languages.php';
190                        if ( ! empty( $languages[ $args['locale'] ] ) ) {
191                                $found        = $languages[ $args['locale'] ];
192                                $args['name'] = $args['name'] ?? $found['name'];
193                                $args['slug'] = $args['slug'] ?? $found['code'];
194                                $args['rtl']  = $args['rtl'] ?? 'rtl' === $found['dir'];
195                                $args['flag'] = $args['flag'] ?? $found['flag'];
196                        }
197                }
198
199                $errors = $this->validate_lang( $args );
200                if ( $errors->has_errors() ) {
201                        return $errors;
202                }
203
204                /**
205                 * @phpstan-var array{
206                 *     name: non-empty-string,
207                 *     slug:  non-empty-string,
208                 *     locale:  non-empty-string,
209                 *     rtl: bool,
210                 *     term_group: int|numeric-string,
211                 *     flag?:  non-empty-string,
212                 *     no_default_cat?: bool
213                 * } $args
214                 */
215                // First the language taxonomy.
216                $result = wp_insert_term(
217                        $args['name'],
218                        'language',
219                        array(
220                                'slug'        => $args['slug'],
221                                'description' => $this->build_metas( $args ),
222                        )
223                );
224                if ( is_wp_error( $result ) ) {
225                        return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
226                }
227
228                $id = (int) $result['term_id'];
229
230                $result = wp_update_term( $id, 'language', array( 'term_group' => (int) $args['term_group'] ) ); // Can't set the term group directly in `wp_insert_term()`.
231                if ( is_wp_error( $result ) ) {
232                        return new WP_Error( 'pll_add_language', __( 'Could not set the language order.', 'polylang' ) );
233                }
234
235                // The other language taxonomies.
236                $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'] );
237                if ( $errors->has_errors() ) {
238                        return $errors;
239                }
240
241                if ( empty( $this->options['default_lang'] ) ) {
242                        // If this is the first language created, set it as default language
243                        $this->options['default_lang'] = $args['slug'];
244                }
245
246                // Refresh languages.
247                $this->clean_cache();
248                $new_language = $this->get( $id );
249
250                if ( ! $new_language ) {
251                        return new WP_Error( 'pll_add_language', __( 'Could not add the language.', 'polylang' ) );
252                }
253
254                flush_rewrite_rules(); // Refresh rewrite rules.
255
256                /**
257                 * Fires after a language is added.
258                 *
259                 * @since 1.9
260                 *
261                 * @param array $args {
262                 *   Arguments used to create the language.
263                 *
264                 *   @type string $name           Language name (used only for display).
265                 *   @type string $slug           Language code (ideally 2-letters ISO 639-1 language code).
266                 *   @type string $locale         WordPress locale. If something wrong is used for the locale, the .mo files will
267                 *                                not be loaded...
268                 *   @type bool   $rtl            True if rtl language, false otherwise.
269                 *   @type int    $term_group     Language order when displayed.
270                 *   @type string $flag           Optional. Country code, {@see src/settings/flags.php}.
271                 *   @type bool   $no_default_cat Optional. If set, no default category will be created for this language.
272                 * }
273                 */
274                do_action( 'pll_add_language', $args );
275
276                return $new_language;
277        }
278
279        /**
280         * Updates language properties.
281         *
282         * @since 1.2
283         * @since 3.7 Moved from `PLL_Admin_Model::update_language()` to `WP_Syntex\Polylang\Model\Languages::update()`.
284         * @since 3.8 Returns the updated object language.
285         *
286         * @param array $args {
287         *   Arguments used to modify the language.
288         *
289         *   @type int    $lang_id    ID of the language to modify.
290         *   @type string $name       Optional. Language name (used only for display).
291         *   @type string $slug       Optional. Language code (ideally 2-letters ISO 639-1 language code).
292         *   @type string $locale     Optional. WordPress locale. If something wrong is used for the locale, the .mo files will
293         *                            not be loaded...
294         *   @type bool   $rtl        Optional. True if rtl language, false otherwise.
295         *   @type bool   $is_rtl     Optional. True if rtl language, false otherwise. Will be converted to rtl.
296         *   @type int    $term_group Optional. Language order when displayed.
297         *   @type string $flag       Optional, country code, {@see src/settings/flags.php}.
298         *   @type string $flag_code  Optional. Country code, {@see src/settings/flags.php}. Will be converted to flag.
299         * }
300         * @return PLL_Language|WP_Error The updated object language on success, a `WP_Error` otherwise.
301         *
302         * @phpstan-param array{
303         *     lang_id: int|numeric-string,
304         *     name?: string,
305         *     slug?: string,
306         *     locale?: string,
307         *     rtl?: bool,
308         *     is_rtl?: bool,
309         *     term_group?: int|numeric-string,
310         *     flag?: string,
311         *     flag_code?: string
312         * } $args
313         */
314        public function update( $args ) {
315                $id   = (int) $args['lang_id'];
316                $lang = $this->get( $id );
317
318                if ( empty( $lang ) ) {
319                        return new WP_Error( 'pll_invalid_language_id', __( 'The language does not seem to exist.', 'polylang' ) );
320                }
321
322                $args['locale']     = $args['locale'] ?? $lang->locale;
323                $args['name']       = $args['name'] ?? $lang->name;
324                $args['slug']       = $args['slug'] ?? $lang->slug;
325                $args['rtl']        = $args['rtl'] ?? $args['is_rtl'] ?? $lang->is_rtl;
326                $args['flag']       = $args['flag'] ?? $args['flag_code'] ?? $lang->flag_code;
327                $args['term_group'] = $args['term_group'] ?? $lang->term_group;
328
329                $errors = $this->validate_lang( $args, $lang );
330                if ( $errors->has_errors() ) {
331                        return $errors;
332                }
333
334                /**
335                 * @phpstan-var array{
336                 *     lang_id: int|numeric-string,
337                 *     name: non-empty-string,
338                 *     slug:  non-empty-string,
339                 *     locale:  non-empty-string,
340                 *     rtl: bool,
341                 *     term_group: int|numeric-string,
342                 *     flag?:  non-empty-string
343                 * } $args
344                 */
345                // Update links to this language in posts and terms in case the slug has been modified.
346                $slug     = $args['slug'];
347                $old_slug = $lang->slug;
348
349                // Update the language itself.
350                $errors = $this->update_secondary_language_terms( $args['slug'], $args['name'], $lang );
351                if ( $errors->has_errors() ) {
352                        return $errors;
353                }
354
355                $result = wp_update_term(
356                        $lang->get_tax_prop( 'language', 'term_id' ),
357                        'language',
358                        array(
359                                'slug'        => $slug,
360                                'name'        => $args['name'],
361                                'description' => $this->build_metas( $args ),
362                                'term_group'  => (int) $args['term_group'],
363                        )
364                );
365                if ( is_wp_error( $result ) ) {
366                        return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
367                }
368
369                if ( $old_slug !== $slug ) {
370                        // Update the language slug in translations.
371                        $errors = $this->update_translations( $old_slug, $slug );
372                        if ( $errors->has_errors() ) {
373                                return $errors;
374                        }
375
376                        // Update language option in widgets.
377                        foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
378                                if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
379                                        $obj = $widget['callback'][0];
380                                        $number = $widget['params'][0]['number'];
381                                        if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
382                                                $settings = $obj->get_settings();
383                                                if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
384                                                        $settings[ $number ]['pll_lang'] = $slug;
385                                                        $obj->save_settings( $settings );
386                                                }
387                                        }
388                                }
389                        }
390
391                        // Update menus locations in options.
392                        $nav_menus = $this->options->get( 'nav_menus' );
393
394                        if ( ! empty( $nav_menus ) ) {
395                                foreach ( $nav_menus as $theme => $locations ) {
396                                        foreach ( array_keys( $locations ) as $location ) {
397                                                if ( isset( $nav_menus[ $theme ][ $location ][ $old_slug ] ) ) {
398                                                        $nav_menus[ $theme ][ $location ][ $slug ] = $nav_menus[ $theme ][ $location ][ $old_slug ];
399                                                        unset( $nav_menus[ $theme ][ $location ][ $old_slug ] );
400                                                }
401                                        }
402                                }
403
404                                $this->options->set( 'nav_menus', $nav_menus );
405                        }
406
407                        /*
408                         * Update domains in options.
409                         * This must happen after the term is saved (see `Options\Business\Domains::sanitize()`).
410                         */
411                        $domains = $this->options->get( 'domains' );
412
413                        if ( isset( $domains[ $old_slug ] ) ) {
414                                $domains[ $slug ] = $domains[ $old_slug ];
415                                unset( $domains[ $old_slug ] );
416                                $this->options->set( 'domains', $domains );
417                        }
418
419                        /*
420                         * Update the default language option if necessary.
421                         * This must happen after the term is saved (see `Options\Business\Default_Lang::sanitize()`).
422                         */
423                        if ( $lang->is_default ) {
424                                $this->options->set( 'default_lang', $slug );
425                        }
426                }
427
428                // Refresh languages.
429                $this->clean_cache();
430                $updated_language = $this->get( $id );
431
432                if ( ! $updated_language ) {
433                        return new WP_Error( 'pll_update_language', __( 'Could not update the language.', 'polylang' ) );
434                }
435
436                // Refresh rewrite rules.
437                flush_rewrite_rules();
438
439                /**
440                 * Fires after a language is updated.
441                 *
442                 * @since 1.9
443                 * @since 3.2 Added $lang parameter.
444                 *
445                 * @param array $args {
446                 *   Arguments used to modify the language. @see WP_Syntex\Polylang\Model\Languages::update().
447                 *
448                 *   @type string $name           Language name (used only for display).
449                 *   @type string $slug           Language code (ideally 2-letters ISO 639-1 language code).
450                 *   @type string $locale         WordPress locale.
451                 *   @type bool   $rtl            True if rtl language, false otherwise.
452                 *   @type int    $term_group     Language order when displayed.
453                 *   @type string $no_default_cat Optional, if set, no default category has been created for this language.
454                 *   @type string $flag           Optional, country code, @see src/settings/flags.php.
455                 * }
456                 * @param PLL_Language $lang Previous value of the language being edited.
457                 */
458                do_action( 'pll_update_language', $args, $lang );
459
460                return $updated_language;
461        }
462
463        /**
464         * Deletes a language.
465         *
466         * @since 1.2
467         * @since 3.7 Moved from `PLL_Admin_Model::delete_language()` to `WP_Syntex\Polylang\Model\Languages::delete()`.
468         *
469         * @param int $lang_id Language term_id.
470         * @return bool
471         */
472        public function delete( $lang_id ): bool {
473                $lang = $this->get( (int) $lang_id );
474
475                if ( empty( $lang ) ) {
476                        return false;
477                }
478
479                // Oops! We are deleting the default language...
480                // Need to do this before losing the information for default category translations.
481                if ( $lang->is_default ) {
482                        $slugs = $this->get_list( array( 'fields' => 'slug' ) );
483                        $slugs = array_diff( $slugs, array( $lang->slug ) );
484
485                        if ( ! empty( $slugs ) ) {
486                                $this->update_default( reset( $slugs ) ); // Arbitrary choice...
487                        } else {
488                                unset( $this->options['default_lang'] );
489                        }
490                }
491
492                // Delete the translations.
493                $this->update_translations( $lang->slug );
494
495                // Delete language option in widgets.
496                foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
497                        if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
498                                $obj = $widget['callback'][0];
499                                $number = $widget['params'][0]['number'];
500                                if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
501                                        $settings = $obj->get_settings();
502                                        if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
503                                                unset( $settings[ $number ]['pll_lang'] );
504                                                $obj->save_settings( $settings );
505                                        }
506                                }
507                        }
508                }
509
510                // Delete menus locations.
511                $nav_menus = $this->options->get( 'nav_menus' );
512
513                if ( ! empty( $nav_menus ) ) {
514                        foreach ( $nav_menus as $theme => $locations ) {
515                                foreach ( array_keys( $locations ) as $location ) {
516                                        unset( $nav_menus[ $theme ][ $location ][ $lang->slug ] );
517                                }
518                        }
519
520                        $this->options->set( 'nav_menus', $nav_menus );
521                }
522
523                // Delete users options.
524                delete_metadata( 'user', 0, 'pll_filter_content', '', true );
525                delete_metadata( 'user', 0, "description_{$lang->slug}", '', true );
526
527                // Delete domain.
528                $domains = $this->options->get( 'domains' );
529                unset( $domains[ $lang->slug ] );
530                $this->options->set( 'domains', $domains );
531
532                /*
533                 * Delete the language itself.
534                 *
535                 * Reverses the language taxonomies order is required to make sure 'language' is deleted in last.
536                 *
537                 * The initial order with the 'language' taxonomy at the beginning of 'PLL_Language::term_props' property
538                 * is done by {@see PLL_Model::filter_terms_orderby()}
539                 */
540                foreach ( array_reverse( $lang->get_tax_props( 'term_id' ) ) as $taxonomy_name => $term_id ) {
541                        wp_delete_term( $term_id, $taxonomy_name );
542                }
543
544                // Refresh languages.
545                $this->clean_cache();
546                $this->get_list();
547
548                flush_rewrite_rules(); // refresh rewrite rules
549                return true;
550        }
551
552        /**
553         * Checks if there are languages or not.
554         *
555         * @since 3.3
556         * @since 3.7 Moved from `PLL_Model::has_languages()` to `WP_Syntex\Polylang\Model\Languages::has()`.
557         *
558         * @return bool True if there are, false otherwise.
559         */
560        public function has(): bool {
561                if ( ! empty( $this->cache->get( self::CACHE_KEY ) ) ) {
562                        return true;
563                }
564
565                if ( ! empty( get_transient( self::TRANSIENT_NAME ) ) ) {
566                        return true;
567                }
568
569                return ! empty( $this->get_terms() );
570        }
571
572        /**
573         * Returns the list of available languages.
574         * - Stores the list in a db transient (except flags), unless `PLL_CACHE_LANGUAGES` is set to false.
575         * - Caches the list (with flags) in a `PLL_Cache` object.
576         *
577         * @since 0.1
578         * @since 3.7 Moved from `PLL_Model::get_languages_list()` to `WP_Syntex\Polylang\Model\Languages::get_list()`.
579         *
580         * @param array $args {
581         *   @type string $fields       Returns only that field if set; {@see PLL_Language} for a list of fields.
582         *   @type bool   $hide_empty   Hides languages with no posts if set to `true` (defaults to `false`). Deprecated, use the `hide_empty` filter instead.
583         *   @type bool   $hide_default Hides default language from the list (default to `false`). Deprecated, use the `hide_default` filter instead.
584         * }
585         * @return array List of PLL_Language objects or PLL_Language object properties.
586         */
587        public function get_list( $args = array() ): array {
588                if ( ! $this->are_ready() ) {
589                        _doing_it_wrong(
590                                __METHOD__ . '()',
591                                "It must not be called before the hook 'pll_pre_init'.",
592                                '3.4'
593                        );
594                }
595
596                if ( ! empty( $args['hide_empty'] ) ) {
597                        _deprecated_argument(
598                                __METHOD__,
599                                '3.8',
600                                sprintf(
601                                        /* translators: 1: argument name, 2: PHP code snippet. */
602                                        esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
603                                        '<code>hide_empty</code>',
604                                        '<code>PLL()->model->languages->filter( \'hide_empty\' )->get_list()</code>'
605                                )
606                        );
607                }
608
609                if ( ! empty( $args['hide_default'] ) ) {
610                        _deprecated_argument(
611                                __METHOD__,
612                                '3.8',
613                                sprintf(
614                                        /* translators: 1: argument name, 2: PHP code snippet. */
615                                        esc_html__( 'The argument %1$s has been replaced by %2$s.', 'polylang' ),
616                                        '<code>hide_default</code>',
617                                        '<code>PLL()->model->languages->filter( \'hide_default\' )->get_list()</code>'
618                                )
619                        );
620                }
621
622                $languages = $this->cache->get( self::CACHE_KEY );
623
624                if ( ! is_array( $languages ) ) {
625                        // Bail out early if languages are currently created to avoid an infinite loop.
626                        if ( $this->is_creating_list ) {
627                                return array();
628                        }
629
630                        $this->is_creating_list = true;
631
632                        if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) ) {
633                                // Create the languages from taxonomies.
634                                $languages = $this->get_from_taxonomies();
635                        } else {
636                                $languages = get_transient( self::TRANSIENT_NAME );
637
638                                if ( empty( $languages ) || ! is_array( $languages ) || empty( reset( $languages )['term_props'] ) ) { // Test `term_props` in case we got a transient older than 3.4.
639                                        // Create the languages from taxonomies.
640                                        $languages = $this->get_from_taxonomies();
641                                } else {
642                                        // Create the languages directly from arrays stored in the transient.
643                                        $languages = array_map(
644                                                array( new PLL_Language_Factory( $this->options ), 'get' ),
645                                                $languages
646                                        );
647
648                                        // Remove potential empty language.
649                                        $languages = array_filter( $languages );
650
651                                        // Re-index.
652                                        $languages = array_values( $languages );
653                                }
654                        }
655
656                        /**
657                         * Filters the list of languages *after* it is stored in the persistent cache.
658                         * /!\ This filter is fired *before* the $polylang object is available.
659                         *
660                         * @since 1.8
661                         * @since 3.4 Deprecated. If you used this hook to filter URLs, you may hook `'site_url'` instead.
662                         * @deprecated
663                         *
664                         * @param PLL_Language[] $languages The list of language objects.
665                         */
666                        $languages = apply_filters_deprecated( 'pll_after_languages_cache', array( $languages ), '3.4' );
667
668                        foreach ( $this->automatic_proxies as $proxy ) {
669                                $languages = $proxy->filter( $languages );
670                        }
671
672                        if ( $this->are_ready() ) {
673                                $this->cache->set( self::CACHE_KEY, $languages );
674                        }
675
676                        $this->is_creating_list = false;
677                }
678
679                // Backward compatibility with Polylang < 3.8.
680                $languages = array_filter(
681                        $languages,
682                        function ( $lang ) use ( $args ) {
683                                $keep_empty   = empty( $args['hide_empty'] ) || $lang->get_tax_prop( 'language', 'count' );
684                                $keep_default = empty( $args['hide_default'] ) || ! $lang->is_default;
685                                return $keep_empty && $keep_default;
686                        }
687                );
688
689                $languages = array_values( $languages ); // Re-index.
690                return $this->maybe_convert_list( $languages, (array) $args );
691        }
692
693        /**
694         * Tells if {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
695         *
696         * @since 3.4
697         * @since 3.7 Moved from `PLL_Model::are_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::are_ready()`.
698         *
699         * @return bool
700         */
701        public function are_ready(): bool {
702                return $this->languages_ready;
703        }
704
705        /**
706         * Sets the internal property `$languages_ready` to `true`, telling that {@see WP_Syntex\Polylang\Model\Languages::get_list()} can be used.
707         *
708         * @since 3.4
709         * @since 3.7 Moved from `PLL_Model::set_languages_ready()` to `WP_Syntex\Polylang\Model\Languages::set_ready()`.
710         *
711         * @return void
712         */
713        public function set_ready(): void {
714                $this->languages_ready = true;
715        }
716
717        /**
718         * Returns the default language.
719         *
720         * @since 3.4
721         * @since 3.7 Moved from `PLL_Model::get_default_language()` to `WP_Syntex\Polylang\Model\Languages::get_default()`.
722         *
723         * @return PLL_Language|false Default language object, `false` if no language found.
724         */
725        public function get_default() {
726                if ( empty( $this->options['default_lang'] ) ) {
727                        return false;
728                }
729
730                return $this->get( $this->options['default_lang'] );
731        }
732
733        /**
734         * Updates the default language.
735         * Takes care to update default category, nav menu locations, and flushes cache and rewrite rules.
736         *
737         * @since 1.8
738         * @since 3.7 Moved from `PLL_Admin_Model::update_default_lang()` to `WP_Syntex\Polylang\Model\Languages::update_default()`.
739         *            Returns a `WP_Error` object.
740         *
741         * @param string $slug New language slug.
742         * @return WP_Error A `WP_Error` object containing possible errors during slug validation/sanitization.
743         */
744        public function update_default( $slug ): WP_Error {
745                $prev_default_lang = $this->options->get( 'default_lang' );
746
747                if ( $prev_default_lang === $slug ) {
748                        return new WP_Error();
749                }
750
751                $errors = $this->options->set( 'default_lang', $slug );
752
753                if ( $errors->has_errors() ) {
754                        return $errors;
755                }
756
757                // The nav menus stored in theme locations should be in the default language.
758                $theme = get_stylesheet();
759                if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
760                        $menus = array();
761
762                        foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
763                                $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
764                        }
765                        set_theme_mod( 'nav_menu_locations', $menus );
766                }
767
768                /**
769                 * Fires when a default language is updated.
770                 *
771                 * @since 3.1
772                 * @since 3.7 The previous default language's slug is passed as 2nd param.
773                 *            The default language is updated before this hook is fired.
774                 *
775                 * @param string $slug              New default language's slug.
776                 * @param string $prev_default_lang Previous default language's slug.
777                 */
778                do_action( 'pll_update_default_lang', $slug, $prev_default_lang );
779
780                // Update options.
781
782                $this->clean_cache();
783                flush_rewrite_rules();
784
785                return new WP_Error();
786        }
787
788        /**
789         * Maybe adds the missing language terms for 3rd party language taxonomies.
790         *
791         * @since 3.4
792         * @since 3.7 Moved from `PLL_Model::maybe_create_language_terms()` to `WP_Syntex\Polylang\Model\Languages::maybe_create_terms()`.
793         *
794         * @return void
795         */
796        public function maybe_create_terms(): void {
797                $registered_taxonomies = array_diff(
798                        $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
799                        // Exclude the post and term language taxonomies from the list.
800                        array(
801                                $this->translatable_objects->get( 'post' )->get_tax_language(),
802                                $this->translatable_objects->get( 'term' )->get_tax_language(),
803                        )
804                );
805
806                if ( empty( $registered_taxonomies ) ) {
807                        // No 3rd party language taxonomies.
808                        return;
809                }
810
811                // We have at least one 3rd party language taxonomy.
812                $known_taxonomies = get_option( 'pll_language_taxonomies', array() );
813                $known_taxonomies = is_array( $known_taxonomies ) ? $known_taxonomies : array();
814                $new_taxonomies   = array_diff( $registered_taxonomies, $known_taxonomies );
815
816                if ( empty( $new_taxonomies ) ) {
817                        // No new 3rd party language taxonomies.
818                        return;
819                }
820
821                // We have at least one unknown 3rd party language taxonomy.
822                foreach ( $this->get_list() as $language ) {
823                        $this->update_secondary_language_terms( $language->slug, $language->name, $language, $new_taxonomies );
824                }
825
826                // Clear the cache, so the new `term_id` and `term_taxonomy_id` appear in the languages list.
827                $this->clean_cache();
828
829                // Keep the previous values, so this is triggered only once per taxonomy.
830                update_option( 'pll_language_taxonomies', array_merge( $known_taxonomies, $new_taxonomies ) );
831        }
832
833        /**
834         * Cleans language cache.
835         *
836         * @since 3.7
837         *
838         * @return void
839         */
840        public function clean_cache(): void {
841                delete_transient( self::TRANSIENT_NAME );
842                $this->clean_local_cache();
843        }
844
845        /**
846         * Cleans local language cache.
847         *
848         * @since 3.8
849         *
850         * @return void
851         */
852        public function clean_local_cache(): void {
853                $this->cache->clean();
854        }
855
856        /**
857         * Deletes the transient from the options table since WordPress does not do it when using object cache.
858         *
859         * @since 3.8
860         *
861         * @return void
862         */
863        public function delete_transient_from_options_table(): void {
864                if ( wp_using_ext_object_cache() || wp_installing() ) {
865                        delete_option( '_transient_' . self::TRANSIENT_NAME );
866                }
867        }
868
869        /**
870         * Applies arguments that change the type of the elements of the given list of languages.
871         *
872         * @since 3.8
873         *
874         * @param PLL_Language[] $languages The list of language objects.
875         * @param array          $args {
876         *   @type string $fields Optional. Returns only that field if set; {@see PLL_Language} for a list of fields.
877         * }
878         * @return array List of `PLL_Language` objects or `PLL_Language` object properties.
879         */
880        public function maybe_convert_list( array $languages, array $args ): array {
881                if ( ! empty( $args['fields'] ) ) {
882                        return wp_list_pluck( $languages, $args['fields'] );
883                }
884                return $languages;
885        }
886
887        /**
888         * Registers languages proxies.
889         *
890         * @since 3.8
891         *
892         * @param Languages_Proxy_Interface $proxy Proxy instance.
893         * @param string                    $mode  Optional. Tell how the proxy must be applied. Possible values are:
894         *                                         - `callable`: the proxy must be called manually with `filter()`.
895         *                                         - `automatic`: the proxy is meant to be always called, automatically.
896         *                                         Default is `callable`.
897         * @return self
898         *
899         * @phpstan-param 'callable'|'automatic' $mode
900         */
901        public function register_proxy( Languages_Proxy_Interface $proxy, string $mode = 'callable' ): self {
902                if ( 'automatic' === $mode ) {
903                        $this->automatic_proxies[ $proxy->key() ] = $proxy;
904                } else {
905                        $this->proxies[ $proxy->key() ] = $proxy;
906                }
907                return $this;
908        }
909
910        /**
911         * Stacks a proxy that will filter the list of languages.
912         *
913         * @since 3.8
914         *
915         * @param string $key Proxy's key.
916         * @return Languages_Proxies
917         */
918        public function filter( string $key ): Languages_Proxies {
919                return new Languages_Proxies( $this, $this->proxies, $key );
920        }
921
922        /**
923         * Builds the language metas into an array and serializes it, to be stored in the term description.
924         *
925         * @since 3.4
926         * @since 3.7 Moved from `PLL_Admin_Model::build_language_metas()` to `WP_Syntex\Polylang\Model\Languages::build_metas()`.
927         *
928         * @param array $args {
929         *   Arguments used to build the language metas.
930         *
931         *   @type string $name       Language name (used only for display).
932         *   @type string $slug       Language code (ideally 2-letters ISO 639-1 language code).
933         *   @type string $locale     WordPress locale. If something wrong is used for the locale, the .mo files will not
934         *                            be loaded...
935         *   @type bool   $rtl        True if rtl language, false otherwise.
936         *   @type int    $term_group Language order when displayed.
937         *   @type int    $lang_id    Optional, ID of the language to modify. An empty value means the language is being
938         *                            created.
939         *   @type string $flag       Optional, country code, {@see src/settings/flags.php}.
940         * }
941         * @return string The serialized description array updated.
942         *
943         * @phpstan-param array{
944         *     name: non-empty-string,
945         *     slug: non-empty-string,
946         *     locale: non-empty-string,
947         *     rtl: bool,
948         *     term_group: int|numeric-string,
949         *     lang_id?: int|numeric-string,
950         *     flag?: non-empty-string
951         * } $args
952         */
953        protected function build_metas( array $args ): string {
954                if ( ! empty( $args['lang_id'] ) ) {
955                        $language_term = get_term( (int) $args['lang_id'] );
956
957                        if ( $language_term instanceof WP_Term ) {
958                                $old_data = maybe_unserialize( $language_term->description );
959                        }
960                }
961
962                if ( empty( $old_data ) || ! is_array( $old_data ) ) {
963                        $old_data = array();
964                }
965
966                $new_data = array(
967                        'locale'    => $args['locale'],
968                        'rtl'       => ! empty( $args['rtl'] ),
969                        'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'],
970                );
971
972                /**
973                 * Allow to add data to store for a language.
974                 * `$locale`, `$rtl`, and `$flag_code` cannot be overwritten.
975                 *
976                 * @since 3.4
977                 *
978                 * @param mixed[] $add_data Data to add.
979                 * @param mixed[] $args     {
980                 *     Arguments used to create the language.
981                 *
982                 *     @type string $name       Language name (used only for display).
983                 *     @type string $slug       Language code (ideally 2-letters ISO 639-1 language code).
984                 *     @type string $locale     WordPress locale. If something wrong is used for the locale, the .mo files will
985                 *                              not be loaded...
986                 *     @type bool   $rtl        True if rtl language, false otherwise.
987                 *     @type int    $term_group Language order when displayed.
988                 *     @type int    $lang_id    Optional, ID of the language to modify. An empty value means the language is
989                 *                              being created.
990                 *     @type string $flag       Optional, country code, {@see src/settings/flags.php}.
991                 * }
992                 * @param mixed[] $new_data New data.
993                 * @param mixed[] $old_data {
994                 *     Original data. Contains at least the following:
995                 *
996                 *     @type string $locale    WordPress locale.
997                 *     @type bool   $rtl       True if rtl language, false otherwise.
998                 *     @type string $flag_code Country code.
999                 * }
1000                 */
1001                $add_data = apply_filters( 'pll_language_metas', array(), $args, $new_data, $old_data );
1002                // Don't allow to overwrite `$locale`, `$rtl`, and `$flag_code`.
1003                $new_data = array_merge( $old_data, $add_data, $new_data );
1004
1005                /** @var non-empty-string $serialized maybe_serialize() cannot return anything else than a string when fed by an array. */
1006                $serialized = maybe_serialize( $new_data );
1007                return $serialized;
1008        }
1009
1010        /**
1011         * Validates data entered when creating or updating a language.
1012         *
1013         * @since 0.4
1014         * @since 3.7 Moved from `PLL_Admin_Model::validate_lang()` to `WP_Syntex\Polylang\Model\Languages::validate_lang()`.
1015         *
1016         * @param array             $args Parameters of {@see WP_Syntex\Polylang\Model\Languages::add() or @see WP_Syntex\Polylang\Model\Languages::update()}.
1017         * @param PLL_Language|null $lang Optional the language currently updated, the language is created if not set.
1018         * @return WP_Error
1019         *
1020         * @phpstan-param array{
1021         *     locale?: string,
1022         *     slug?: string,
1023         *     name?: string,
1024         *     flag?: string
1025         * } $args
1026         */
1027        protected function validate_lang( $args, ?PLL_Language $lang = null ): WP_Error {
1028                $errors = new WP_Error();
1029
1030                // Validate locale with the same pattern as WP 4.3. See #28303.
1031                if ( empty( $args['locale'] ) || ! preg_match( '#' . self::LOCALE_PATTERN . '#', $args['locale'], $matches ) ) {
1032                        $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
1033                }
1034
1035                // Validate slug characters.
1036                if ( empty( $args['slug'] ) || ! preg_match( '#' . self::SLUG_PATTERN . '#', $args['slug'] ) ) {
1037                        $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
1038                }
1039
1040                // Validate slug is unique.
1041                foreach ( $this->get_list() as $language ) {
1042                        if ( ! empty( $args['slug'] ) && $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
1043                                $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
1044                        }
1045                }
1046
1047                // Validate name.
1048                // No need to sanitize it as `wp_insert_term()` will do it for us.
1049                if ( empty( $args['name'] ) ) {
1050                        $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
1051                }
1052
1053                // Validate flag.
1054                if ( ! empty( $args['flag'] ) && ! is_readable( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
1055                        $flag = PLL_Language::get_flag_information( $args['flag'] );
1056
1057                        if ( ! empty( $flag['url'] ) ) {
1058                                $response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( sanitize_url( $flag['url'] ) ) : wp_remote_get( sanitize_url( $flag['url'] ) );
1059                        }
1060
1061                        if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
1062                                $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
1063                        }
1064                }
1065
1066                return $errors;
1067        }
1068
1069        /**
1070         * Updates the translations when a language slug has been modified in settings or deletes them when a language is removed.
1071         *
1072         * @since 0.5
1073         * @since 3.7 Moved from `PLL_Admin_Model::update_translations()` to `WP_Syntex\Polylang\Model\Languages::update_translations()`.
1074         *            Visibility changed from public to protected.
1075         *
1076         * @global $wpdb wpdb global instance.
1077         *
1078         * @param string $old_slug The old language slug.
1079         * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
1080         * @return WP_Error
1081         *
1082         * @phpstan-param non-empty-string $old_slug
1083         */
1084        protected function update_translations( $old_slug, $new_slug = '' ): WP_Error {
1085                global $wpdb;
1086
1087                $term_ids = array();
1088                $dr       = array();
1089                $dt       = array();
1090                $ut       = array();
1091                $errors   = new WP_Error();
1092
1093                $taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'translations' ) );
1094                $terms      = get_terms( array( 'taxonomy' => $taxonomies ) );
1095
1096                if ( is_array( $terms ) ) {
1097                        foreach ( $terms as $term ) {
1098                                $term_ids[ $term->taxonomy ][] = $term->term_id;
1099                                $tr = maybe_unserialize( $term->description );
1100                                $tr = is_array( $tr ) ? $tr : array();
1101
1102                                /**
1103                                 * Filters the unserialized translation group description before it is
1104                                 * updated when a language is deleted or a language slug is changed.
1105                                 *
1106                                 * @since 3.2
1107                                 *
1108                                 * @param (int|string[])[] $tr {
1109                                 *     List of translations with lang codes as array keys and IDs as array values.
1110                                 *     Also in this array:
1111                                 *
1112                                 *     @type string[] $sync List of synchronized translations with lang codes as array keys and array values.
1113                                 * }
1114                                 * @param string           $old_slug The old language slug.
1115                                 * @param string           $new_slug The new language slug.
1116                                 * @param WP_Term          $term     The term containing the post or term translation group.
1117                                 */
1118                                $tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
1119
1120                                if ( ! empty( $tr[ $old_slug ] ) ) {
1121                                        if ( $new_slug ) {
1122                                                $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete.
1123                                        } else {
1124                                                $dr['id'][] = (int) $tr[ $old_slug ];
1125                                                $dr['tt'][] = (int) $term->term_taxonomy_id;
1126                                        }
1127                                        unset( $tr[ $old_slug ] );
1128
1129                                        if ( empty( $tr ) || 1 == count( $tr ) ) {
1130                                                $dt['t'][]  = (int) $term->term_id;
1131                                                $dt['tt'][] = (int) $term->term_taxonomy_id;
1132                                        } else {
1133                                                $ut['case'][] = array( $term->term_id, maybe_serialize( $tr ) );
1134                                                $ut['in'][]   = (int) $term->term_id;
1135                                        }
1136                                }
1137                        }
1138                }
1139
1140                // Delete relationships.
1141                if ( ! empty( $dr ) ) {
1142                        $result = $wpdb->query(
1143                                $wpdb->prepare(
1144                                        sprintf(
1145                                                "DELETE FROM {$wpdb->term_relationships} WHERE object_id IN (%s) AND term_taxonomy_id IN (%s)",
1146                                                implode( ',', array_fill( 0, count( $dr['id'] ), '%d' ) ),
1147                                                implode( ',', array_fill( 0, count( $dr['tt'] ), '%d' ) )
1148                                        ),
1149                                        array_merge( $dr['id'], $dr['tt'] )
1150                                )
1151                        );
1152                        if ( false === $result ) {
1153                                $errors->add( 'pll_delete_relationships', __( 'Could not delete relationships.', 'polylang' ) );
1154                        }
1155                }
1156
1157                // Delete terms.
1158                if ( ! empty( $dt ) ) {
1159                        $result = $wpdb->query(
1160                                $wpdb->prepare(
1161                                        sprintf(
1162                                                "DELETE FROM {$wpdb->terms} WHERE term_id IN (%s)",
1163                                                implode( ',', array_fill( 0, count( $dt['t'] ), '%d' ) )
1164                                        ),
1165                                        $dt['t']
1166                                )
1167                        );
1168                        if ( false === $result ) {
1169                                $errors->add( 'pll_delete_terms', __( 'Could not delete translation groups.', 'polylang' ) );
1170                        }
1171
1172                        $result = $wpdb->query(
1173                                $wpdb->prepare(
1174                                        sprintf(
1175                                                "DELETE FROM {$wpdb->term_taxonomy} WHERE term_taxonomy_id IN (%s)",
1176                                                implode( ',', array_fill( 0, count( $dt['tt'] ), '%d' ) )
1177                                        ),
1178                                        $dt['tt']
1179                                )
1180                        );
1181                        if ( false === $result ) {
1182                                $errors->add( 'pll_delete_term_taxonomy', __( 'Could not delete translation groups.', 'polylang' ) );
1183                        }
1184                }
1185
1186                // Update terms.
1187                if ( ! empty( $ut ) ) {
1188                        $result = $wpdb->query(
1189                                $wpdb->prepare(
1190                                        sprintf(
1191                                                "UPDATE {$wpdb->term_taxonomy} SET description = ( CASE term_id %s END ) WHERE term_id IN (%s)",
1192                                                implode( ' ', array_fill( 0, count( $ut['case'] ), 'WHEN %d THEN %s' ) ),
1193                                                implode( ',', array_fill( 0, count( $ut['in'] ), '%d' ) )
1194                                        ),
1195                                        array_merge( array_merge( ...$ut['case'] ), $ut['in'] )
1196                                )
1197                        );
1198                        if ( false === $result ) {
1199                                $errors->add( 'pll_update_term_taxonomy', __( 'Could not update translation groups.', 'polylang' ) );
1200                        }
1201                }
1202
1203                if ( ! empty( $term_ids ) ) {
1204                        foreach ( $term_ids as $taxonomy => $ids ) {
1205                                clean_term_cache( $ids, $taxonomy );
1206                        }
1207                }
1208
1209                return $errors;
1210        }
1211
1212        /**
1213         * Updates or adds new terms for a secondary language taxonomy (aka not 'language').
1214         *
1215         * @since 3.4
1216         * @since 3.7 Moved from `PLL_Model::update_secondary_language_terms()` to `WP_Syntex\Polylang\Model\Languages::update_secondary_language_terms()`.
1217         *
1218         * @param string            $slug       Language term slug (with or without the `pll_` prefix).
1219         * @param string            $name       Language name (label).
1220         * @param PLL_Language|null $language   Optional. A language object. Required to update the existing terms.
1221         * @param string[]          $taxonomies Optional. List of language taxonomies to deal with. An empty value means
1222         *                                      all of them. Defaults to all taxonomies.
1223         * @return WP_Error
1224         *
1225         * @phpstan-param non-empty-string $slug
1226         * @phpstan-param non-empty-string $name
1227         * @phpstan-param array<non-empty-string> $taxonomies
1228         */
1229        protected function update_secondary_language_terms( $slug, $name, ?PLL_Language $language = null, array $taxonomies = array() ): WP_Error {
1230                $slug = 0 === strpos( $slug, 'pll_' ) ? $slug : "pll_$slug";
1231                $errors = new WP_Error();
1232
1233                foreach ( $this->translatable_objects->get_secondary_translatable_objects() as $object ) {
1234                        if ( ! empty( $taxonomies ) && ! in_array( $object->get_tax_language(), $taxonomies, true ) ) {
1235                                // Not in the list.
1236                                continue;
1237                        }
1238
1239                        if ( ! empty( $language ) ) {
1240                                $term_id = $language->get_tax_prop( $object->get_tax_language(), 'term_id' );
1241                        } else {
1242                                $term_id = 0;
1243                        }
1244
1245                        if ( empty( $term_id ) ) {
1246                                // Attempt to repair the language if a term has been deleted by a database cleaning tool.
1247                                $result = wp_insert_term( $name, $object->get_tax_language(), array( 'slug' => $slug ) );
1248                                if ( is_wp_error( $result ) ) {
1249                                        $errors->add(
1250                                                'pll_add_secondary_language_terms',
1251                                                /* translators: %s is a taxonomy name */
1252                                                sprintf( __( 'Could not add secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1253                                        );
1254                                }
1255                                continue;
1256                        }
1257
1258                        /** @var PLL_Language $language */
1259                        if ( "pll_{$language->slug}" !== $slug || $language->name !== $name ) {
1260                                // Something has changed.
1261                                $result = wp_update_term( $term_id, $object->get_tax_language(), array( 'slug' => $slug, 'name' => $name ) );
1262                                if ( is_wp_error( $result ) ) {
1263                                        $errors->add(
1264                                                'pll_update_secondary_language_terms',
1265                                                /* translators: %s is a taxonomy name */
1266                                                sprintf( __( 'Could not update secondary language term for taxonomy %s.', 'polylang' ), $object->get_tax_language() )
1267                                        );
1268                                }
1269                        }
1270                }
1271
1272                return $errors;
1273        }
1274
1275        /**
1276         * Returns the list of available languages, based on the language taxonomy terms.
1277         * Stores the list in a db transient and in a `PLL_Cache` object.
1278         *
1279         * @since 3.4
1280         * @since 3.7 Moved from `PLL_Model::get_languages_from_taxonomies()` to `WP_Syntex\Polylang\Model\Languages::get_from_taxonomies()`.
1281         *
1282         * @return PLL_Language[] An array of `PLL_Language` objects, array keys are the type.
1283         *
1284         * @phpstan-return list<PLL_Language>
1285         */
1286        protected function get_from_taxonomies(): array {
1287                $terms_by_slug = array();
1288
1289                foreach ( $this->get_terms() as $term ) {
1290                        // Except for language taxonomy term slugs, remove 'pll_' prefix from the other language taxonomy term slugs.
1291                        $key = 'language' === $term->taxonomy ? $term->slug : substr( $term->slug, 4 );
1292                        $terms_by_slug[ $key ][ $term->taxonomy ] = $term;
1293                }
1294
1295                /**
1296                 * @var (
1297                 *     array{
1298                 *         string: array{
1299                 *             language: WP_Term,
1300                 *         }&array<non-empty-string, WP_Term>
1301                 *     }
1302                 * ) $terms_by_slug
1303                 */
1304                $languages = array_filter(
1305                        array_map(
1306                                array( new PLL_Language_Factory( $this->options ), 'get_from_terms' ),
1307                                array_values( $terms_by_slug )
1308                        )
1309                );
1310
1311                /**
1312                 * Filters the list of languages *before* it is stored in the persistent cache.
1313                 * /!\ This filter is fired *before* the $polylang object is available.
1314                 *
1315                 * @since 1.7.5
1316                 * @since 3.4 Deprecated.
1317                 * @deprecated
1318                 *
1319                 * @param PLL_Language[]       $languages The list of language objects.
1320                 * @param Language $model     `Language` object.
1321                 */
1322                $languages = apply_filters_deprecated( 'pll_languages_list', array( $languages, $this ), '3.4', 'pll_additional_language_data' );
1323
1324                if ( ! $this->are_ready() ) {
1325                        // Do not cache an incomplete list.
1326                        /** @var list<PLL_Language> $languages */
1327                        return $languages;
1328                }
1329
1330                /*
1331                 * Don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache.
1332                 * Thanks to captin411 for catching this!
1333                 *
1334                 * @see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
1335                 */
1336                $languages_data = array_map(
1337                        function ( $language ) {
1338                                return $language->to_array( 'db' );
1339                        },
1340                        $languages
1341                );
1342
1343                set_transient( self::TRANSIENT_NAME, $languages_data );
1344
1345                /** @var list<PLL_Language> $languages */
1346                return $languages;
1347        }
1348
1349        /**
1350         * Returns the list of existing language terms.
1351         * - Returns all terms, that are or not assigned to posts.
1352         * - Terms are ordered by `term_group` and `term_id` (see `WP_Syntex\Polylang\Model\Languages::filter_terms_orderby()`).
1353         *
1354         * @since 3.2.3
1355         * @since 3.7 Moved from `PLL_Model::get_language_terms()` to `WP_Syntex\Polylang\Model\Languages::get_terms()`.
1356         *
1357         * @return WP_Term[]
1358         */
1359        protected function get_terms(): array {
1360                $terms = get_terms(
1361                        array(
1362                                'taxonomy'   => $this->translatable_objects->get_taxonomy_names( array( 'language' ) ),
1363                                'hide_empty' => false,
1364                        )
1365                );
1366
1367                if ( empty( $terms ) || is_wp_error( $terms ) ) {
1368                        return array();
1369                }
1370
1371                $callback = static function ( $a, $b ) {
1372                        if ( $a->taxonomy !== $b->taxonomy ) {
1373                                // Sort terms by 'language' taxonomy first.
1374                                return 'language' === $a->taxonomy ? -1 : 1;
1375                        }
1376                        if ( $a->term_group !== $b->term_group ) {
1377                                // Then by term_group.
1378                                return $a->term_group < $b->term_group ? -1 : 1;
1379                        }
1380                        // Then by term_id.
1381                        return $a->term_id < $b->term_id ? -1 : 1;
1382                };
1383
1384                usort( $terms, $callback );
1385                return $terms;
1386        }
1387}
Note: See TracBrowser for help on using the repository browser.