PluginProbe
Polylang / 3.2.4
Polylang v3.2.4
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 / admin / admin-model.php

admin-model.php in Polylang 3.2.4, at admin/admin-model.php

581 lines 20.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 /**
7 * Extends the PLL_Model class with methods needed only in Polylang settings pages.
8 *
9 * @since 1.2
10 */
11 class PLL_Admin_Model extends PLL_Model {
12
13 /**
14 * Adds a new language
15 * and creates a default category for this language.
16 *
17 * @since 1.2
18 *
19 * @param array $args {
20 * @type string $name Language name ( used only for display ).
21 * @type string $slug Language code ( ideally 2-letters ISO 639-1 language code ).
22 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be loaded...
23 * @type int $rtl 1 if rtl language, 0 otherwise.
24 * @type int $term_group Language order when displayed.
25 * @type string $no_default_cat Optional, if set, no default category will be created for this language.
26 * @type string $flag Optional, country code, @see flags.php.
27 * }
28 * @return WP_Error|true true if success / WP_Error if failed.
29 */
30 public function add_language( $args ) {
31 $errors = $this->validate_lang( $args );
32 if ( $errors->get_error_code() ) { // Using has_errors() would be more meaningful but is available only since WP 5.0
33 return $errors;
34 }
35
36 // First the language taxonomy
37 $description = maybe_serialize( array( 'locale' => $args['locale'], 'rtl' => (int) $args['rtl'], 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'] ) );
38 $r = wp_insert_term( $args['name'], 'language', array( 'slug' => $args['slug'], 'description' => $description ) );
39 if ( is_wp_error( $r ) ) {
40 // Avoid an ugly fatal error if something went wrong ( reported once in the forum )
41 return new WP_Error( 'pll_add_language', __( 'Impossible to add the language.', 'polylang' ) );
42 }
43 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
44
45 // The term_language taxonomy
46 // Don't want shared terms so use a different slug
47 wp_insert_term( $args['name'], 'term_language', array( 'slug' => 'pll_' . $args['slug'] ) );
48
49 $this->clean_languages_cache(); // Update the languages list now !
50
51 if ( ! isset( $this->options['default_lang'] ) ) {
52 // If this is the first language created, set it as default language
53 $this->options['default_lang'] = $args['slug'];
54 update_option( 'polylang', $this->options );
55 }
56
57 // Init a mo_id for this language
58 $mo = new PLL_MO();
59 $mo->export_to_db( $this->get_language( $args['slug'] ) );
60
61 /**
62 * Fires when a language is added.
63 *
64 * @since 1.9
65 *
66 * @param array $args Arguments used to create the language. @see PLL_Admin_Model::add_language().
67 */
68 do_action( 'pll_add_language', $args );
69
70 $this->clean_languages_cache(); // Again to set add mo_id in the cached languages list
71 flush_rewrite_rules(); // Refresh rewrite rules
72 return true;
73 }
74
75 /**
76 * Delete a language.
77 *
78 * @since 1.2
79 *
80 * @param int $lang_id Language term_id.
81 * @return bool
82 */
83 public function delete_language( $lang_id ) {
84 $lang = $this->get_language( (int) $lang_id );
85
86 if ( empty( $lang ) ) {
87 return false;
88 }
89
90 // Oops ! we are deleting the default language...
91 // Need to do this before loosing the information for default category translations
92 if ( $this->options['default_lang'] == $lang->slug ) {
93 $slugs = $this->get_languages_list( array( 'fields' => 'slug' ) );
94 $slugs = array_diff( $slugs, array( $lang->slug ) );
95
96 if ( ! empty( $slugs ) ) {
97 $this->update_default_lang( reset( $slugs ) ); // Arbitrary choice...
98 } else {
99 unset( $this->options['default_lang'] );
100 }
101 }
102
103 // Delete the translations
104 $this->update_translations( $lang->slug );
105
106 // Delete language option in widgets
107 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
108 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
109 $obj = $widget['callback'][0];
110 $number = $widget['params'][0]['number'];
111 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
112 $settings = $obj->get_settings();
113 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
114 unset( $settings[ $number ]['pll_lang'] );
115 $obj->save_settings( $settings );
116 }
117 }
118 }
119 }
120
121 // Delete menus locations
122 if ( ! empty( $this->options['nav_menus'] ) ) {
123 foreach ( $this->options['nav_menus'] as $theme => $locations ) {
124 foreach ( array_keys( $locations ) as $location ) {
125 unset( $this->options['nav_menus'][ $theme ][ $location ][ $lang->slug ] );
126 }
127 }
128 }
129
130 // Delete users options
131 foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
132 delete_user_meta( $user_id, 'pll_filter_content', $lang->slug );
133 delete_user_meta( $user_id, 'description_' . $lang->slug );
134 }
135
136 // Delete the string translations
137 $post = wpcom_vip_get_page_by_title( 'polylang_mo_' . $lang->term_id, OBJECT, 'polylang_mo' );
138 if ( $post instanceof WP_Post ) {
139 wp_delete_post( $post->ID );
140 }
141
142 // Delete domain
143 unset( $this->options['domains'][ $lang->slug ] );
144
145 // Delete the language itself
146 wp_delete_term( $lang->term_id, 'language' );
147 wp_delete_term( $lang->tl_term_id, 'term_language' );
148
149 // Update languages list
150 $this->clean_languages_cache();
151
152 update_option( 'polylang', $this->options );
153 flush_rewrite_rules(); // refresh rewrite rules
154 return true;
155 }
156
157 /**
158 * Updates language properties.
159 *
160 * @since 1.2
161 *
162 * @param array $args {
163 * @type int $lang_id Id of the language to modify.
164 * @type string $name Language name ( used only for display ).
165 * @type string $slug Language code ( ideally 2-letters ISO 639-1 language code ).
166 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be loaded...
167 * @type int $rtl 1 if rtl language, 0 otherwise.
168 * @type int $term_group Language order when displayed.
169 * @type string $flag Optional, country code, @see flags.php.
170 * }
171 * @return WP_Error|true true if success / WP_Error if failed.
172 */
173 public function update_language( $args ) {
174 $lang = $this->get_language( (int) $args['lang_id'] );
175
176 $errors = $this->validate_lang( $args, $lang );
177 if ( $errors->get_error_code() ) { // Using has_errors() would be more meaningful but is available only since WP 5.0
178 return $errors;
179 }
180
181 // Update links to this language in posts and terms in case the slug has been modified
182 $slug = $args['slug'];
183 $old_slug = $lang->slug;
184
185 if ( $old_slug != $slug ) {
186 // Update the language slug in translations
187 $this->update_translations( $old_slug, $slug );
188
189 // Update language option in widgets
190 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
191 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
192 $obj = $widget['callback'][0];
193 $number = $widget['params'][0]['number'];
194 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
195 $settings = $obj->get_settings();
196 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
197 $settings[ $number ]['pll_lang'] = $slug;
198 $obj->save_settings( $settings );
199 }
200 }
201 }
202 }
203
204 // Update menus locations
205 if ( ! empty( $this->options['nav_menus'] ) ) {
206 foreach ( $this->options['nav_menus'] as $theme => $locations ) {
207 foreach ( array_keys( $locations ) as $location ) {
208 if ( ! empty( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] ) ) {
209 $this->options['nav_menus'][ $theme ][ $location ][ $slug ] = $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ];
210 unset( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] );
211 }
212 }
213 }
214 }
215
216 // Update domains
217 if ( ! empty( $this->options['domains'][ $old_slug ] ) ) {
218 $this->options['domains'][ $slug ] = $this->options['domains'][ $old_slug ];
219 unset( $this->options['domains'][ $old_slug ] );
220 }
221
222 // Update the default language option if necessary
223 if ( $this->options['default_lang'] == $old_slug ) {
224 $this->options['default_lang'] = $slug;
225 }
226 }
227
228 update_option( 'polylang', $this->options );
229
230 // And finally update the language itself
231 $description = maybe_serialize( array( 'locale' => $args['locale'], 'rtl' => (int) $args['rtl'], 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'] ) );
232 wp_update_term( (int) $lang->term_id, 'language', array( 'slug' => $slug, 'name' => $args['name'], 'description' => $description, 'term_group' => (int) $args['term_group'] ) );
233 if ( empty( $lang->tl_term_id ) ) {
234 // Attempt to repair the term_language if it has been deleted by a database cleaning tool.
235 wp_insert_term( $args['name'], 'term_language', array( 'slug' => 'pll_' . $slug ) );
236 } else {
237 wp_update_term( (int) $lang->tl_term_id, 'term_language', array( 'slug' => 'pll_' . $slug, 'name' => $args['name'] ) );
238 }
239
240 /**
241 * Fires after a language is updated.
242 *
243 * @since 1.9
244 * @since 3.2 Added $lang parameter.
245 *
246 * @param array<mixed> $args {
247 * Arguments used to modify the language. @see PLL_Admin_Model::update_language().
248 *
249 * @type string $name Language name (used only for display).
250 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
251 * @type string $locale WordPress locale.
252 * @type int $rtl 1 if rtl language, 0 otherwise.
253 * @type int $term_group Language order when displayed.
254 * @type string $no_default_cat Optional, if set, no default category has been created for this language.
255 * @type string $flag Optional, country code, @see flags.php.
256 * }
257 * @param PLL_Language $lang Previous value of the language beeing edited.
258 */
259 do_action( 'pll_update_language', $args, $lang );
260
261 $this->clean_languages_cache();
262 flush_rewrite_rules(); // Refresh rewrite rules
263 return true;
264 }
265
266 /**
267 * Validates data entered when creating or updating a language.
268 *
269 * @see PLL_Admin_Model::add_language().
270 *
271 * @since 0.4
272 *
273 * @param array $args Parameters of {@see PLL_Admin_Model::add_language() or @see PLL_Admin_Model::update_language()}.
274 * @param PLL_Language $lang Optional the language currently updated, the language is created if not set.
275 * @return WP_Error
276 */
277 protected function validate_lang( $args, $lang = null ) {
278 $errors = new WP_Error();
279
280 // Validate locale with the same pattern as WP 4.3. See #28303
281 if ( ! preg_match( '#^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?$#', $args['locale'], $matches ) ) {
282 $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
283 }
284
285 // Validate slug characters
286 if ( ! preg_match( '#^[a-z_-]+$#', $args['slug'] ) ) {
287 $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
288 }
289
290 // Validate slug is unique
291 foreach ( $this->get_languages_list() as $language ) {
292 if ( $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
293 $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
294 }
295 }
296
297 // Validate name
298 // No need to sanitize it as wp_insert_term will do it for us
299 if ( empty( $args['name'] ) ) {
300 $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
301 }
302
303 // Validate flag
304 if ( ! empty( $args['flag'] ) && ! file_exists( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
305 $flag = PLL_Language::get_flag_informations( $args['flag'] );
306
307 if ( ! empty( $flag['url'] ) ) {
308 $response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( esc_url_raw( $flag['url'] ) ) : wp_remote_get( esc_url_raw( $flag['url'] ) );
309 }
310
311 if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
312 $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
313 }
314 }
315
316 return $errors;
317 }
318
319 /**
320 * Assigns a language to posts or terms in mass.
321 *
322 * @since 1.2
323 *
324 * @param string $type Either 'post' or 'term'.
325 * @param int[] $ids Array of post ids or term ids.
326 * @param PLL_Language|string $lang Language to assign to the posts or terms.
327 * @return void
328 */
329 public function set_language_in_mass( $type, $ids, $lang ) {
330 global $wpdb;
331
332 $lang = $this->get_language( $lang );
333
334 if ( empty( $lang ) ) {
335 return;
336 }
337
338 $tt_id = 'term' === $type ? $lang->tl_term_taxonomy_id : $lang->term_taxonomy_id;
339 $values = array();
340 $ids = array_map( 'intval', $ids );
341
342 foreach ( $ids as $id ) {
343 $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
344 }
345
346 if ( ! empty( $values ) ) {
347 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
348 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
349 $lang->update_count(); // Updating term count is mandatory ( thanks to AndyDeGroo )
350 }
351
352 if ( 'term' === $type ) {
353 clean_term_cache( $ids, 'term_language' );
354 $translations = array();
355
356 foreach ( $ids as $id ) {
357 $translations[] = array( $lang->slug => $id );
358 }
359
360 if ( ! empty( $translations ) ) {
361 $this->set_translation_in_mass( 'term', $translations );
362 }
363 } else {
364 clean_term_cache( $ids, 'language' );
365 }
366 }
367
368 /**
369 * Creates translations groups in mass.
370 *
371 * @since 1.6.3
372 *
373 * @param string $type Either 'post' or 'term'
374 * @param array $translations Array of translations arrays.
375 * @return void
376 */
377 public function set_translation_in_mass( $type, $translations ) {
378 global $wpdb;
379
380 $taxonomy = $type . '_translations';
381 $terms = array();
382 $slugs = array();
383 $description = array();
384 $count = array();
385
386 foreach ( $translations as $t ) {
387 $term = uniqid( 'pll_' ); // the term name
388 $terms[] = $wpdb->prepare( '( %s, %s )', $term, $term );
389 $slugs[] = $wpdb->prepare( '%s', $term );
390 $description[ $term ] = maybe_serialize( $t );
391 $count[ $term ] = count( $t );
392 }
393
394 // Insert terms
395 if ( ! empty( $terms ) ) {
396 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
397 $wpdb->query( "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES " . implode( ',', array_unique( $terms ) ) );
398 }
399
400 // Get all terms with their term_id
401 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
402 $terms = $wpdb->get_results( "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN ( " . implode( ',', $slugs ) . ' )' );
403 $term_ids = array();
404 $tts = array();
405
406 // Prepare terms taxonomy relationship
407 foreach ( $terms as $term ) {
408 $term_ids[] = $term->term_id;
409 $tts[] = $wpdb->prepare( '( %d, %s, %s, %d )', $term->term_id, $taxonomy, $description[ $term->slug ], $count[ $term->slug ] );
410 }
411
412 // Insert term_taxonomy
413 if ( ! empty( $tts ) ) {
414 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
415 $wpdb->query( "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES " . implode( ',', array_unique( $tts ) ) );
416 }
417
418 // Get all terms with term_taxonomy_id
419 $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
420 $trs = array();
421
422 // Prepare objects relationships.
423 if ( is_array( $terms ) ) {
424 foreach ( $terms as $term ) {
425 $t = maybe_unserialize( $term->description );
426 if ( in_array( $t, $translations ) ) {
427 foreach ( $t as $object_id ) {
428 if ( ! empty( $object_id ) ) {
429 $trs[] = $wpdb->prepare( '( %d, %d )', $object_id, $term->term_taxonomy_id );
430 }
431 }
432 }
433 }
434 }
435
436 // Insert term_relationships
437 if ( ! empty( $trs ) ) {
438 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
439 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', $trs ) );
440 $trs = array_unique( $trs );
441 }
442
443 clean_term_cache( $term_ids, $taxonomy );
444 }
445
446 /**
447 * Updates the translations when a language slug has been modified in settings
448 * or deletes them when a language is removed.
449 *
450 * @since 0.5
451 *
452 * @param string $old_slug The old language slug.
453 * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
454 * @return void
455 */
456 public function update_translations( $old_slug, $new_slug = '' ) {
457 global $wpdb;
458
459 $terms = get_terms( array( 'post_translations', 'term_translations' ) );
460 $term_ids = array();
461 $dr = array();
462 $dt = array();
463 $ut = array();
464
465 if ( is_array( $terms ) ) {
466 foreach ( $terms as $term ) {
467 $term_ids[ $term->taxonomy ][] = $term->term_id;
468 $tr = maybe_unserialize( $term->description );
469
470 /**
471 * Filters the unserialized translation group description before it is
472 * updated when a language is deleted or a language slug is changed.
473 *
474 * @since 3.2
475 *
476 * @param array<int|array<string>> $tr {
477 * List of translations with lang codes as array keys and IDs as array values.
478 * Also in this array:
479 *
480 * @type array<string> $sync List of synchronized translations with lang codes as array keys and array values.
481 * }
482 * @param string $old_slug The old language slug.
483 * @param string $new_slug The new language slug.
484 * @param WP_Term $term The term containing the post or term translation group.
485 */
486 $tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
487
488 if ( ! empty( $tr[ $old_slug ] ) ) {
489 if ( $new_slug ) {
490 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete
491 } else {
492 $dr['id'][] = (int) $tr[ $old_slug ];
493 $dr['tt'][] = (int) $term->term_taxonomy_id;
494 }
495 unset( $tr[ $old_slug ] );
496
497 if ( empty( $tr ) || 1 == count( $tr ) ) {
498 $dt['t'][] = (int) $term->term_id;
499 $dt['tt'][] = (int) $term->term_taxonomy_id;
500 } else {
501 $ut['case'][] = $wpdb->prepare( 'WHEN %d THEN %s', $term->term_id, maybe_serialize( $tr ) );
502 $ut['in'][] = (int) $term->term_id;
503 }
504 }
505 }
506 }
507
508 // Delete relationships
509 if ( ! empty( $dr ) ) {
510 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
511 $wpdb->query(
512 "DELETE FROM $wpdb->term_relationships
513 WHERE object_id IN ( " . implode( ',', $dr['id'] ) . ' )
514 AND term_taxonomy_id IN ( ' . implode( ',', $dr['tt'] ) . ' )'
515 );
516 // PHPCS:enable
517 }
518
519 // Delete terms
520 if ( ! empty( $dt ) ) {
521 $wpdb->query( "DELETE FROM $wpdb->terms WHERE term_id IN ( " . implode( ',', $dt['t'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
522 $wpdb->query( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ( " . implode( ',', $dt['tt'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
523 }
524
525 // Update terms
526 if ( ! empty( $ut ) ) {
527 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
528 $wpdb->query(
529 "UPDATE $wpdb->term_taxonomy
530 SET description = ( CASE term_id " . implode( ' ', $ut['case'] ) . ' END )
531 WHERE term_id IN ( ' . implode( ',', $ut['in'] ) . ' )'
532 );
533 // PHPCS:enable
534 }
535
536 if ( ! empty( $term_ids ) ) {
537 foreach ( $term_ids as $taxonomy => $ids ) {
538 clean_term_cache( $ids, $taxonomy );
539 }
540 }
541 }
542
543 /**
544 * Updates the default language
545 * taking care to update the default category & the nav menu locations.
546 *
547 * @since 1.8
548 *
549 * @param string $slug New language slug.
550 * @return void
551 */
552 public function update_default_lang( $slug ) {
553 // The nav menus stored in theme locations should be in the default language
554 $theme = get_stylesheet();
555 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
556 $menus = array();
557
558 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
559 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
560 }
561 set_theme_mod( 'nav_menu_locations', $menus );
562 }
563
564 /**
565 * Fires when a default language is updated.
566 *
567 * @since 3.1
568 *
569 * @param string $slug Slug.
570 */
571 do_action( 'pll_update_default_lang', $slug );
572
573 // Update options
574 $this->options['default_lang'] = $slug;
575 update_option( 'polylang', $this->options );
576
577 $this->clean_languages_cache();
578 flush_rewrite_rules();
579 }
580 }
581