PluginProbe
Polylang / 3.6
Polylang v3.6
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.6, at admin/admin-model.php

541 lines 18.9 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
23 * not be loaded...
24 * @type int $rtl 1 if rtl language, 0 otherwise.
25 * @type int $term_group Language order when displayed.
26 * @type string $no_default_cat Optional, if set, no default category will be created for this language.
27 * @type string $flag Optional, country code, {@see settings/flags.php}.
28 * }
29 * @return WP_Error|true true if success / WP_Error if failed.
30 */
31 public function add_language( $args ) {
32 $errors = $this->validate_lang( $args );
33 if ( $errors->has_errors() ) {
34 return $errors;
35 }
36
37 // First the language taxonomy
38 $r = wp_insert_term(
39 $args['name'],
40 'language',
41 array(
42 'slug' => $args['slug'],
43 'description' => $this->build_language_metas( $args ),
44 )
45 );
46 if ( is_wp_error( $r ) ) {
47 // Avoid an ugly fatal error if something went wrong ( reported once in the forum )
48 return new WP_Error( 'pll_add_language', __( 'Impossible to add the language.', 'polylang' ) );
49 }
50 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
51
52 // The other language taxonomies.
53 $this->update_secondary_language_terms( $args['slug'], $args['name'] );
54
55 if ( ! isset( $this->options['default_lang'] ) ) {
56 // If this is the first language created, set it as default language
57 $this->options['default_lang'] = $args['slug'];
58 update_option( 'polylang', $this->options );
59 }
60
61 // Refresh languages.
62 $this->clean_languages_cache();
63 $this->get_languages_list();
64
65 flush_rewrite_rules(); // Refresh rewrite rules.
66
67 /**
68 * Fires when a language is added.
69 *
70 * @since 1.9
71 *
72 * @param array $args Arguments used to create the language. @see PLL_Admin_Model::add_language().
73 */
74 do_action( 'pll_add_language', $args );
75
76 return true;
77 }
78
79 /**
80 * Delete a language.
81 *
82 * @since 1.2
83 *
84 * @param int $lang_id Language term_id.
85 * @return bool
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 ( $lang->is_default ) {
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 delete_metadata( 'user', 0, 'pll_filter_content', '', true );
136 delete_metadata( 'user', 0, "description_{$lang->slug}", '', true );
137
138 // Delete domain
139 unset( $this->options['domains'][ $lang->slug ] );
140
141 /*
142 * Delete the language itself.
143 *
144 * Reverses the language taxonomies order is required to make sure 'language' is deleted in last.
145 *
146 * The initial order with the 'language' taxonomy at the beginning of 'PLL_Language::term_props' property
147 * is done by {@see PLL_Model::filter_language_terms_orderby()}
148 */
149 foreach ( array_reverse( $lang->get_tax_props( 'term_id' ) ) as $taxonomy_name => $term_id ) {
150 wp_delete_term( $term_id, $taxonomy_name );
151 }
152
153 // Refresh languages.
154 $this->clean_languages_cache();
155 $this->get_languages_list();
156
157 update_option( 'polylang', $this->options );
158 flush_rewrite_rules(); // refresh rewrite rules
159 return true;
160 }
161
162 /**
163 * Updates language properties.
164 *
165 * @since 1.2
166 *
167 * @param array $args {
168 * @type int $lang_id Id of the language to modify.
169 * @type string $name Language name ( used only for display ).
170 * @type string $slug Language code ( ideally 2-letters ISO 639-1 language code ).
171 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be loaded...
172 * @type int $rtl 1 if rtl language, 0 otherwise.
173 * @type int $term_group Language order when displayed.
174 * @type string $flag Optional, country code, @see flags.php.
175 * }
176 * @return WP_Error|true true if success / WP_Error if failed.
177 */
178 public function update_language( $args ) {
179 $lang = $this->get_language( (int) $args['lang_id'] );
180
181 if ( empty( $lang ) ) {
182 return new WP_Error( 'pll_invalid_language_id', __( 'The language does not seem to exist.', 'polylang' ) );
183 }
184
185 $errors = $this->validate_lang( $args, $lang );
186 if ( $errors->get_error_code() ) { // Using has_errors() would be more meaningful but is available only since WP 5.0
187 return $errors;
188 }
189
190 // Update links to this language in posts and terms in case the slug has been modified
191 $slug = $args['slug'];
192 $old_slug = $lang->slug;
193
194 if ( $old_slug != $slug ) {
195 // Update the language slug in translations
196 $this->update_translations( $old_slug, $slug );
197
198 // Update language option in widgets
199 foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
200 if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
201 $obj = $widget['callback'][0];
202 $number = $widget['params'][0]['number'];
203 if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
204 $settings = $obj->get_settings();
205 if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
206 $settings[ $number ]['pll_lang'] = $slug;
207 $obj->save_settings( $settings );
208 }
209 }
210 }
211 }
212
213 // Update menus locations
214 if ( ! empty( $this->options['nav_menus'] ) ) {
215 foreach ( $this->options['nav_menus'] as $theme => $locations ) {
216 foreach ( array_keys( $locations ) as $location ) {
217 if ( ! empty( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] ) ) {
218 $this->options['nav_menus'][ $theme ][ $location ][ $slug ] = $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ];
219 unset( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] );
220 }
221 }
222 }
223 }
224
225 // Update domains
226 if ( ! empty( $this->options['domains'][ $old_slug ] ) ) {
227 $this->options['domains'][ $slug ] = $this->options['domains'][ $old_slug ];
228 unset( $this->options['domains'][ $old_slug ] );
229 }
230
231 // Update the default language option if necessary
232 if ( $lang->is_default ) {
233 $this->options['default_lang'] = $slug;
234 }
235 }
236
237 update_option( 'polylang', $this->options );
238
239 // And finally update the language itself.
240 $this->update_secondary_language_terms( $args['slug'], $args['name'], $lang );
241
242 $description = $this->build_language_metas( $args );
243 wp_update_term( $lang->get_tax_prop( 'language', 'term_id' ), 'language', array( 'slug' => $slug, 'name' => $args['name'], 'description' => $description, 'term_group' => (int) $args['term_group'] ) );
244
245 // Refresh languages.
246 $this->clean_languages_cache();
247 $this->get_languages_list();
248
249 // Refresh rewrite rules.
250 flush_rewrite_rules();
251
252 /**
253 * Fires after a language is updated.
254 *
255 * @since 1.9
256 * @since 3.2 Added $lang parameter.
257 *
258 * @param array $args {
259 * Arguments used to modify the language. @see PLL_Admin_Model::update_language().
260 *
261 * @type string $name Language name (used only for display).
262 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
263 * @type string $locale WordPress locale.
264 * @type int $rtl 1 if rtl language, 0 otherwise.
265 * @type int $term_group Language order when displayed.
266 * @type string $no_default_cat Optional, if set, no default category has been created for this language.
267 * @type string $flag Optional, country code, @see flags.php.
268 * }
269 * @param PLL_Language $lang Previous value of the language being edited.
270 */
271 do_action( 'pll_update_language', $args, $lang );
272
273 return true;
274 }
275
276 /**
277 * Builds the language metas into an array and serializes it, to be stored in the term description.
278 *
279 * @since 3.4
280 *
281 * @param array $args {
282 * @type string $name Language name (used only for display).
283 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
284 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be
285 * loaded...
286 * @type int $rtl 1 if rtl language, 0 otherwise.
287 * @type int $term_group Language order when displayed.
288 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is being
289 * created.
290 * @type string $flag Optional, country code, {@see settings/flags.php}.
291 * }
292 * @return string The serialized description array updated.
293 */
294 protected function build_language_metas( array $args ) {
295 if ( ! empty( $args['lang_id'] ) ) {
296 $language_term = get_term( (int) $args['lang_id'] );
297
298 if ( $language_term instanceof WP_Term ) {
299 $old_data = maybe_unserialize( $language_term->description );
300 }
301 }
302
303 if ( empty( $old_data ) || ! is_array( $old_data ) ) {
304 $old_data = array();
305 }
306
307 $new_data = array(
308 'locale' => $args['locale'],
309 'rtl' => ! empty( $args['rtl'] ) ? 1 : 0,
310 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'],
311 );
312
313 /**
314 * Allow to add data to store for a language.
315 * `$locale`, `$rtl`, and `$flag_code` cannot be overwritten.
316 *
317 * @since 3.4
318 *
319 * @param mixed[] $add_data Data to add.
320 * @param mixed[] $args {
321 * Arguments used to create the language.
322 *
323 * @type string $name Language name (used only for display).
324 * @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
325 * @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
326 * not be loaded...
327 * @type int $rtl 1 if rtl language, 0 otherwise.
328 * @type int $term_group Language order when displayed.
329 * @type int $lang_id Optional, ID of the language to modify. An empty value means the language is
330 * being created.
331 * @type string $flag Optional, country code, {@see settings/flags.php}.
332 * }
333 * @param mixed[] $new_data New data.
334 * @param mixed[] $old_data {
335 * Original data. Contains at least the following:
336 *
337 * @type string $locale WordPress locale.
338 * @type int $rtl 1 if rtl language, 0 otherwise.
339 * @type string $flag_code Country code.
340 * }
341 */
342 $add_data = apply_filters( 'pll_language_metas', array(), $args, $new_data, $old_data );
343 // Don't allow to overwrite `$locale`, `$rtl`, and `$flag_code`.
344 $new_data = array_merge( $old_data, $add_data, $new_data );
345
346 /** @var non-empty-string $serialized maybe_serialize() cannot return anything else than a string when fed by an array. */
347 $serialized = maybe_serialize( $new_data );
348 return $serialized;
349 }
350
351 /**
352 * Validates data entered when creating or updating a language.
353 *
354 * @see PLL_Admin_Model::add_language().
355 *
356 * @since 0.4
357 *
358 * @param array $args Parameters of {@see PLL_Admin_Model::add_language() or @see PLL_Admin_Model::update_language()}.
359 * @param PLL_Language|null $lang Optional the language currently updated, the language is created if not set.
360 * @return WP_Error
361 */
362 protected function validate_lang( $args, $lang = null ) {
363 $errors = new WP_Error();
364
365 // Validate locale with the same pattern as WP 4.3. See #28303
366 if ( ! preg_match( '#^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?$#', $args['locale'], $matches ) ) {
367 $errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
368 }
369
370 // Validate slug characters
371 if ( ! preg_match( '#^[a-z_-]+$#', $args['slug'] ) ) {
372 $errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
373 }
374
375 // Validate slug is unique
376 foreach ( $this->get_languages_list() as $language ) {
377 if ( $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
378 $errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
379 }
380 }
381
382 // Validate name
383 // No need to sanitize it as wp_insert_term will do it for us
384 if ( empty( $args['name'] ) ) {
385 $errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
386 }
387
388 // Validate flag
389 if ( ! empty( $args['flag'] ) && ! is_readable( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
390 $flag = PLL_Language::get_flag_informations( $args['flag'] );
391
392 if ( ! empty( $flag['url'] ) ) {
393 $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'] ) );
394 }
395
396 if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
397 $errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
398 }
399 }
400
401 return $errors;
402 }
403
404 /**
405 * Updates the translations when a language slug has been modified in settings
406 * or deletes them when a language is removed.
407 *
408 * @since 0.5
409 *
410 * @param string $old_slug The old language slug.
411 * @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
412 * @return void
413 */
414 public function update_translations( $old_slug, $new_slug = '' ) {
415 global $wpdb;
416
417 $term_ids = array();
418 $dr = array();
419 $dt = array();
420 $ut = array();
421
422 $taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'translations' ) );
423 $terms = get_terms( array( 'taxonomy' => $taxonomies ) );
424
425 if ( is_array( $terms ) ) {
426 foreach ( $terms as $term ) {
427 $term_ids[ $term->taxonomy ][] = $term->term_id;
428 $tr = maybe_unserialize( $term->description );
429
430 /**
431 * Filters the unserialized translation group description before it is
432 * updated when a language is deleted or a language slug is changed.
433 *
434 * @since 3.2
435 *
436 * @param (int|string[])[] $tr {
437 * List of translations with lang codes as array keys and IDs as array values.
438 * Also in this array:
439 *
440 * @type string[] $sync List of synchronized translations with lang codes as array keys and array values.
441 * }
442 * @param string $old_slug The old language slug.
443 * @param string $new_slug The new language slug.
444 * @param WP_Term $term The term containing the post or term translation group.
445 */
446 $tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
447
448 if ( ! empty( $tr[ $old_slug ] ) ) {
449 if ( $new_slug ) {
450 $tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete
451 } else {
452 $dr['id'][] = (int) $tr[ $old_slug ];
453 $dr['tt'][] = (int) $term->term_taxonomy_id;
454 }
455 unset( $tr[ $old_slug ] );
456
457 if ( empty( $tr ) || 1 == count( $tr ) ) {
458 $dt['t'][] = (int) $term->term_id;
459 $dt['tt'][] = (int) $term->term_taxonomy_id;
460 } else {
461 $ut['case'][] = $wpdb->prepare( 'WHEN %d THEN %s', $term->term_id, maybe_serialize( $tr ) );
462 $ut['in'][] = (int) $term->term_id;
463 }
464 }
465 }
466 }
467
468 // Delete relationships
469 if ( ! empty( $dr ) ) {
470 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
471 $wpdb->query(
472 "DELETE FROM $wpdb->term_relationships
473 WHERE object_id IN ( " . implode( ',', $dr['id'] ) . ' )
474 AND term_taxonomy_id IN ( ' . implode( ',', $dr['tt'] ) . ' )'
475 );
476 // PHPCS:enable
477 }
478
479 // Delete terms
480 if ( ! empty( $dt ) ) {
481 $wpdb->query( "DELETE FROM $wpdb->terms WHERE term_id IN ( " . implode( ',', $dt['t'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
482 $wpdb->query( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ( " . implode( ',', $dt['tt'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
483 }
484
485 // Update terms
486 if ( ! empty( $ut ) ) {
487 // PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
488 $wpdb->query(
489 "UPDATE $wpdb->term_taxonomy
490 SET description = ( CASE term_id " . implode( ' ', $ut['case'] ) . ' END )
491 WHERE term_id IN ( ' . implode( ',', $ut['in'] ) . ' )'
492 );
493 // PHPCS:enable
494 }
495
496 if ( ! empty( $term_ids ) ) {
497 foreach ( $term_ids as $taxonomy => $ids ) {
498 clean_term_cache( $ids, $taxonomy );
499 }
500 }
501 }
502
503 /**
504 * Updates the default language
505 * taking care to update the default category & the nav menu locations.
506 *
507 * @since 1.8
508 *
509 * @param string $slug New language slug.
510 * @return void
511 */
512 public function update_default_lang( $slug ) {
513 // The nav menus stored in theme locations should be in the default language
514 $theme = get_stylesheet();
515 if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
516 $menus = array();
517
518 foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
519 $menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
520 }
521 set_theme_mod( 'nav_menu_locations', $menus );
522 }
523
524 /**
525 * Fires when a default language is updated.
526 *
527 * @since 3.1
528 *
529 * @param string $slug Slug.
530 */
531 do_action( 'pll_update_default_lang', $slug );
532
533 // Update options
534 $this->options['default_lang'] = $slug;
535 update_option( 'polylang', $this->options );
536
537 $this->clean_languages_cache();
538 flush_rewrite_rules();
539 }
540 }
541