PluginProbe
Polylang / 1.7.3
Polylang v1.7.3
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.3, at include/model.php

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