PluginProbe
Polylang / 2.7
Polylang v2.7
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 2.7, at admin/admin-model.php

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