PluginProbe
Polylang / 3.1.3
Polylang v3.1.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.1.3, at admin/admin-model.php

550 lines 18.6 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 when a language is updated.
242 *
243 * @since 1.9
244 *
245 * @param array $args Arguments used to modify the language. @see PLL_Admin_Model::update_language().
246 */
247 do_action( 'pll_update_language', $args );
248
249 $this->clean_languages_cache();
250 flush_rewrite_rules(); // Refresh rewrite rules
251 return true;
252 }
253
254 /**
255 * Validates data entered when creating or updating a language.
256 *
257 * @see PLL_Admin_Model::add_language().
258 *
259 * @since 0.4
260 *
261 * @param array $args Parameters of {@see PLL_Admin_Model::add_language() or @see PLL_Admin_Model::update_language()}.
262 * @param PLL_Language $lang Optional the language currently updated, the language is created if not set.
263 * @return WP_Error
264 */
265 protected function validate_lang( $args, $lang = null ) {
266 $errors = new WP_Error();
267
268 // Validate locale with the same pattern as WP 4.3. See #28303
269 if ( ! preg_match( '#^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?$#', $args['locale'], $matches ) ) {
270 $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
271 }
272
273 // Validate slug characters
274 if ( ! preg_match( '#^[a-z_-]+$#', $args['slug'] ) ) {
275 $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
276 }
277
278 // Validate slug is unique
279 foreach ( $this->get_languages_list() as $language ) {
280 if ( $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
281 $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
282 }
283 }
284
285 // Validate name
286 // No need to sanitize it as wp_insert_term will do it for us
287 if ( empty( $args['name'] ) ) {
288 $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
289 }
290
291 // Validate flag
292 if ( ! empty( $args['flag'] ) && ! file_exists( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
293 $flag = PLL_Language::get_flag_informations( $args['flag'] );
294
295 if ( ! empty( $flag['url'] ) ) {
296 $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'] ) );
297 }
298
299 if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
300 $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
301 }
302 }
303
304 return $errors;
305 }
306
307 /**
308 * Assigns a language to posts or terms in mass.
309 *
310 * @since 1.2
311 *
312 * @param string $type Either 'post' or 'term'.
313 * @param int[] $ids Array of post ids or term ids.
314 * @param PLL_Language|string $lang Language to assign to the posts or terms.
315 * @return void
316 */
317 public function set_language_in_mass( $type, $ids, $lang ) {
318 global $wpdb;
319
320 $lang = $this->get_language( $lang );
321
322 if ( empty( $lang ) ) {
323 return;
324 }
325
326 $tt_id = 'term' === $type ? $lang->tl_term_taxonomy_id : $lang->term_taxonomy_id;
327 $values = array();
328 $ids = array_map( 'intval', $ids );
329
330 foreach ( $ids as $id ) {
331 $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
332 }
333
334 if ( ! empty( $values ) ) {
335 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
336 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
337 $lang->update_count(); // Updating term count is mandatory ( thanks to AndyDeGroo )
338 }
339
340 if ( 'term' === $type ) {
341 clean_term_cache( $ids, 'term_language' );
342 $translations = array();
343
344 foreach ( $ids as $id ) {
345 $translations[] = array( $lang->slug => $id );
346 }
347
348 if ( ! empty( $translations ) ) {
349 $this->set_translation_in_mass( 'term', $translations );
350 }
351 } else {
352 clean_term_cache( $ids, 'language' );
353 }
354 }
355
356 /**
357 * Creates translations groups in mass.
358 *
359 * @since 1.6.3
360 *
361 * @param string $type Either 'post' or 'term'
362 * @param array $translations Array of translations arrays.
363 * @return void
364 */
365 public function set_translation_in_mass( $type, $translations ) {
366 global $wpdb;
367
368 $taxonomy = $type . '_translations';
369 $terms = array();
370 $slugs = array();
371 $description = array();
372 $count = array();
373
374 foreach ( $translations as $t ) {
375 $term = uniqid( 'pll_' ); // the term name
376 $terms[] = $wpdb->prepare( '( %s, %s )', $term, $term );
377 $slugs[] = $wpdb->prepare( '%s', $term );
378 $description[ $term ] = maybe_serialize( $t );
379 $count[ $term ] = count( $t );
380 }
381
382 // Insert terms
383 if ( ! empty( $terms ) ) {
384 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
385 $wpdb->query( "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES " . implode( ',', array_unique( $terms ) ) );
386 }
387
388 // Get all terms with their term_id
389 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
390 $terms = $wpdb->get_results( "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN ( " . implode( ',', $slugs ) . ' )' );
391 $term_ids = array();
392 $tts = array();
393
394 // Prepare terms taxonomy relationship
395 foreach ( $terms as $term ) {
396 $term_ids[] = $term->term_id;
397 $tts[] = $wpdb->prepare( '( %d, %s, %s, %d )', $term->term_id, $taxonomy, $description[ $term->slug ], $count[ $term->slug ] );
398 }
399
400 // Insert term_taxonomy
401 if ( ! empty( $tts ) ) {
402 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
403 $wpdb->query( "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES " . implode( ',', array_unique( $tts ) ) );
404 }
405
406 // Get all terms with term_taxonomy_id
407 $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
408 $trs = array();
409
410 // Prepare objects relationships.
411 if ( is_array( $terms ) ) {
412 foreach ( $terms as $term ) {
413 $t = maybe_unserialize( $term->description );
414 if ( in_array( $t, $translations ) ) {
415 foreach ( $t as $object_id ) {
416 if ( ! empty( $object_id ) ) {
417 $trs[] = $wpdb->prepare( '( %d, %d )', $object_id, $term->term_taxonomy_id );
418 }
419 }
420 }
421 }
422 }
423
424 // Insert term_relationships
425 if ( ! empty( $trs ) ) {
426 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
427 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', $trs ) );
428 $trs = array_unique( $trs );
429 }
430
431 clean_term_cache( $term_ids, $taxonomy );
432 }
433
434 /**
435 * Updates the translations when a language slug has been modified in settings
436 * or deletes them when a language is removed.
437 *
438 * @since 0.5
439 *
440 * @param string $old_slug The old language slug.
441 * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
442 * @return void
443 */
444 public function update_translations( $old_slug, $new_slug = '' ) {
445 global $wpdb;
446
447 $terms = get_terms( array( 'post_translations', 'term_translations' ) );
448 $term_ids = array();
449 $dr = array();
450 $dt = array();
451 $ut = array();
452
453 if ( is_array( $terms ) ) {
454 foreach ( $terms as $term ) {
455 $term_ids[ $term->taxonomy ][] = $term->term_id;
456 $tr = maybe_unserialize( $term->description );
457 if ( ! empty( $tr[ $old_slug ] ) ) {
458 if ( $new_slug ) {
459 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete
460 } else {
461 $dr['id'][] = (int) $tr[ $old_slug ];
462 $dr['tt'][] = (int) $term->term_taxonomy_id;
463 }
464 unset( $tr[ $old_slug ] );
465
466 if ( empty( $tr ) || 1 == count( $tr ) ) {
467 $dt['t'][] = (int) $term->term_id;
468 $dt['tt'][] = (int) $term->term_taxonomy_id;
469 } else {
470 $ut['case'][] = $wpdb->prepare( 'WHEN %d THEN %s', $term->term_id, maybe_serialize( $tr ) );
471 $ut['in'][] = (int) $term->term_id;
472 }
473 }
474 }
475 }
476
477 // Delete relationships
478 if ( ! empty( $dr ) ) {
479 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
480 $wpdb->query(
481 "DELETE FROM $wpdb->term_relationships
482 WHERE object_id IN ( " . implode( ',', $dr['id'] ) . ' )
483 AND term_taxonomy_id IN ( ' . implode( ',', $dr['tt'] ) . ' )'
484 );
485 // PHPCS:enable
486 }
487
488 // Delete terms
489 if ( ! empty( $dt ) ) {
490 $wpdb->query( "DELETE FROM $wpdb->terms WHERE term_id IN ( " . implode( ',', $dt['t'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
491 $wpdb->query( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ( " . implode( ',', $dt['tt'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
492 }
493
494 // Update terms
495 if ( ! empty( $ut ) ) {
496 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
497 $wpdb->query(
498 "UPDATE $wpdb->term_taxonomy
499 SET description = ( CASE term_id " . implode( ' ', $ut['case'] ) . ' END )
500 WHERE term_id IN ( ' . implode( ',', $ut['in'] ) . ' )'
501 );
502 // PHPCS:enable
503 }
504
505 if ( ! empty( $term_ids ) ) {
506 foreach ( $term_ids as $taxonomy => $ids ) {
507 clean_term_cache( $ids, $taxonomy );
508 }
509 }
510 }
511
512 /**
513 * Updates the default language
514 * taking care to update the default category & the nav menu locations.
515 *
516 * @since 1.8
517 *
518 * @param string $slug New language slug.
519 * @return void
520 */
521 public function update_default_lang( $slug ) {
522 // The nav menus stored in theme locations should be in the default language
523 $theme = get_stylesheet();
524 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
525 $menus = array();
526
527 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
528 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
529 }
530 set_theme_mod( 'nav_menu_locations', $menus );
531 }
532
533 /**
534 * Fires when a default language is updated.
535 *
536 * @since 3.1
537 *
538 * @param string $slug Slug.
539 */
540 do_action( 'pll_update_default_lang', $slug );
541
542 // Update options
543 $this->options['default_lang'] = $slug;
544 update_option( 'polylang', $this->options );
545
546 $this->clean_languages_cache();
547 flush_rewrite_rules();
548 }
549 }
550