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

577 lines 19.7 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 if ( ! $this->validate_lang( $args ) ) {
32 return false;
33 }
34
35 // First the language taxonomy
36 $description = serialize( array( 'locale' => $args['locale'], 'rtl' => (int) $args['rtl'], 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'] ) );
37 $r = wp_insert_term( $args['name'], 'language', array( 'slug' => $args['slug'], 'description' => $description ) );
38 if ( is_wp_error( $r ) ) {
39 // Avoid an ugly fatal error if something went wrong ( reported once in the forum )
40 add_settings_error( 'general', 'pll_add_language', __( 'Impossible to add the language.', 'polylang' ) );
41 return false;
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
78 add_settings_error( 'general', 'pll_languages_created', __( 'Language added.', 'polylang' ), 'updated' );
79 return true;
80 }
81
82 /**
83 * Delete a language
84 *
85 * @since 1.2
86 *
87 * @param int $lang_id language term_id
88 */
89 public function delete_language( $lang_id ) {
90 $lang = $this->get_language( (int) $lang_id );
91
92 if ( empty( $lang ) ) {
93 return;
94 }
95
96 // Oops ! we are deleting the default language...
97 // Need to do this before loosing the information for default category translations
98 if ( $this->options['default_lang'] == $lang->slug ) {
99 $slugs = $this->get_languages_list( array( 'fields' => 'slug' ) );
100 $slugs = array_diff( $slugs, array( $lang->slug ) );
101
102 if ( ! empty( $slugs ) ) {
103 $this->update_default_lang( reset( $slugs ) ); // Arbitrary choice...
104 } else {
105 unset( $this->options['default_lang'] );
106 }
107 }
108
109 // Delete the translations
110 $this->update_translations( $lang->slug );
111
112 // Delete language option in widgets
113 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
114 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
115 $obj = $widget['callback'][0];
116 $number = $widget['params'][0]['number'];
117 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
118 $settings = $obj->get_settings();
119 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
120 unset( $settings[ $number ]['pll_lang'] );
121 $obj->save_settings( $settings );
122 }
123 }
124 }
125 }
126
127 // Delete menus locations
128 if ( ! empty( $this->options['nav_menus'] ) ) {
129 foreach ( $this->options['nav_menus'] as $theme => $locations ) {
130 foreach ( $locations as $location => $languages ) {
131 unset( $this->options['nav_menus'][ $theme ][ $location ][ $lang->slug ] );
132 }
133 }
134 }
135
136 // Delete users options
137 foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
138 delete_user_meta( $user_id, 'pll_filter_content', $lang->slug );
139 delete_user_meta( $user_id, 'description_' . $lang->slug );
140 }
141
142 // Delete the string translations
143 $post = wpcom_vip_get_page_by_title( 'polylang_mo_' . $lang->term_id, OBJECT, 'polylang_mo' );
144 if ( ! empty( $post ) ) {
145 wp_delete_post( $post->ID );
146 }
147
148 // Delete domain
149 unset( $this->options['domains'][ $lang->slug ] );
150
151 // Delete the language itself
152 wp_delete_term( $lang->term_id, 'language' );
153 wp_delete_term( $lang->tl_term_id, 'term_language' );
154
155 // Update languages list
156 $this->clean_languages_cache();
157
158 update_option( 'polylang', $this->options );
159 flush_rewrite_rules(); // refresh rewrite rules
160 add_settings_error( 'general', 'pll_languages_deleted', __( 'Language deleted.', 'polylang' ), 'updated' );
161 }
162
163 /**
164 * Update language properties
165 *
166 * List of arguments that $args must contain:
167 * lang_id -> term_id of the language to modify
168 * name -> language name ( used only for display )
169 * slug -> language code ( ideally 2-letters ISO 639-1 language code
170 * locale -> WordPress locale. If something wrong is used for the locale, the .mo files will not be loaded...
171 * rtl -> 1 if rtl language, 0 otherwise
172 * term_group -> language order when displayed
173 *
174 * Optional arguments that $args can contain:
175 * flag -> country code, see flags.php
176 *
177 * @since 1.2
178 *
179 * @param array $args
180 * @return bool true if success / false if failed
181 */
182 public function update_language( $args ) {
183 $lang = $this->get_language( (int) $args['lang_id'] );
184 if ( ! $this->validate_lang( $args, $lang ) ) {
185 return false;
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 ( $locations as $location => $languages ) {
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 = 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 add_settings_error( 'general', 'pll_languages_updated', __( 'Language updated.', 'polylang' ), 'updated' );
254 return true;
255 }
256
257 /**
258 * Validates data entered when creating or updating a language
259 *
260 * @see PLL_Admin_Model::add_language
261 *
262 * @since 0.4
263 *
264 * @param array $args
265 * @param object $lang optional the language currently updated, the language is created if not set
266 * @return bool true if success / false if failed
267 */
268 protected function validate_lang( $args, $lang = null ) {
269 // Validate locale with the same pattern as WP 4.3. See #28303
270 if ( ! preg_match( '#^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?$#', $args['locale'], $matches ) ) {
271 add_settings_error( 'general', 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
272 }
273
274 // Validate slug characters
275 if ( ! preg_match( '#^[a-z_-]+$#', $args['slug'] ) ) {
276 add_settings_error( 'general', 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
277 }
278
279 // Validate slug is unique
280 foreach ( $this->get_languages_list() as $language ) {
281 if ( $language->slug === $args['slug'] && ( null === $lang || ( isset( $lang ) && $lang->term_id != $language->term_id ) ) ) {
282 add_settings_error( 'general', 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
283 }
284 }
285
286 // Validate name
287 // No need to sanitize it as wp_insert_term will do it for us
288 if ( empty( $args['name'] ) ) {
289 add_settings_error( 'general', 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
290 }
291
292 // Validate flag
293 if ( ! empty( $args['flag'] ) && ! file_exists( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
294 add_settings_error( 'general', 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
295 }
296
297 return get_settings_errors() ? false : true;
298 }
299
300 /**
301 * Used to set the language of posts or terms in mass
302 *
303 * @since 1.2
304 *
305 * @param string $type either 'post' or 'term'
306 * @param array $ids array of post ids or term ids
307 * @param object|string $lang object or slug
308 */
309 public function set_language_in_mass( $type, $ids, $lang ) {
310 global $wpdb;
311
312 $ids = array_map( 'intval', $ids );
313 $lang = $this->get_language( $lang );
314 $tt_id = 'term' === $type ? $lang->tl_term_taxonomy_id : $lang->term_taxonomy_id;
315
316 foreach ( $ids as $id ) {
317 $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
318 }
319
320 if ( ! empty( $values ) ) {
321 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
322 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
323 $lang->update_count(); // Updating term count is mandatory ( thanks to AndyDeGroo )
324 }
325
326 if ( 'term' === $type ) {
327 clean_term_cache( $ids, 'term_language' );
328
329 foreach ( $ids as $id ) {
330 $translations[] = array( $lang->slug => $id );
331 }
332
333 if ( ! empty( $translations ) ) {
334 $this->set_translation_in_mass( 'term', $translations );
335 }
336 } else {
337 clean_term_cache( $ids, 'language' );
338 }
339 }
340
341 /**
342 * Used to create a translations groups in mass
343 *
344 * @since 1.6.3
345 *
346 * @param string $type either 'post' or 'term'
347 * @param array $translations array of translations arrays
348 */
349 public function set_translation_in_mass( $type, $translations ) {
350 global $wpdb;
351
352 $taxonomy = $type . '_translations';
353
354 foreach ( $translations as $t ) {
355 $term = uniqid( 'pll_' ); // the term name
356 $terms[] = $wpdb->prepare( '( %s, %s )', $term, $term );
357 $slugs[] = $wpdb->prepare( '%s', $term );
358 $description[ $term ] = serialize( $t );
359 $count[ $term ] = count( $t );
360 }
361
362 // Insert terms
363 if ( ! empty( $terms ) ) {
364 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
365 $wpdb->query( "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES " . implode( ',', array_unique( $terms ) ) );
366 }
367
368 // Get all terms with their term_id
369 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
370 $terms = $wpdb->get_results( "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN ( " . implode( ',', $slugs ) . ' )' );
371
372 // Prepare terms taxonomy relationship
373 foreach ( $terms as $term ) {
374 $term_ids[] = $term->term_id;
375 $tts[] = $wpdb->prepare( '( %d, %s, %s, %d )', $term->term_id, $taxonomy, $description[ $term->slug ], $count[ $term->slug ] );
376 }
377
378 // Insert term_taxonomy
379 if ( ! empty( $tts ) ) {
380 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
381 $wpdb->query( "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES " . implode( ',', array_unique( $tts ) ) );
382 }
383
384 // Get all terms with term_taxonomy_id
385 $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) );
386
387 // Prepare objects relationships
388 foreach ( $terms as $term ) {
389 $t = unserialize( $term->description );
390 if ( in_array( $t, $translations ) ) {
391 foreach ( $t as $object_id ) {
392 if ( ! empty( $object_id ) ) {
393 $trs[] = $wpdb->prepare( '( %d, %d )', $object_id, $term->term_taxonomy_id );
394 }
395 }
396 }
397 }
398
399 // Insert term_relationships
400 if ( ! empty( $trs ) ) {
401 // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
402 $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', $trs ) );
403 $trs = array_unique( $trs );
404 }
405
406 clean_term_cache( $term_ids, $taxonomy );
407 }
408
409 /**
410 * Returns untranslated posts and terms ids ( used in settings )
411 *
412 * @since 0.9
413 * @since 2.2.6 Add the $limit argument
414 *
415 * @param in $limit Max number of posts or terms to return. Defaults to -1 (no limit).
416 * @return array Array made of an array of post ids and an array of term ids
417 */
418 public function get_objects_with_no_lang( $limit = -1 ) {
419 global $wpdb;
420
421 /**
422 * Filters the max number of posts or terms to return when searching objects with no language
423 * This filter can be used to decrease the memory usage in case the number of objects
424 * without language is too big. Using a negative value is equivalent to have no limit.
425 *
426 * @since 2.2.6
427 *
428 * @param int $limit Max number of posts or terms to retrieve from the database
429 */
430 $limit = (int) apply_filters( 'get_objects_with_no_lang_limit', $limit );
431
432 $posts = get_posts(
433 array(
434 'numberposts' => $limit,
435 'nopaging' => $limit <= 0,
436 'post_type' => $this->get_translated_post_types(),
437 'post_status' => 'any',
438 'fields' => 'ids',
439 'tax_query' => array(
440 array(
441 'taxonomy' => 'language',
442 'terms' => $this->get_languages_list( array( 'fields' => 'term_id' ) ),
443 'operator' => 'NOT IN',
444 ),
445 ),
446 )
447 );
448
449 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
450 $terms = $wpdb->get_col(
451 sprintf(
452 "SELECT {$wpdb->term_taxonomy}.term_id FROM {$wpdb->term_taxonomy}
453 WHERE taxonomy IN ('%s')
454 AND {$wpdb->term_taxonomy}.term_id NOT IN (
455 SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN (%s)
456 )
457 %s",
458 implode( "','", array_map( 'esc_sql', $this->get_translated_taxonomies() ) ),
459 implode( ',', array_map( 'intval', $this->get_languages_list( array( 'fields' => 'tl_term_taxonomy_id' ) ) ) ),
460 $limit > 0 ? "LIMIT {$limit}" : ''
461 )
462 );
463 // PHPCS:enable
464
465 /**
466 * Filter the list of untranslated posts ids and terms ids
467 *
468 * @since 0.9
469 *
470 * @param bool|array $objects false if no ids found, list of post and/or term ids otherwise
471 */
472 return apply_filters( 'pll_get_objects_with_no_lang', empty( $posts ) && empty( $terms ) ? false : array( 'posts' => $posts, 'terms' => $terms ) );
473 }
474
475 /**
476 * Used to delete translations or update the translations when a language slug has been modified in settings
477 *
478 * @since 0.5
479 *
480 * @param string $old_slug the old language slug
481 * @param string $new_slug optional, the new language slug, if not set it means the correspondent has been deleted
482 */
483 public function update_translations( $old_slug, $new_slug = '' ) {
484 global $wpdb;
485
486 $terms = get_terms( array( 'post_translations', 'term_translations' ) );
487
488 foreach ( $terms as $term ) {
489 $term_ids[ $term->taxonomy ][] = $term->term_id;
490 $tr = unserialize( $term->description );
491 if ( ! empty( $tr[ $old_slug ] ) ) {
492 if ( $new_slug ) {
493 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete
494 } else {
495 $dr['id'][] = (int) $tr[ $old_slug ];
496 $dr['tt'][] = (int) $term->term_taxonomy_id;
497 }
498 unset( $tr[ $old_slug ] );
499
500 if ( empty( $tr ) || 1 == count( $tr ) ) {
501 $dt['t'][] = (int) $term->term_id;
502 $dt['tt'][] = (int) $term->term_taxonomy_id;
503 } else {
504 $ut['case'][] = $wpdb->prepare( 'WHEN %d THEN %s', $term->term_id, serialize( $tr ) );
505 $ut['in'][] = (int) $term->term_id;
506 }
507 }
508 }
509
510 // Delete relationships
511 if ( ! empty( $dr ) ) {
512 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
513 $wpdb->query(
514 "DELETE FROM $wpdb->term_relationships
515 WHERE object_id IN ( " . implode( ',', $dr['id'] ) . ' )
516 AND term_taxonomy_id IN ( ' . implode( ',', $dr['tt'] ) . ' )'
517 );
518 // PHPCS:enable
519 }
520
521 // Delete terms
522 if ( ! empty( $dt ) ) {
523 $wpdb->query( "DELETE FROM $wpdb->terms WHERE term_id IN ( " . implode( ',', $dt['t'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
524 $wpdb->query( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ( " . implode( ',', $dt['tt'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
525 }
526
527 // Update terms
528 if ( ! empty( $ut ) ) {
529 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
530 $wpdb->query(
531 "UPDATE $wpdb->term_taxonomy
532 SET description = ( CASE term_id " . implode( ' ', $ut['case'] ) . ' END )
533 WHERE term_id IN ( ' . implode( ',', $ut['in'] ) . ' )'
534 );
535 // PHPCS:enable
536 }
537
538 if ( ! empty( $term_ids ) ) {
539 foreach ( $term_ids as $taxonomy => $ids ) {
540 clean_term_cache( $ids, $taxonomy );
541 }
542 }
543 }
544
545 /**
546 * Updates the default language
547 * taking care to update the default category & the nav menu locations
548 *
549 * @since 1.8
550 *
551 * @param string $slug new language slug
552 */
553 public function update_default_lang( $slug ) {
554 // The nav menus stored in theme locations should be in the default language
555 $theme = get_stylesheet();
556 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
557 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
558 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
559 }
560 set_theme_mod( 'nav_menu_locations', $menus );
561 }
562
563 // The default category should be in the default language
564 $default_cats = $this->term->get_translations( get_option( 'default_category' ) );
565 if ( isset( $default_cats[ $slug ] ) ) {
566 update_option( 'default_category', $default_cats[ $slug ] );
567 }
568
569 // Update options
570 $this->options['default_lang'] = $slug;
571 update_option( 'polylang', $this->options );
572
573 $this->clean_languages_cache();
574 flush_rewrite_rules();
575 }
576 }
577