PluginProbe
Polylang / 1.8.2
Polylang v1.8.2
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 / include / model.php

model.php in Polylang 1.8.2, at include/model.php

555 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * setups the language and translations model based on WordPress taxonomies
5 *
6 * @since 1.2
7 */
8 class PLL_Model {
9 public $cache; // our internal non persistent cache object
10 public $options;
11 public $post, $term; // translated objects models
12
13 /*
14 * constructor: registers custom taxonomies and setups filters and actions
15 *
16 * @since 1.2
17 *
18 * @param array $options Polylang options
19 */
20 public function __construct( &$options ) {
21 $this->options = &$options;
22
23 $this->cache = new PLL_Cache();
24 $this->post = new PLL_Translated_Post( $this ); // translated post sub model
25 $this->term = new PLL_Translated_Term( $this ); // translated term sub model
26
27 // we need to clean languages cache when editing a language and when modifying the permalink structure
28 add_action( 'edited_term_taxonomy', array( &$this, 'clean_languages_cache' ), 10, 2 );
29 add_action( 'update_option_permalink_structure', array( &$this, 'clean_languages_cache' ) );
30 add_action( 'update_option_siteurl', array( &$this, 'clean_languages_cache' ) );
31 add_action( 'update_option_home', array( &$this, 'clean_languages_cache' ) );
32
33 // just in case someone would like to display the language description ;- )
34 add_filter( 'language_description', '__return_empty_string' );
35 }
36
37 /*
38 * returns the list of available languages
39 * caches the list in a db transient ( except flags ), unless PLL_CACHE_LANGUAGES is set to false
40 * caches the list ( with flags ) in the private property $languages
41 *
42 * list of parameters accepted in $args:
43 *
44 * hide_empty => hides languages with no posts if set to true ( defaults to false )
45 * fields => return only that field if set ( see PLL_Language for a list of fields )
46 *
47 * @since 0.1
48 *
49 * @param array $args
50 * @return array|string|int list of PLL_Language objects or PLL_Language object properties
51 */
52 public function get_languages_list( $args = array() ) {
53 if ( false === $languages = $this->cache->get( 'languages' ) ) {
54
55 // create the languages from taxonomies
56 if ( ( defined( 'PLL_CACHE_LANGUAGES' ) && ! PLL_CACHE_LANGUAGES ) || false === ( $languages = get_transient( 'pll_languages_list' ) ) ) {
57 $languages = get_terms( 'language', array( 'hide_empty' => false, 'orderby' => 'term_group' ) );
58 $languages = empty( $languages ) || is_wp_error( $languages ) ? array() : $languages;
59
60 $term_languages = get_terms( 'term_language', array( 'hide_empty' => false ) );
61 $term_languages = empty( $term_languages ) || is_wp_error( $term_languages ) ?
62 array() : array_combine( wp_list_pluck( $term_languages, 'slug' ), $term_languages );
63
64 if ( ! empty( $languages ) && ! empty( $term_languages ) ) {
65 // don't use array_map + create_function to instantiate an autoloaded class as it breaks badly in old versions of PHP
66 foreach ( $languages as $k => $v ) {
67 $languages[ $k ] = new PLL_Language( $v, $term_languages[ 'pll_' . $v->slug ] );
68 }
69
70 // we will need the languages list to allow its access in the filter below
71 $this->cache->set( 'languages', $languages );
72
73 // filters the list of languages *before* it is stored in the persistent cache
74 // /!\ this filter is fired *before* the $polylang object is available
75 $languages = apply_filters( 'pll_languages_list', $languages, $this );
76
77 // don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache
78 // thanks to captin411 for catching this!
79 // see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255;
80 set_transient( 'pll_languages_list', array_map( 'get_object_vars', $languages ) );
81 }
82 else {
83 $languages = array(); // in case something went wrong
84 }
85 }
86
87 // create the languages directly from arrays stored in transients
88 else {
89 foreach ( $languages as $k => $v ) {
90 $languages[ $k ] = new PLL_Language( $v );
91 }
92 }
93
94 // custom flags
95 if ( ! PLL_ADMIN ) {
96 foreach ( $languages as $language ) {
97 $language->set_custom_flag();
98 }
99 }
100
101 // filters the list of languages *after* it is stored in the persistent cache
102 // /!\ this filter is fired *before* the $polylang object is available
103 $languages = apply_filters( 'pll_after_languages_cache', $languages );
104 $this->cache->set( 'languages', $languages );
105 }
106
107 $args = wp_parse_args( $args, array( 'hide_empty' => false ) );
108
109 // remove empty languages if requested
110 if ( $args['hide_empty'] ) {
111 $languages = wp_list_filter( $languages, array( 'count' => 0 ), 'NOT' );
112 }
113
114 return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] );
115 }
116
117 /*
118 * cleans language cache
119 * can be called directly with no parameter
120 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
121 *
122 * @since 1.2
123 *
124 * @param int $term not used
125 * @param string $taxonomy taxonomy name
126 */
127 public function clean_languages_cache( $term = 0, $taxonomy = null ) {
128 // depending on WP version, the action is passed an object or a string
129 // backward compatibility with WP < 4.2
130 if ( ! empty( $taxonomy ) && is_object( $taxonomy ) ) {
131 $taxonomy = $taxonomy->name;
132 }
133
134 if ( empty( $taxonomy ) || 'language' == $taxonomy ) {
135 delete_transient( 'pll_languages_list' );
136 $this->cache->clean();
137 }
138 }
139
140 /*
141 * returns the language by its term_id, tl_term_id, slug or locale
142 *
143 * @since 0.1
144 *
145 * @param int|string term_id, tl_term_id, slug or locale of the queried language
146 * @return object|bool PLL_Language object, false if no language found
147 */
148 public function get_language( $value ) {
149 if ( is_object( $value ) ) {
150 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // will force cast to PLL_Language
151 }
152
153 if ( false === $return = $this->cache->get( 'language:' . $value ) ) {
154 foreach ( $this->get_languages_list() as $lang ) {
155 $this->cache->set( 'language:' . $lang->term_id, $lang );
156 $this->cache->set( 'language:' . $lang->tl_term_id, $lang );
157 $this->cache->set( 'language:' . $lang->slug, $lang );
158 $this->cache->set( 'language:' . $lang->locale, $lang );
159 }
160 $return = $this->cache->get( 'language:' . $value );
161 }
162
163 return $return;
164 }
165
166 /*
167 * adds terms clauses to get_terms to filter them by languages - used in both frontend and admin
168 *
169 * @since 1.2
170 *
171 * @param array $clauses the list of sql clauses in terms query
172 * @param object $lang PLL_Language object
173 * @return array modifed list of clauses
174 */
175 public function terms_clauses( $clauses, $lang ) {
176 if ( ! empty( $lang ) ) {
177 $clauses['join'] .= $this->term->join_clause();
178 $clauses['where'] .= $this->term->where_clause( $lang );
179 }
180 return $clauses;
181 }
182
183 /*
184 * returns post types that need to be translated
185 * the post types list is cached for better better performance
186 * wait for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php
187 *
188 * @since 1.2
189 *
190 * @param bool $filter true if we should return only valid registered post types
191 * @return array post type names for which Polylang manages languages and translations
192 */
193 public function get_translated_post_types( $filter = true ) {
194 if ( false === $post_types = $this->cache->get( 'post_types' ) ) {
195 $post_types = array( 'post' => 'post', 'page' => 'page' );
196
197 if ( ! empty( $this->options['media_support'] ) ) {
198 $post_types['attachement'] = 'attachment';
199 }
200
201 if ( ! empty( $this->options['post_types'] ) && is_array( $this->options['post_types'] ) ) {
202 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
203 }
204
205 $post_types = apply_filters( 'pll_get_post_types', $post_types , false );
206
207 if ( did_action( 'after_setup_theme' ) ) {
208 $this->cache->set( 'post_types', $post_types );
209 }
210 }
211
212 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
213 }
214
215 /*
216 * returns true if Polylang manages languages and translations for this post type
217 *
218 * @since 1.2
219 *
220 * @param string|array $post_type post type name or array of post type names
221 * @return bool
222 */
223 public function is_translated_post_type( $post_type ) {
224 $post_types = $this->get_translated_post_types( false );
225 return ( is_array( $post_type ) && array_intersect( $post_type, $post_types ) || in_array( $post_type, $post_types ) );
226 }
227
228 /*
229 * return taxonomies that need to be translated
230 *
231 * @since 1.2
232 *
233 * @param bool $filter true if we should return only valid registered taxonmies
234 * @return array array of registered taxonomy names for which Polylang manages languages and translations
235 */
236 public function get_translated_taxonomies( $filter = true ) {
237 if ( false === $taxonomies = $this->cache->get( 'taxonomies' ) ) {
238 $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
239
240 if ( ! empty( $this->options['taxonomies'] ) && is_array( $this->options['taxonomies'] ) ) {
241 $taxonomies = array_merge( $taxonomies, array_combine( $this->options['taxonomies'], $this->options['taxonomies'] ) );
242 }
243
244 $taxonomies = apply_filters( 'pll_get_taxonomies', $taxonomies, false );
245 if ( did_action( 'after_setup_theme' ) ) {
246 $this->cache->set( 'taxonomies', $taxonomies );
247 }
248 }
249
250 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
251 }
252
253 /*
254 * returns true if Polylang manages languages and translations for this taxonomy
255 *
256 * @since 1.2
257 *
258 * @param string|array $tax taxonomy name or array of taxonomy names
259 * @return bool
260 */
261 public function is_translated_taxonomy( $tax ) {
262 $taxonomies = $this->get_translated_taxonomies( false );
263 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
264 }
265
266 /*
267 * return taxonomies that need to be filtered ( post_format like )
268 *
269 * @since 1.7
270 *
271 * @param bool $filter true if we should return only valid registered taxonomies
272 * @return array array of registered taxonomy names
273 */
274 public function get_filtered_taxonomies( $filter = true ) {
275 if ( did_action( 'after_setup_theme' ) ) {
276 static $taxonomies = null;
277 }
278
279 if ( empty( $taxonomies ) ) {
280 $taxonomies = array( 'post_format' => 'post_format' );
281 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
282 }
283
284 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
285 }
286
287 /*
288 * returns true if Polylang filters this taxonomy per language
289 *
290 * @since 1.7
291 *
292 * @param string|array $tax taxonomy name or array of taxonomy names
293 * @return bool
294 */
295 public function is_filtered_taxonomy( $tax ) {
296 $taxonomies = $this->get_filtered_taxonomies( false );
297 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
298 }
299
300 /*
301 * returns the query vars of all filtered taxonomies
302 *
303 * @since 1.7
304 *
305 * @return array
306 */
307 public function get_filtered_taxonomies_query_vars() {
308 $query_vars = array();
309 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
310 $tax = get_taxonomy( $filtered_tax );
311 $query_vars[] = $tax->query_var;
312 }
313 return $query_vars;
314 }
315
316 /*
317 * create a default category for a language
318 *
319 * @since 1.2
320 *
321 * @param object|string|int $lang language
322 */
323 public function create_default_category( $lang ) {
324 $lang = $this->get_language( $lang );
325
326 // create a new category
327 // FIXME this is translated in admin language when we would like it in $lang
328 $cat_name = __( 'Uncategorized' );
329 $cat_slug = sanitize_title( $cat_name . '-' . $lang->slug );
330 $cat = wp_insert_term( $cat_name, 'category', array( 'slug' => $cat_slug ) );
331
332 // check that the category was not previously created ( in case the language was deleted and recreated )
333 $cat = isset( $cat->error_data['term_exists'] ) ? $cat->error_data['term_exists'] : $cat['term_id'];
334
335 // set language
336 $this->term->set_language( (int) $cat, $lang );
337
338 // this is a translation of the default category
339 $default = (int) get_option( 'default_category' );
340 $translations = $this->term->get_translations( $default );
341 if ( empty( $translations ) ) {
342 if ( $lg = $this->term->get_language( $default ) ) {
343 $translations[ $lg->slug ] = $default;
344 }
345 else {
346 $translations = array();
347 }
348 }
349
350 $this->term->save_translations( (int) $cat, $translations );
351 }
352
353 /*
354 * it is possible to have several terms with the same name in the same taxonomy ( one per language )
355 * but the native term_exists will return true even if only one exists
356 * so here the function adds the language parameter
357 *
358 * @since 1.4
359 *
360 * @param string $term_name the term name
361 * @param string $taxonomy taxonomy name
362 * @param int $parent parent term id
363 * @param string|object $language the language slug or object
364 * @return null|int the term_id of the found term
365 */
366 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
367 global $wpdb;
368
369 $term_name = trim( wp_unslash( $term_name ) );
370
371 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
372 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
373 $join .= $this->term->join_clause();
374 $where = $wpdb->prepare( " WHERE tt.taxonomy = %s AND t.name = %s", $taxonomy, $term_name );
375 $where .= $this->term->where_clause( $this->get_language( $language ) );
376
377 if ( $parent > 0 ) {
378 $where .= $wpdb->prepare( " AND tt.parent = %d", $parent );
379 }
380
381 return $wpdb->get_var( $select . $join . $where );
382 }
383
384 /*
385 * gets the number of posts per language in a date, author or post type archive
386 *
387 * @since 1.2
388 *
389 * @param object lang
390 * @param array $q WP_Query arguments ( accepted: post_type, m, year, monthnum, day, author, author_name, post_format )
391 * @return int
392 */
393 public function count_posts( $lang, $q = array() ) {
394 global $wpdb;
395
396 $q = wp_parse_args( $q, array( 'post_type' => 'post' ) );
397
398 if ( ! is_array( $q['post_type'] ) ) {
399 $q['post_type'] = array( $q['post_type'] );
400 }
401
402 foreach ( $q['post_type'] as $key => $type ) {
403 if ( ! post_type_exists( $type ) ) {
404 unset( $q['post_type'][ $key ] );
405 }
406 }
407
408 if ( empty( $q['post_type'] ) ) {
409 $q['post_type'] = array( 'post' ); // we *need* a post type
410 }
411
412 $cache_key = md5( serialize( $q ) );
413 $counts = wp_cache_get( $cache_key, 'pll_count_posts' );
414
415 if ( false === $counts ) {
416 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts} AS p";
417 $join = $this->post->join_clause();
418 $where = " WHERE post_status = 'publish'";
419 $where .= $wpdb->prepare( " AND p.post_type IN ( '%s' )", join( "', '", $q['post_type'] ) );
420 $where .= $this->post->where_clause( $this->get_languages_list() );
421 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
422
423 if ( ! empty( $q['m'] ) ) {
424 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
425 $where .= $wpdb->prepare( ' AND YEAR( p.post_date ) = %d', substr( $q['m'], 0, 4 ) );
426 if ( strlen( $q['m'] ) > 5 ) {
427 $where .= $wpdb->prepare( ' AND MONTH( p.post_date ) = %d', substr( $q['m'], 4, 2 ) );
428 }
429 if ( strlen( $q['m'] ) > 7 ) {
430 $where .= $wpdb->prepare( ' AND DAYOFMONTH( p.post_date ) = %d', substr( $q['m'], 6, 2 ) );
431 }
432 }
433
434 if ( ! empty( $q['year'] ) ) {
435 $where .= $wpdb->prepare( ' AND YEAR( p.post_date ) = %d', $q['year'] );
436 }
437
438 if ( ! empty( $q['monthnum'] ) ) {
439 $where .= $wpdb->prepare( ' AND MONTH( p.post_date ) = %d', $q['monthnum'] );
440 }
441
442 if ( ! empty( $q['day'] ) ) {
443 $where .= $wpdb->prepare( ' AND DAYOFMONTH( p.post_date ) = %d', $q['day'] );
444 }
445
446 if ( ! empty( $q['author_name'] ) ) {
447 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
448 if ( $author ) {
449 $q['author'] = $author->ID;
450 }
451 }
452
453 if ( ! empty( $q['author'] ) ) {
454 $where .= $wpdb->prepare( ' AND p.post_author = %d', $q['author'] );
455 }
456
457 // filtered taxonomies ( post_format )
458 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
459
460 if ( ! empty( $q[ $tax_qv ] ) ) {
461 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID";
462 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
463 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
464 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
465 }
466 }
467
468 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
469 foreach ( (array) $res as $row ) {
470 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
471 }
472
473 wp_cache_set( $cache_key, $counts, 'pll_count_posts' );
474 }
475
476 return empty( $counts[ $lang->term_taxonomy_id ] ) ? 0 : $counts[ $lang->term_taxonomy_id ];
477 }
478
479 /*
480 * setup the links model based on options
481 *
482 * @since 1.2
483 *
484 * @return object implementing "links_model interface"
485 */
486 public function get_links_model() {
487 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
488 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
489 return new $class( $this );
490 }
491
492 /*
493 * some backward compatibility with Polylang < 1.8
494 * allows for example to call $polylang->model->get_post_languages( $post_id ) instead of $polylang->model->post->get_language( $post_id )
495 * this works but should be slower than the direct call, thus an error is triggered in debug mode
496 *
497 * @since 1.8
498 *
499 * @param string $func function name
500 * @param array $args function arguments
501 */
502 public function __call( $func, $args ) {
503 $f = $func;
504
505 switch ( $func ) {
506 case 'get_object_term':
507 $o = false === strpos( $args[1], 'term' ) ? 'post' : 'term';
508 break;
509
510 case 'save_translations':
511 case 'delete_translation':
512 case 'get_translations':
513 case 'get_translation':
514 case 'join_clause':
515 $o = ( 'post' == $args[0] || $this->is_translated_post_type( $args[0] ) ) ? 'post' : ( 'term' == $args[0] || $this->is_translated_taxonomy( $args[0] ) ? 'term' : false );
516 unset( $args[0] );
517 break;
518
519 case 'set_post_language':
520 case 'get_post_language':
521 case 'set_term_language':
522 case 'get_term_language':
523 case 'delete_term_language':
524 case 'get_post':
525 case 'get_term':
526 $str = explode( '_', $func );
527 $f = empty( $str[2] ) ? $str[0] : $str[0] . '_' . $str[2];
528 $o = $str[1];
529 break;
530
531 case 'where_clause':
532 case 'get_objects_in_language':
533 $o = $args[1];
534 unset( $args[1] );
535 break;
536 }
537
538 if ( ! empty( $o ) && is_object( $this->$o ) && method_exists( $this->$o, $f ) ) {
539 if ( WP_DEBUG ) {
540 $debug = debug_backtrace();
541 $i = 1 + empty( $debug[1]['line'] ); // the file and line are in $debug[2] if the function was called using call_user_func
542
543 trigger_error( sprintf(
544 '%1$s was called incorrectly in %4$s on line %5$s: the call to $polylang->model->%1$s() has been deprecated in Polylang 1.8, use PLL()->model->%2$s->%3$s() instead.' . "\nError handler",
545 $func, $o, $f, $debug[ $i ]['file'], $debug[ $i ]['line']
546 ) );
547 }
548 return call_user_func_array( array( $this->$o, $f ), $args );
549 }
550
551 $debug = debug_backtrace();
552 trigger_error( sprintf( 'Call to undefined function PLL()->model->%1$s() in %2$s on line %3$s' . "\nError handler", $func, $debug[0]['file'], $debug[0]['line'] ), E_USER_ERROR );
553 }
554 }
555