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

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

922 lines 31.8 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 $options;
10 protected $cache; // our internal non persistent cache object
11
12 /*
13 * constructor: registers custom taxonomies and setups filters and actions
14 *
15 * @since 1.2
16 *
17 * @param array $options Polylang options
18 */
19 public function __construct(&$options) {
20 $this->options = &$options;
21 $this->cache = new PLL_Cache();
22
23 // register our taxonomies as soon as possible
24 // this is early registration, not ready for rewrite rules as wp_rewrite will be setup later
25 $args = array('label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false, '_pll' => true);
26 register_taxonomy('language', null, $args);
27 register_taxonomy('term_language', 'term', $args);
28 register_taxonomy('term_translations', 'term', $args);
29 $args['update_count_callback'] = '_update_generic_term_count'; // count *all* posts to avoid deleting in clean_translations_terms
30 register_taxonomy('post_translations', null, $args);
31
32 add_filter('get_terms', array(&$this, '_prime_terms_cache'), 10, 2);
33 add_filter('wp_get_object_terms', array(&$this, 'wp_get_object_terms'), 10, 3);
34
35 // we need to clean languages cache when editing a language,
36 // when editing page of front, page for posts or when modifying the permalink structure
37 add_action('edited_term_taxonomy', array(&$this, 'clean_languages_cache'), 10, 2);
38 add_action('update_option_page_on_front', array(&$this, 'clean_languages_cache'));
39 add_action('update_option_page_for_posts', array(&$this, 'clean_languages_cache'));
40 add_action('update_option_permalink_structure', array(&$this, 'clean_languages_cache'));
41
42 // registers completely the language taxonomy
43 add_action('setup_theme', array(&$this, 'register_taxonomy'), 1);
44
45 // setups post types to translate
46 add_action('registered_post_type', array(&$this, 'registered_post_type'));
47
48 // just in case someone would like to display the language description ;-)
49 add_filter('language_description', create_function('$v', "return '';"));
50 }
51
52 /*
53 * cache language and translations when terms are queried by get_terms
54 *
55 * @since 1.2
56 *
57 * @param array $terms queried terms
58 * @param array $taxonomies queried taxonomies
59 * @return array unmodified $terms
60 */
61 public function _prime_terms_cache($terms, $taxonomies) {
62 if ($this->is_translated_taxonomy($taxonomies)) {
63 foreach ($terms as $term) {
64 $term_ids[] = is_object($term) ? $term->term_id : (int) $term;
65 }
66 }
67
68 if (!empty($term_ids))
69 update_object_term_cache(array_unique($term_ids), 'term'); // adds language and translation of terms to cache
70 return $terms;
71 }
72
73 /*
74 * when terms are found for posts, add their language and translations to cache
75 *
76 * @since 1.2
77 *
78 * @param array $terms terms found
79 * @param array $object_ids not used
80 * @param array $taxonomies terms taxonomies
81 * @return array unmodified $terms
82 */
83 public function wp_get_object_terms($terms, $object_ids, $taxonomies) {
84 $taxonomies = explode("', '", trim($taxonomies, "'"));
85 if (!in_array('term_translations', $taxonomies))
86 $this->_prime_terms_cache($terms, $taxonomies);
87 return $terms;
88 }
89
90 /*
91 * wrap wp_get_object_terms to cache it and return only one object
92 * inspired by the function get_the_terms
93 *
94 * @since 1.2
95 *
96 * @param int $object_id post_id or term_id
97 * @param string $taxonomy Polylang taxonomy depending if we are looking for a post (or term) language (or translation)
98 * @return bool|object the term associated to the object in the requested taxonomy if exists, false otherwise
99 */
100 public function get_object_term($object_id, $taxonomy) {
101 if (empty($object_id))
102 return false;
103
104 $object_id = (int) $object_id;
105 $term = get_object_term_cache($object_id, $taxonomy);
106
107 if (false === $term) {
108 // query language and translations at the same time
109 $taxonomies = (false !== strpos($taxonomy, 'term_')) ?
110 array('term_language', 'term_translations') :
111 array('language', 'post_translations');
112
113 // query terms
114 foreach (wp_get_object_terms($object_id, $taxonomies) as $t) {
115 $terms[$t->taxonomy] = $t;
116 if ($t->taxonomy == $taxonomy)
117 $term = $t;
118 }
119
120 // store it the way WP wants it
121 // set an empty cache if no term found in the taxonomy
122 foreach ($taxonomies as $tax) {
123 wp_cache_add($object_id, empty($terms[$tax]) ? array() : array($terms[$tax]), $tax . '_relationships');
124 }
125 }
126 else {
127 $term = reset($term);
128 }
129
130 return empty($term) ? false : $term;
131 }
132
133 /*
134 * returns the list of available languages
135 * caches the list in a db transient (except flags), unless PLL_CACHE_LANGUAGES is set to false
136 * caches the list (with flags) in the private property $languages
137 *
138 * list of parameters accepted in $args:
139 *
140 * hide_empty => hides languages with no posts if set to true (defaults to false)
141 * fields => return only that field if set (see PLL_Language for a list of fields)
142 *
143 * @since 0.1
144 *
145 * @param array $args
146 * @return array|string|int list of PLL_Language objects or PLL_Language object properties
147 */
148 public function get_languages_list($args = array()) {
149 if (false === $languages = $this->cache->get('languages')) {
150
151 // create the languages from taxonomies
152 if ((defined('PLL_CACHE_LANGUAGES') && !PLL_CACHE_LANGUAGES) || false === ($languages = get_transient('pll_languages_list'))) {
153 $languages = get_terms('language', array('hide_empty' => false, 'orderby'=> 'term_group'));
154 $languages = empty($languages) || is_wp_error($languages) ? array() : $languages;
155
156 $term_languages = get_terms('term_language', array('hide_empty' => false));
157 $term_languages = empty($term_languages) || is_wp_error($term_languages) ?
158 array() : array_combine(wp_list_pluck($term_languages, 'name'), $term_languages);
159
160 if (!empty($languages) && !empty($term_languages)) {
161 // don't use array_map + create_function to instantiate an autoloaded class as it breaks badly in old versions of PHP
162 foreach ($languages as $k => $v) {
163 $languages[$k] = new PLL_Language($v, $term_languages[$v->name]);
164 }
165
166 $languages = apply_filters('pll_languages_list', $languages);
167 }
168 else {
169 $languages = array(); // in case something went wrong
170 }
171 }
172
173 // create the languages directly from arrays stored in transients
174 else {
175 foreach ($languages as $k => $v) {
176 $languages[$k] = new PLL_Language($v);
177 }
178 }
179
180 $this->cache->set('languages', $languages);
181
182 // need to wait for $wp_rewrite availibility to set homepage urls
183 did_action('setup_theme') ? $this->_languages_list() : add_action('setup_theme', array(&$this, '_languages_list'));
184 }
185
186 $args = wp_parse_args($args, array('hide_empty' => false));
187
188 // remove empty languages if requested
189 $languages = array_filter($languages, create_function('$v', sprintf('return $v->count || !%d;', $args['hide_empty'])));
190
191 return empty($args['fields']) ? $languages : wp_list_pluck($languages, $args['fields']);
192 }
193
194 /*
195 * fills home urls and flags in language list and set transient in db
196 * delayed to be sure we have access to $wp_rewrite for home urls
197 * languages objects are not cached in db if PLL_CACHE_LANGUAGES is set to false
198 * home urls are not cached in db if PLL_CACHE_HOME_URL is set to false
199 *
200 * @since 1.4
201 */
202 public function _languages_list() {
203 // cache the languages after getting the home urls
204 if ((!defined('PLL_CACHE_LANGUAGES') || PLL_CACHE_LANGUAGES) && false === get_transient('pll_languages_list')) {
205 foreach ($languages = $this->cache->get('languages') as $language)
206 $language->set_home_url();
207
208 // don't store directly objects as it badly break with some hosts (GoDaddy) due to race conditions when using object cache
209 // thanks to captin411 for catching this!
210 // see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255
211 set_transient('pll_languages_list', array_map(create_function('$o', 'return (array) $o;'), $languages));
212 }
213
214 foreach ($this->cache->get('languages') as $language) {
215 // get the home urls when not cached
216 if ((defined('PLL_CACHE_LANGUAGES') && !PLL_CACHE_LANGUAGES) || (defined('PLL_CACHE_HOME_URL') && !PLL_CACHE_HOME_URL))
217 $language->set_home_url();
218
219 // ensures that the (possibly cached) home url uses the right scheme http or https
220 $language->set_home_url_scheme();
221
222 // use custom flags on frontend only
223 if (!PLL_ADMIN)
224 $language->set_custom_flag();
225 }
226 }
227
228 /*
229 * cleans language cache
230 * can be called directly with no parameter
231 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
232 *
233 * @since 1.2
234 *
235 * @param int $term not used
236 * @param string $taxonomy taxonomy name
237 */
238 public function clean_languages_cache($term = 0, $taxonomy = null) {
239 // depending on WP version, the action is passed an object or a string
240 // backward compatibility with WP < 4.2
241 if ( !empty($taxonomy) && is_object($taxonomy) ) {
242 $taxonomy = $taxonomy->name;
243 }
244
245 if (empty($taxonomy) || 'language' == $taxonomy) {
246 delete_transient('pll_languages_list');
247 $this->cache->clean();
248 }
249 }
250
251 /*
252 * returns the language by its term_id, tl_term_id, slug or locale
253 *
254 * @since 0.1
255 *
256 * @param int|string term_id, tl_term_id, slug or locale of the queried language
257 * @return object|bool PLL_Language object, false if no language found
258 */
259 public function get_language($value) {
260 if (is_object($value))
261 return $this->get_language($value->term_id); // will force cast to PLL_Language
262
263 if (false === $return = $this->cache->get('language:' . $value)) {
264 foreach ($this->get_languages_list() as $lang) {
265 $this->cache->set('language:' . $lang->term_id, $lang);
266 $this->cache->set('language:' . $lang->tl_term_id, $lang);
267 $this->cache->set('language:' . $lang->slug, $lang);
268 $this->cache->set('language:' . $lang->locale, $lang);
269 }
270 $return = $this->cache->get('language:' . $value);
271 }
272
273 return $return;
274 }
275
276 /*
277 * saves translations for posts or terms
278 *
279 * @since 0.5
280 *
281 * @param string $type either 'post' or 'term'
282 * @param int $id post id or term id
283 * @param array $translations: an associative array of translations with language code as key and translation id as value
284 */
285 public function save_translations($type, $id, $translations) {
286 $id = (int) $id;
287
288 if (($lang = call_user_func(array(&$this, 'get_'.$type.'_language'), $id)) && isset($translations) && is_array($translations)) {
289 // sanitize the translations array
290 $translations = array_map('intval', $translations);
291 $translations = array_merge(array($lang->slug => $id), $translations); // make sure this object is in translations
292 $translations = array_diff($translations, array(0)); // don't keep non translated languages
293 $translations = array_intersect_key($translations, array_flip($this->get_languages_list(array('fields' => 'slug')))); // keep only valid languages slugs as keys
294
295 // unlink removed translations
296 $old_translations = $this->get_translations($type, $id);
297 foreach (array_diff_assoc($old_translations, $translations) as $object_id)
298 $this->delete_translation($type, $object_id);
299
300 // don't create a translation group for untranslated posts as it is useless
301 // but we need one for terms to allow relationships remap when importing from a WXR file
302 if ('term' == $type || count($translations) > 1) {
303 $terms = wp_get_object_terms($translations, $type . '_translations');
304 $term = reset($terms);
305
306 // create a new term if necessary
307 if (empty($term)) {
308 wp_insert_term($group = uniqid('pll_'), $type . '_translations', array('description' => serialize($translations)));
309 }
310 else {
311 // take care not to overwrite extra data stored in description field, if any
312 $d = unserialize($term->description);
313 $d = is_array($d) ? array_diff_key($d, $old_translations) : array(); // remove old translations
314 $d = array_merge($d, $translations); // add new one
315 wp_update_term($group = (int) $term->term_id, $type . '_translations', array('description' => serialize($d)));
316 }
317
318 // link all translations to the new term
319 foreach($translations as $p)
320 wp_set_object_terms($p, $group, $type . '_translations');
321
322 // clean now unused translation groups
323 foreach (wp_list_pluck($terms, 'term_id') as $term_id) {
324 $term = get_term($term_id, $type . '_translations');
325 if (empty($term->count))
326 wp_delete_term($term_id, $type . '_translations');
327 }
328 }
329 }
330 }
331
332 /*
333 * deletes a translation of a post or term
334 *
335 * @since 0.5
336 *
337 * @param string $type either 'post' or 'term'
338 * @param int $id post id or term id
339 */
340 public function delete_translation($type, $id) {
341 global $wpdb;
342 $id = (int) $id;
343 $term = $this->get_object_term($id, $type . '_translations');
344
345 if (!empty($term)) {
346 $d = unserialize($term->description);
347 $slug = array_search($id, $this->get_translations($type, $id)); // in case some plugin stores the same value with different key
348 unset($d[$slug]);
349
350 if (empty($d))
351 wp_delete_term((int) $term->term_id, $type . '_translations');
352 else
353 wp_update_term((int) $term->term_id, $type . '_translations', array('description' => serialize($d)));
354
355 if ('post' == $type)
356 wp_set_object_terms($id, null, $type . '_translations');
357
358 elseif ($wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->terms WHERE term_id = %d;", $id))) {
359 // always keep a group for terms to allow relationships remap when importing from a WXR file
360 $translations[$slug] = $id;
361 wp_insert_term($group = uniqid('pll_'), $type . '_translations', array('description' => serialize($translations)));
362 wp_set_object_terms($id, $group, $type . '_translations');
363 }
364 }
365 }
366
367 /*
368 * returns the id of the translation of a post or term
369 *
370 * @since 0.5
371 *
372 * @param string $type either 'post' or 'term'
373 * @param int $id post id or term id
374 * @param object|string $lang object or slug
375 * @return bool|int post id or term id of the translation, flase if there is none
376 */
377 public function get_translation($type, $id, $lang) {
378 if (!$lang = $this->get_language($lang))
379 return false;
380
381 $translations = $this->get_translations($type, $id);
382
383 return isset($translations[$lang->slug]) ? $translations[$lang->slug] : false;
384 }
385
386 /*
387 * returns an array of translations of a post or term
388 *
389 * @since 0.5
390 *
391 * @param string $type either 'post' or 'term'
392 * @param int $id post id or term id
393 * @return array an associative array of translations with language code as key and translation id as value
394 */
395 public function get_translations($type, $id) {
396 $type = ($type == 'post' || $this->is_translated_post_type($type)) ? 'post' : (($type == 'term' || $this->is_translated_taxonomy($type)) ? 'term' : false);
397 $translations = $type && ($term = $this->get_object_term($id, $type . '_translations')) && !empty($term) ? unserialize($term->description) : array();
398
399 // make sure we return only translations (thus we allow plugins to store other informations in the array)
400 $translations = array_intersect_key($translations, array_flip($this->get_languages_list(array('fields' => 'slug'))));
401
402 // make sure to return at least the passed post or term in its translation array
403 if (empty($translations) && $type && $lang = call_user_func(array(&$this, 'get_'.$type.'_language'), $id))
404 $translations = array($lang->slug => $id);
405
406 return $translations;
407 }
408
409 /*
410 * store the post language in the database
411 *
412 * @since 0.6
413 *
414 * @param int $post_id post id
415 * @param int|string|object language (term_id or slug or object)
416 */
417 public function set_post_language($post_id, $lang) {
418 wp_set_post_terms((int) $post_id, $lang ? $this->get_language($lang)->slug : '', 'language' );
419 }
420
421 /*
422 * returns the language of a post
423 *
424 * @since 0.1
425 *
426 * @param int $post_id post id
427 * @return bool|object PLL_Language object, false if no language is associated to that post
428 */
429 public function get_post_language($post_id) {
430 $lang = $this->get_object_term($post_id, 'language' );
431 return ($lang) ? $this->get_language($lang) : false;
432 }
433
434 /*
435 * among the post and its translations, returns the id of the post which is in $lang
436 *
437 * @since 0.1
438 *
439 * @param int $post_id post id
440 * @param int|string|object language (term_id or slug or object)
441 * @return bool|int the translation post id if exists, otherwise the post id, false if the post has no language
442 */
443 public function get_post($post_id, $lang) {
444 $post_lang = $this->get_post_language($post_id);
445 if (!$lang || !$post_lang)
446 return false;
447
448 $lang = $this->get_language($lang);
449 return $post_lang->term_id == $lang->term_id ? $post_id : $this->get_translation('post', $post_id, $lang);
450 }
451
452 /*
453 * stores the term language in the database
454 *
455 * @since 0.6
456 *
457 * @param int $term_id term id
458 * @param int|string|object language (term_id or slug or object)
459 */
460 public function set_term_language($term_id, $lang) {
461 $term_id = (int) $term_id;
462 wp_set_object_terms($term_id, $lang ? $this->get_language($lang)->tl_term_id : '', 'term_language');
463
464 // add translation group for correct WXR export
465 $translations = $this->get_translations('term', $term_id);
466 if ($slug = array_search($term_id, $translations))
467 unset($translations[$slug]);
468
469 $this->save_translations('term', $term_id, $translations);
470 }
471
472 /*
473 * removes the term language in database
474 *
475 * @since 0.5
476 *
477 * @param int $term_id term id
478 */
479 public function delete_term_language($term_id) {
480 wp_delete_object_term_relationships($term_id, 'term_language');
481 }
482
483 /*
484 * returns the language of a term
485 *
486 * @since 0.1
487 *
488 * @param int|string $value term id or term slug
489 * @param string $taxonomy optional taxonomy needed when the term slug is passed as first parameter
490 * @return bool|object PLL_Language object, false if no language is associated to that term
491 */
492 public function get_term_language($value, $taxonomy = '') {
493 if (is_numeric($value))
494 $term_id = $value;
495
496 // get_term_by still not cached in WP 3.5.1 but internally, the function is always called by term_id
497 elseif (is_string($value) && $taxonomy)
498 $term_id = get_term_by('slug', $value , $taxonomy)->term_id;
499
500 // get the language and make sure it is a PLL_Language object
501 return isset($term_id) && ($lang = $this->get_object_term($term_id, 'term_language')) ? $this->get_language($lang->term_id) : false;
502 }
503
504 /*
505 * among the term and its translations, returns the id of the term which is in $lang
506 *
507 * @since 0.1
508 *
509 * @param int $term_id term id
510 * @param int|string|object language (term_id or slug or object)
511 * @return bool|int the translation term id if exists, otherwise the term id, false if the term has no language
512 */
513 public function get_term($term_id, $lang) {
514 $lg = $this->get_term_language($term_id); // FIXME is this necessary?
515 if (!$lang || !$lg)
516 return false;
517
518 $lang = $this->get_language($lang);
519 return $lg->term_id == $lang->term_id ? $term_id : $this->get_translation('term', $term_id, $lang);
520 }
521
522 /*
523 * a join clause to add to sql queries when filtering by language is needed directly in query
524 *
525 * @since 1.2
526 *
527 * @param string $type either 'post' or 'term'
528 * @return string join clause
529 */
530 public function join_clause($type) {
531 global $wpdb;
532 return " INNER JOIN $wpdb->term_relationships AS pll_tr ON pll_tr.object_id = " . ('term' == $type ? "t.term_id" : "ID");
533 }
534
535 /*
536 * a where clause to add to sql queries when filtering by language is needed directly in query
537 *
538 * @since 1.2
539 *
540 * @param object|array|string $lang a PLL_Language object or a comma separated list of languag slug or an array of language slugs
541 * @param string $type either 'post' or 'term'
542 * @return string where clause
543 */
544 public function where_clause($lang, $type) {
545 global $wpdb;
546 $tt_id = 'term' == $type ? 'tl_term_taxonomy_id' : 'term_taxonomy_id';
547
548 // $lang is an object
549 // generally the case if the query is coming from Polylang
550 if (is_object($lang))
551 return $wpdb->prepare(" AND pll_tr.term_taxonomy_id = %d", $lang->$tt_id);
552
553 // $lang is a comma separated list of slugs (or an array of slugs)
554 // generally the case is the query is coming from outside with 'lang' parameter
555 $slugs = is_array($lang) ? $lang : explode(',', $lang);
556 foreach ($slugs as $slug)
557 $languages[] = (int) $this->get_language($slug)->$tt_id;
558
559 return " AND pll_tr.term_taxonomy_id IN (" . implode(',', $languages) . ")";
560 }
561
562 /*
563 * adds terms clauses to get_terms to filter them by languages - used in both frontend and admin
564 *
565 * @since 1.2
566 *
567 * @param array $clauses the list of sql clauses in terms query
568 * @param object $lang PLL_Language object
569 * @return array modifed list of clauses
570 */
571 public function terms_clauses($clauses, $lang) {
572 if (!empty($lang)) {
573 $clauses['join'] .= $this->join_clause('term');
574 $clauses['where'] .= $this->where_clause($lang, 'term');
575 }
576 return $clauses;
577 }
578
579 /*
580 * register the language taxonomy
581 *
582 * @since 1.2
583 */
584 public function register_taxonomy() {
585 // registers the language taxonomy
586 register_taxonomy('language', $this->get_translated_post_types(), array(
587 'labels' => array(
588 'name' => __('Languages', 'polylang'),
589 'singular_name' => __('Language', 'polylang'),
590 'all_items' => __('All languages', 'polylang'),
591 ),
592 'public' => false, // avoid displaying the 'like post tags text box' in the quick edit
593 'query_var' => 'lang',
594 'rewrite' => $this->options['force_lang'] < 2, // no rewrite for domains and sub-domains
595 '_pll' => true // polylang taxonomy
596 ));
597 }
598
599 /*
600 * returns post types that need to be translated
601 * the post types list is cached for better better performance
602 * wait for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php
603 *
604 * @since 1.2
605 *
606 * @param bool $filter true if we should return only valid registered post types
607 * @return array post type names for which Polylang manages languages and translations
608 */
609 public function get_translated_post_types($filter = true) {
610 if (did_action('after_setup_theme'))
611 static $post_types = null;
612
613 if (empty($post_types)) {
614 $post_types = array('post' => 'post', 'page' => 'page');
615
616 if (!empty($this->options['media_support']))
617 $post_types['attachement'] = 'attachment';
618
619 if (is_array($this->options['post_types']))
620 $post_types = array_merge($post_types, array_combine($this->options['post_types'], $this->options['post_types']));
621
622 $post_types = apply_filters('pll_get_post_types', $post_types , false);
623 }
624
625 return $filter ? array_intersect($post_types, get_post_types()) : $post_types;
626 }
627
628 /*
629 * check if registered post type must be translated
630 *
631 * @since 1.2
632 *
633 * @param string $post_type post type name
634 */
635 public function registered_post_type($post_type) {
636 if ($this->is_translated_post_type($post_type)) {
637 register_taxonomy_for_object_type('language', $post_type);
638 register_taxonomy_for_object_type('post_translations', $post_type);
639 }
640 }
641
642 /*
643 * returns true if Polylang manages languages and translations for this post type
644 *
645 * @since 1.2
646 *
647 * @param string|array $post_type post type name or array of post type names
648 * @return bool
649 */
650 public function is_translated_post_type($post_type) {
651 $post_types = $this->get_translated_post_types(false);
652 return (is_array($post_type) && array_intersect($post_type, $post_types) || in_array($post_type, $post_types));
653 }
654
655 /*
656 * return taxonomies that need to be translated
657 *
658 * @since 1.2
659 *
660 * @param bool $filter true if we should return only valid registered taxonmies
661 * @return array array of registered taxonomy names for which Polylang manages languages and translations
662 */
663 public function get_translated_taxonomies($filter = true) {
664 if (did_action('after_setup_theme'))
665 static $taxonomies = null;
666
667 if (empty($taxonomies)) {
668 $taxonomies = array('category' => 'category', 'post_tag' => 'post_tag');
669
670 if (is_array($this->options['taxonomies']))
671 $taxonomies = array_merge($taxonomies, array_combine($this->options['taxonomies'], $this->options['taxonomies']));
672
673 $taxonomies = apply_filters('pll_get_taxonomies', $taxonomies, false);
674 }
675
676 return $filter ? array_intersect($taxonomies, get_taxonomies()) : $taxonomies;
677 }
678
679 /*
680 * returns true if Polylang manages languages and translations for this taxonomy
681 *
682 * @since 1.2
683 *
684 * @param string|array $tax taxonomy name or array of taxonomy names
685 * @return bool
686 */
687 public function is_translated_taxonomy($tax) {
688 $taxonomies = $this->get_translated_taxonomies(false);
689 return (is_array($tax) && array_intersect($tax, $taxonomies) || in_array($tax, $taxonomies));
690 }
691
692 /*
693 * return taxonomies that need to be filtered (post_format like)
694 *
695 * @since 1.7
696 *
697 * @param bool $filter true if we should return only valid registered taxonomies
698 * @return array array of registered taxonomy names
699 */
700 public function get_filtered_taxonomies($filter = true) {
701 if (did_action('after_setup_theme'))
702 static $taxonomies = null;
703
704 if (empty($taxonomies)) {
705 $taxonomies = array('post_format' => 'post_format');
706 $taxonomies = apply_filters('pll_filtered_taxonomies', $taxonomies, false);
707 }
708
709 return $filter ? array_intersect($taxonomies, get_taxonomies()) : $taxonomies;
710 }
711
712 /*
713 * returns true if Polylang filters this taxonomy per language
714 *
715 * @since 1.7
716 *
717 * @param string|array $tax taxonomy name or array of taxonomy names
718 * @return bool
719 */
720 public function is_filtered_taxonomy($tax) {
721 $taxonomies = $this->get_filtered_taxonomies(false);
722 return (is_array($tax) && array_intersect($tax, $taxonomies) || in_array($tax, $taxonomies));
723 }
724
725 /*
726 * returns the query vars of all filtered taxonomies
727 *
728 * @since 1.7
729 *
730 * @return array
731 */
732 public function get_filtered_taxonomies_query_vars() {
733 $query_vars = array();
734 foreach ($this->get_filtered_taxonomies() as $filtered_tax) {
735 $tax = get_taxonomy($filtered_tax);
736 $query_vars[] = $tax->query_var;
737 }
738 return $query_vars;
739 }
740
741 /*
742 * create a default category for a language
743 *
744 * @since 1.2
745 *
746 * @param object|string|int $lang language
747 */
748 public function create_default_category($lang) {
749 $lang = $this->get_language($lang);
750
751 // create a new category
752 // FIXME this is translated in admin language when we would like it in $lang
753 $cat_name = __('Uncategorized');
754 $cat_slug = sanitize_title($cat_name . '-' . $lang->slug);
755 $cat = wp_insert_term($cat_name, 'category', array('slug' => $cat_slug));
756
757 // check that the category was not previously created (in case the language was deleted and recreated)
758 $cat = isset($cat->error_data['term_exists']) ? $cat->error_data['term_exists'] : $cat['term_id'];
759
760 // set language
761 $this->set_term_language((int) $cat, $lang);
762
763 // this is a translation of the default category
764 $default = (int) get_option('default_category');
765 $translations = $this->get_translations('term', $default);
766 if (empty($translations)) {
767 if ($lg = $this->get_term_language($default))
768 $translations[$lg->slug] = $default;
769 else
770 $translations = array();
771 }
772
773 $this->save_translations('term', (int) $cat, $translations);
774 }
775
776 /*
777 * it is possible to have several terms with the same name in the same taxonomy (one per language)
778 * but the native term_exists will return true even if only one exists
779 * so here the function adds the language parameter
780 *
781 * @since 1.4
782 *
783 * @param string $term_name the term name
784 * @param string $taxonomy taxonomy name
785 * @param int $parent parent term id
786 * @param string|object $language the language slug or object
787 * @return null|int the term_id of the found term
788 */
789 public function term_exists($term_name, $taxonomy, $parent, $language) {
790 global $wpdb;
791
792 $term_name = trim(wp_unslash($term_name));
793
794 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
795 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
796 $join .= $this->join_clause('term');
797 $where = $wpdb->prepare(" WHERE tt.taxonomy = %s AND t.name = %s", $taxonomy, $term_name);
798 $where .= $this->where_clause($this->get_language($language), 'term');
799
800 if ($parent > 0)
801 $where .= $wpdb->prepare(" AND tt.parent = %d", $parent);
802
803 return $wpdb->get_var($select . $join . $where);
804 }
805
806 /*
807 * gets the number of posts per language in a date, author or post type archive
808 *
809 * @since 1.2
810 *
811 * @param object lang
812 * @param array $q WP_Query arguments (accepted: post_type, m, year, monthnum, day, author, author_name, post_format)
813 * @return int
814 */
815 public function count_posts($lang, $q = array()) {
816 global $wpdb;
817
818 if (!is_array($q['post_type']))
819 $q['post_type'] = array($q['post_type']);
820
821 foreach ($q['post_type'] as $key => $type) {
822 if (!post_type_exists($type))
823 unset($q['post_type'][$key]);
824 }
825
826 if (empty($q['post_type']))
827 $q['post_type'] = array('post'); // we *need* a post type
828
829 $cache_key = md5(serialize($q));
830 $counts = wp_cache_get($cache_key, 'pll_count_posts');
831
832 if (false === $counts) {
833 $select = "SELECT pll_tr.term_taxonomy_id, COUNT(*) AS num_posts FROM {$wpdb->posts} AS p";
834 $join = $this->join_clause('post');
835 $where = " WHERE post_status = 'publish'";
836 $where .= $wpdb->prepare(" AND p.post_type IN ('%s')", join("', '", $q['post_type']));
837 $where .= $this->where_clause($this->get_languages_list(), 'post');
838 $groupby = " GROUP BY pll_tr.term_taxonomy_id";
839
840 if (!empty($q['m'])) {
841 $q['m'] = '' . preg_replace('|[^0-9]|', '', $q['m']);
842 $where .= $wpdb->prepare(" AND YEAR(p.post_date) = %d", substr($q['m'], 0, 4));
843 if ( strlen($q['m']) > 5 )
844 $where .= $wpdb->prepare(" AND MONTH(p.post_date) = %d", substr($q['m'], 4, 2));
845 if ( strlen($q['m']) > 7 )
846 $where .= $wpdb->prepare(" AND DAYOFMONTH(p.post_date) = %d", substr($q['m'], 6, 2));
847 }
848
849 if (!empty($q['year']))
850 $where .= $wpdb->prepare(" AND YEAR(p.post_date) = %d", $q['year']);
851
852 if (!empty($q['monthnum']))
853 $where .= $wpdb->prepare(" AND MONTH(p.post_date) = %d", $q['monthnum']);
854
855 if (!empty($q['day']))
856 $where .= $wpdb->prepare(" AND DAYOFMONTH(p.post_date) = %d", $q['day']);
857
858 if (!empty($q['author_name'])) {
859 $author = get_user_by('slug', sanitize_title_for_query($q['author_name']));
860 if ($author)
861 $q['author'] = $author->ID;
862 }
863
864 if (!empty($q['author']))
865 $where .= $wpdb->prepare(" AND p.post_author = %d", $q['author']);
866
867 // filtered taxonomies (post_format)
868 foreach ($this->get_filtered_taxonomies_query_vars() as $tax_qv) {
869 if (!empty($q[$tax_qv])) {
870 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = p.ID";
871 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
872 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
873 $where .= $wpdb->prepare(" AND t.slug = %s", $q[$tax_qv]);
874 }
875 }
876
877 $res = $wpdb->get_results($select . $join . $where . $groupby, ARRAY_A);
878 foreach ((array) $res as $row)
879 $counts[$row['term_taxonomy_id']] = $row['num_posts'];
880
881 wp_cache_set($cache_key, $counts, 'pll_count_posts');
882 }
883
884 return empty($counts[$lang->term_taxonomy_id]) ? 0 : $counts[$lang->term_taxonomy_id];
885 }
886
887 /*
888 * returns ids of objects in a language similarly to get_objects_in_term for a taxonomy
889 * faster than get_objects_in_term as it avoids a JOIN
890 *
891 * @since 1.4
892 *
893 * @param object $lang a PLL_Language object
894 * @param string $type optional, either 'post' or 'term', defaults to 'post'
895 * @return array
896 */
897 public function get_objects_in_language($lang, $type = 'post') {
898 global $wpdb;
899 return $wpdb->get_col($wpdb->prepare("
900 SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id = %d",
901 'term' == $type ? $lang->tl_term_taxonomy_id : $lang->term_taxonomy_id
902 ));
903 }
904
905 /*
906 * setup the links model based on options
907 *
908 * @since 1.2
909 *
910 * @return object implementing "links_model interface"
911 */
912 public function get_links_model() {
913 if (!$links_model = $this->cache->get('links_model')) {
914 $c = array('Directory', 'Directory', 'Subdomain', 'Domain');
915 $class = get_option('permalink_structure') ? 'PLL_Links_' .$c[$this->options['force_lang']] : 'PLL_Links_Default';
916 $links_model = new $class($this);
917 $this->cache->set('links_model', $links_model);
918 }
919 return $links_model;
920 }
921 }
922