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

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

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