PluginProbe
Polylang / 3.7.3
Polylang v3.7.3
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / include / Model / Languages.php

Languages.php in Polylang 3.7.3, at include/Model/Languages.php

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