PluginProbe
Polylang / 1.1
Polylang v1.1
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
← All changes | include/base.php +349 -136 2.71.1 View file →
@@ -1,169 +1,382 @@
1 1 <?php
2 2
3 -/**
4 - * Base class for both admin and frontend
5 - *
6 - * @since 1.2
7 - */
8 -abstract class PLL_Base {
9 - public $links_model, $model, $options;
3 +// setups basic functions used for admin and frontend
4 +abstract class Polylang_Base {
5 + public $post_types; // post types to filter by language
6 + public $taxonomies; // taxonomies to filter by language
7 + public $is_ajax_on_front;
8 + public $is_admin;
10 9
11 - /**
12 - * Constructor
13 - *
14 - * @since 1.2
15 - *
16 - * @param object $links_model
17 - */
18 - public function __construct( &$links_model ) {
19 - $this->links_model = &$links_model;
20 - $this->model = &$links_model->model;
21 - $this->options = &$this->model->options;
10 + protected $options;
11 + protected $home;
12 + protected $using_permalinks;
13 + protected $strings = array(); // strings to translate
22 14
23 - $GLOBALS['l10n_unloaded']['pll_string'] = true; // Short-circuit _load_textdomain_just_in_time() for 'pll_string' domain in WP 4.6+
15 + // used to cache results
16 + // FIXME use wp_cache ?
17 + private $languages_list = array();
18 + private $language = array();
19 + private $links = array();
24 20
25 - add_action( 'widgets_init', array( $this, 'widgets_init' ) );
21 + function __construct() {
22 + // avoid loading polylang admin filters for frontend ajax requests if 'pll_load_front' is set (thanks to g100g)
23 + // FIXME should I permit plugins to overwrite this?
24 + $this->is_ajax_on_front = defined('DOING_AJAX') && isset($_REQUEST['pll_load_front']);
25 + $this->is_admin = defined('DOING_CRON') || (is_admin() && !$this->is_ajax_on_front);
26 26
27 - // User defined strings translations
28 - add_action( 'pll_language_defined', array( $this, 'load_strings_translations' ), 5 );
29 - add_action( 'change_locale', array( $this, 'load_strings_translations' ) ); // Since WP 4.7
27 + // init options often needed
28 + $this->options = get_option('polylang');
29 + $this->home = get_option('home');
30 + $this->using_permalinks = (bool) get_option('permalink_structure');
30 31
31 - // Switch_to_blog
32 - add_action( 'switch_blog', array( $this, 'switch_blog' ), 10, 2 );
32 + add_action('wp_loaded', array(&$this, 'add_post_types_taxonomies'), 1); // must come after 'init' to be sure that all post types and taxonomies are registered
33 33 }
34 34
35 - /**
36 - * Instantiate classes always needed
37 - *
38 - * @since 2.6
39 - */
40 - public function init() {
41 - // REST API
42 - if ( class_exists( 'PLL_REST_API' ) ) {
43 - $this->rest_api = new PLL_REST_API( $this );
35 + // init post types and taxonomies to filter by language
36 + function add_post_types_taxonomies() {
37 + $post_types = array('post', 'page');
38 + if (!empty($this->options['media_support']))
39 + $post_types[] = 'attachment';
40 + if (!empty($this->options['post_types']))
41 + $post_types = array_merge($post_types, $this->options['post_types']);
42 + $this->post_types = apply_filters('pll_get_post_types', array_combine($post_types, $post_types), false);
43 +
44 + $taxonomies = array('category', 'post_tag', 'nav_menu');
45 + if (!empty($this->options['taxonomies']))
46 + $taxonomies = array_merge($taxonomies, $this->options['taxonomies']);
47 + $this->taxonomies = apply_filters('pll_get_taxonomies', array_combine($taxonomies, $taxonomies), false);
48 + }
49 +
50 + // returns the list of available languages
51 + function get_languages_list($args = array()) {
52 + // although get_terms is cached, it is efficient to add our own cache
53 + // FIXME don't use on admin in case we modify the list of languages!
54 + if (!$this->is_admin && isset($this->languages_list[$cache_key = md5(serialize($args))]))
55 + return $this->languages_list[$cache_key];
56 +
57 + $args = wp_parse_args($args, array('hide_empty' => false, 'orderby'=> 'term_group'));
58 + $languages = get_terms('language', $args);
59 + $languages = empty($languages) || is_wp_error($languages) ? array() : $languages;
60 + return isset($cache_key) ? ($this->languages_list[$cache_key] = $languages) : $languages;
61 + }
62 +
63 + // retrieves the dropdown list of the languages
64 + function dropdown_languages($args = array()) {
65 + $args = apply_filters('pll_dropdown_language_args', $args);
66 + $defaults = array('name' => 'lang_choice', 'class' => '', 'add_options' => array(), 'hide_empty' => false, 'value' => 'slug', 'selected' => '');
67 + extract(wp_parse_args($args, $defaults));
68 +
69 + // sort $add_options by value
70 + uasort($add_options, create_function('$a, $b', "return \$a['value'] > \$b['value'];" ));
71 +
72 + $out = sprintf(
73 + '<select name="%1$s" id="%1$s"%2$s>'."\n",
74 + esc_attr($name),
75 + $class ? ' class="'.esc_attr($class).'"' : ''
76 + );
77 +
78 + foreach ($add_options as $option)
79 + $out .= sprintf(
80 + '<option value="%s">%s</option>'."\n",
81 + esc_attr($option['value']),
82 + esc_html($option['text'])
83 + );
84 +
85 + foreach ($this->get_languages_list($args) as $language) {
86 + $out .= sprintf(
87 + '<option value="%s"%s>%s</option>'."\n",
88 + esc_attr($language->$value),
89 + $language->$value == $selected ? ' selected="selected"' : '',
90 + esc_html($language->name)
91 + );
44 92 }
93 + $out .= "</select>\n";
94 + return $out;
95 + }
45 96
46 - if ( $this->model->get_languages_list() ) {
47 - // Used by content duplicate and post synchronization
48 - if ( class_exists( 'PLL_Sync_Content' ) ) {
49 - $this->sync_content = new PLL_Sync_Content( $this );
50 - }
97 + // returns the language by its id or its slug
98 + // Note: it seems that get_term_by slug is not cached (3.2.1)
99 + function get_language($value) {
100 + if (is_object($value))
101 + return $value;
51 102
52 - // Active languages
53 - if ( class_exists( 'PLL_Active_Languages' ) ) {
54 - $this->active_languages = new PLL_Active_Languages( $this );
55 - }
103 + if (isset($this->language[$value]))
104 + return $this->language[$value];
56 105
57 - // Share post slugs
58 - if ( get_option( 'permalink_structure' ) && $this->options['force_lang'] && class_exists( 'PLL_Share_Post_Slug' ) ) {
59 - $this->share_post_slug = new PLL_Share_Post_Slug( $this );
60 - }
106 + $lang = (is_numeric($value) || (int) $value) ? get_term((int) $value, 'language') :
107 + (is_string($value) ? get_term_by('slug', $value , 'language') : false);
108 +
109 + return !empty($lang) && !is_wp_error($lang) ? ($this->language[$value] = $lang) : false;
110 + }
111 +
112 + // saves translations for posts or terms
113 + // the underscore in '_lang' hides the post meta in the Custom Fields metabox in the Edit Post screen
114 + // $type: either 'post' or 'term'
115 + // $id: post id or term id
116 + // $translations: an associative array of translations with language code as key and translation id as value
117 + function save_translations($type, $id, $translations) {
118 + $lang = call_user_func(array(&$this, 'get_'.$type.'_language'), $id);
119 + if (!$lang)
120 + return;
121 +
122 + if (isset($translations) && is_array($translations)) {
123 + $translations = array_merge(array($lang->slug => $id), $translations);
124 + foreach($translations as $p)
125 + update_metadata($type, (int) $p, '_translations', $translations);
61 126 }
62 127 }
63 128
64 - /**
65 - * Registers our widgets
66 - *
67 - * @since 0.1
68 - */
69 - public function widgets_init() {
70 - register_widget( 'PLL_Widget_Languages' );
129 + // deletes a translation of a post or term
130 + function delete_translation($type, $id) {
131 + $translations = $this->get_translations($type, $id);
132 + if (is_array($translations)) {
133 + $slug = array_search($id, $translations);
134 + unset($translations[$slug]);
135 + foreach($translations as $p)
136 + update_metadata($type, (int) $p, '_translations', $translations);
137 + delete_metadata($type, $id, '_translations');
138 + }
139 + }
71 140
72 - // Overwrites the calendar widget to filter posts by language
73 - if ( ! defined( 'PLL_WIDGET_CALENDAR' ) || PLL_WIDGET_CALENDAR ) {
74 - unregister_widget( 'WP_Widget_Calendar' );
75 - register_widget( 'PLL_Widget_Calendar' );
141 + // returns the id of the translation of a post or term
142 + // $type: either 'post' or 'term'
143 + // $id: post id or term id
144 + // $lang: object or slug (in the order of preference latest to avoid)
145 + function get_translation($type, $id, $lang) {
146 + $translations = $this->get_translations($type, $id);
147 + $slug = $this->get_language($lang)->slug;
148 + return isset($translations[$slug]) ? (int) $translations[$slug] : false;
149 + }
150 +
151 + // returns an array of translations of a post or term
152 + function get_translations($type, $id) {
153 + $type = ($type == 'post' || in_array($type, $this->post_types)) ? 'post' : (($type == 'term' || in_array($type, $this->taxonomies)) ? 'term' : false);
154 + // maybe_unserialize due to useless serialization in versions < 0.9
155 + return $type && ($meta = get_metadata($type, $id, '_translations', true)) ? maybe_unserialize($meta) : array();
156 + }
157 +
158 + // store the post language in the database
159 + function set_post_language($post_id, $lang) {
160 + wp_set_post_terms($post_id, $lang ? $this->get_language($lang)->slug : '', 'language' );
161 + }
162 +
163 + // returns the language of a post
164 + function get_post_language($post_id) {
165 + $lang = get_the_terms($post_id, 'language' );
166 + return ($lang) ? reset($lang) : false; // there's only one language per post : first element of the array returned
167 + }
168 +
169 + // among the post and its translations, returns the id of the post which is in $lang
170 + function get_post($post_id, $lang) {
171 + $post_lang = $this->get_post_language($post_id);
172 + if (!$lang || !$post_lang)
173 + return false;
174 +
175 + $lang = $this->get_language($lang);
176 + return $post_lang->term_id == $lang->term_id ? $post_id : $this->get_translation('post', $post_id, $lang);
177 + }
178 +
179 + // store the term language in the database
180 + function set_term_language($term_id, $lang) {
181 + update_metadata('term', $term_id, '_language', $lang ? $this->get_language($lang)->term_id : 0);
182 + }
183 +
184 + // remove the term language in the database
185 + function delete_term_language($term_id) {
186 + delete_metadata('term', $term_id, '_language');
187 + }
188 +
189 + // returns the language of a term
190 + function get_term_language($value, $taxonomy = '') {
191 + if (is_numeric($value))
192 + $term_id = $value;
193 +
194 + elseif (is_string($value) && $taxonomy)
195 + $term_id = get_term_by('slug', $value , $taxonomy)->term_id;
196 +
197 + return empty($term_id) ? false : $this->get_language(get_metadata('term', $term_id, '_language', true));
198 + }
199 +
200 + // among the term and its translations, returns the id of the term which is in $lang
201 + function get_term($term_id, $lang) {
202 + $lg = $this->get_term_language($term_id);
203 + if (!$lang || !$lg)
204 + return false;
205 +
206 + $lang = $this->get_language($lang);
207 + return $lg->term_id == $lang->term_id ? $term_id : $this->get_translation('term', $term_id, $lang);
208 + }
209 +
210 + // adds language information to a link when using pretty permalinks
211 + function add_language_to_link($url, $lang) {
212 + if (empty($lang))
213 + return $url;
214 +
215 + global $wp_rewrite;
216 + if ($this->using_permalinks) {
217 + $base = $this->options['rewrite'] ? '' : 'language/';
218 + $slug = $this->options['default_lang'] == $lang->slug && $this->options['hide_default'] ? '' : $base.$lang->slug.'/';
219 + return esc_url(str_replace($this->home.'/'.$wp_rewrite->root, $this->home.'/'.$wp_rewrite->root.$slug, $url));
76 220 }
221 +
222 + // special case for pages which do not accept adding the lang parameter
223 + elseif ('_get_page_link' != current_filter())
224 + return esc_url(add_query_arg( 'lang', $lang->slug, $url ));
225 +
226 + return $url;
77 227 }
78 228
79 - /**
80 - * Loads user defined strings translations
81 - *
82 - * @since 1.2
83 - * @since 2.1.3 $locale parameter added.
84 - *
85 - * @param string $locale Locale. Defaults to current locale.
86 - */
87 - public function load_strings_translations( $locale = '' ) {
88 - if ( empty( $locale ) ) {
89 - $locale = get_locale();
229 + // optionally rewrite posts, pages links to filter them by language
230 + // rewrite post format (and optionally categories and post tags) archives links to filter them by language
231 + function add_post_term_link_filters() {
232 + if ($this->options['force_lang'] && $this->using_permalinks) {
233 + foreach (array('post_link', '_get_page_link', 'post_type_link') as $filter)
234 + add_filter($filter, array(&$this, 'post_link'), 10, 2);
90 235 }
91 236
92 - $language = $this->model->get_language( $locale );
237 + add_filter('term_link', array(&$this, 'term_link'), 10, 3);
238 + }
93 239
94 - if ( ! empty( $language ) ) {
95 - $mo = new PLL_MO();
96 - $mo->import_from_db( $language );
97 - $GLOBALS['l10n']['pll_string'] = &$mo;
98 - } else {
99 - unset( $GLOBALS['l10n']['pll_string'] );
240 + // modifies post & page links
241 + function post_link($link, $post) {
242 + if (isset($this->links[$link]))
243 + return $this->links[$link];
244 +
245 + if ('post_type_link' == current_filter() && !in_array($post->post_type, $this->post_types))
246 + return $this->links[$link] = $link;
247 +
248 + if ('_get_page_link' == current_filter()) // this filter uses the ID instead of the post object
249 + $post = get_post($post);
250 +
251 + // /!\ when post_status in not "publish", WP does not use pretty permalinks
252 + return $this->links[$link] = $post->post_status != 'publish' ? $link : $this->add_language_to_link($link, $this->get_post_language($post->ID));
253 + }
254 +
255 + // modifies term link
256 + function term_link($link, $term, $tax) {
257 + if (isset($this->links[$link]))
258 + return $this->links[$link];
259 +
260 + return $this->links[$link] = ($tax == 'post_format' || ($this->options['force_lang'] && $this->using_permalinks && in_array($tax, $this->taxonomies))) ?
261 + $this->add_language_to_link($link, $this->get_term_language($term->term_id)) : $link;
262 + }
263 +
264 + // returns the html link to the flag if exists
265 + // $lang: object
266 + function get_flag($lang, $url_only = false) {
267 + if (file_exists(POLYLANG_DIR.($file = '/flags/'.$lang->description.'.png')))
268 + $url = POLYLANG_URL.$file;
269 +
270 + // overwrite with custom flags
271 + // never use custom flags on admin side
272 + if (!$this->is_admin && ( file_exists(PLL_LOCAL_DIR.($file = '/'.$lang->description.'.png')) || file_exists(PLL_LOCAL_DIR.($file = '/'.$lang->description.'.jpg')) ))
273 + $url = PLL_LOCAL_URL.$file;
274 +
275 + return apply_filters('pll_get_flag', empty($url) ? '' :
276 + ($url_only ? $url :
277 + sprintf(
278 + '<img src="%s" title="%s" alt="%s" />',
279 + esc_url($url),
280 + esc_attr(apply_filters('pll_flag_title', $lang->name, $lang->slug, $lang->description)),
281 + esc_attr($lang->name)
282 + )));
283 + }
284 +
285 + // get languages term_ids or term_taxonomy_ids for use in sql queries
286 + function _get_languages_for_sql($lang, $field) {
287 + global $wpdb;
288 + // the query is coming from Polylang and the $lang is an object
289 + if (is_object($lang))
290 + return $field == 'term_id' ? "'" . $lang->$field . "'" : $lang->$field;
291 +
292 + // the query is coming from outside with 'lang' parameter and $lang is a comma separated list of slugs (or an array of slugs)
293 + $languages = is_array($lang) ? $lang : explode(',', $lang);
294 + $languages = "'" . implode("','", array_map( 'sanitize_title_for_query', $languages)) . "'";
295 + $languages = $wpdb->get_col("
296 + SELECT $wpdb->term_taxonomy.$field FROM $wpdb->term_taxonomy
297 + INNER JOIN $wpdb->terms USING (term_id)
298 + WHERE taxonomy = 'language' AND $wpdb->terms.slug IN ($languages)
299 + "); // get ids from slugs
300 +
301 + if ($field == 'term_id')
302 + $languages = array_map(create_function('$v', 'return "\'" . $v . "\'";'), $languages);
303 +
304 + return implode(',', $languages);
305 + }
306 +
307 + // adds clauses to comments query - used in both frontend and admin
308 + function _comments_clauses($clauses, $lang) {
309 + global $wpdb;
310 + if (!empty($lang)) {
311 + // if this clause is not already added by WP
312 + if (!strpos($clauses['join'], '.ID'))
313 + $clauses['join'] .= " JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
314 +
315 + $clauses['join'] .= " INNER JOIN $wpdb->term_relationships AS pll_tr ON pll_tr.object_id = ID";
316 + $clauses['where'] .= " AND pll_tr.term_taxonomy_id IN (" . $this->_get_languages_for_sql($lang, 'term_taxonomy_id') . ")";
100 317 }
318 + return $clauses;
101 319 }
102 320
103 - /**
104 - * Resets some variables when switching blog
105 - * Applies only if Polylang is active on the new blog
106 - *
107 - * @since 1.5.1
108 - *
109 - * @param int $new_blog
110 - * @param int $old_blog
111 - * @return bool not used by WP but by child class
112 - */
113 - public function switch_blog( $new_blog, $old_blog ) {
114 - $plugins = ( $sitewide_plugins = get_site_option( 'active_sitewide_plugins' ) ) && is_array( $sitewide_plugins ) ? array_keys( $sitewide_plugins ) : array();
115 - $plugins = array_merge( $plugins, get_option( 'active_plugins', array() ) );
321 + // adds terms clauses to get_terms - used in both frontend and admin
322 + function _terms_clauses($clauses, $lang) {
323 + global $wpdb;
324 + if (!empty($lang)) {
325 + $clauses['join'] .= " LEFT JOIN $wpdb->termmeta AS pll_tm ON t.term_id = pll_tm.term_id";
326 + $clauses['where'] .= " AND pll_tm.meta_key = '_language' AND pll_tm.meta_value IN (" . $this->_get_languages_for_sql($lang, 'term_id') . ")";
327 + }
328 + return $clauses;
329 + }
116 330
117 - // 2nd test needed when Polylang is not networked activated
118 - // 3rd test needed when Polylang is networked activated and a new site is created
119 - if ( $new_blog != $old_blog && in_array( POLYLANG_BASENAME, $plugins ) && get_option( 'polylang' ) ) {
120 - $this->options = get_option( 'polylang' ); // Needed for menus
121 - remove_action( 'pre_option_rewrite_rules', array( $this->links_model, 'prepare_rewrite_rules' ) );
122 - $this->links_model = $this->model->get_links_model();
123 - return true;
124 - }
125 - return false;
331 + // returns all page ids *not in* language defined by $lang_id
332 + function exclude_pages($lang_id) {
333 + return get_posts(array(
334 + 'lang' => 0, // so this query is not filtered by our pre_get_post filter in core.php
335 + 'numberposts' => -1,
336 + 'nopaging' => true,
337 + 'post_type' => array_intersect(get_post_types(array('hierarchical' => 1)), $this->post_types),
338 + 'fields' => 'ids',
339 + 'tax_query' => array(array(
340 + 'taxonomy' => 'language',
341 + 'terms' => $lang_id,
342 + 'operator' => 'NOT IN'
343 + ))
344 + ));
126 345 }
127 346
128 - /**
129 - * Some backward compatibility with Polylang < 1.2
130 - * Allows for example to call $polylang->get_languages_list() instead of $polylang->model->get_languages_list()
131 - * This works but should be slower than the direct call, thus an error is triggered in debug mode
132 - *
133 - * @since 1.2
134 - *
135 - * @param string $func function name
136 - * @param array $args function arguments
137 - */
138 - public function __call( $func, $args ) {
139 - foreach ( $this as $prop => &$obj ) {
140 - if ( is_object( $obj ) && method_exists( $obj, $func ) ) {
141 - if ( WP_DEBUG ) {
142 - $debug = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
143 - $i = 1 + empty( $debug[1]['line'] ); // The file and line are in $debug[2] if the function was called using call_user_func
144 - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
145 - sprintf(
146 - '%1$s was called incorrectly in %3$s on line %4$s: the call to $polylang->%1$s() has been deprecated in Polylang 1.2, use PLL()->%2$s->%1$s() instead.' . "\nError handler",
147 - esc_html( $func ),
148 - esc_html( $prop ),
149 - esc_html( $debug[ $i ]['file'] ),
150 - absint( $debug[ $i ]['line'] )
151 - )
152 - );
153 - }
154 - return call_user_func_array( array( $obj, $func ), $args );
155 - }
347 + // export a mo object in options
348 + function mo_export($mo, $lang) {
349 + $strings = array();
350 + foreach ($mo->entries as $entry)
351 + $strings[] = array($entry->singular, $mo->translate($entry->singular));
352 + update_option('polylang_mo'.$lang->term_id, $strings);
353 + }
354 +
355 + // import a mo object from options
356 + function mo_import($lang) {
357 + $mo = new MO();
358 + if ($strings = get_option('polylang_mo'.$lang->term_id)) {
359 + foreach ($strings as $msg)
360 + $mo->add_entry($mo->make_entry($msg[0], $msg[1]));
156 361 }
362 + return $mo;
363 + }
157 364
158 - $debug = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
159 - trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions
160 - sprintf(
161 - 'Call to undefined function PLL()->%1$s() in %2$s on line %3$s' . "\nError handler",
162 - esc_html( $func ),
163 - esc_html( $debug[0]['file'] ),
164 - absint( $debug[0]['line'] )
165 - ),
166 - E_USER_ERROR
365 + // list the post metas to synchronize
366 + static function list_metas_to_sync() {
367 + return array(
368 + 'taxonomies' => __('Taxonomies', 'polylang'),
369 + 'post_meta' => __('Custom fields', 'polylang'),
370 + 'comment_status' => __('Comment status', 'polylang'),
371 + 'ping_status' => __('Ping status', 'polylang'),
372 + 'sticky_posts' => __('Sticky posts', 'polylang'),
373 + 'post_date' => __('Published date', 'polylang'),
374 + 'post_format' => __('Post format', 'polylang'),
375 + 'post_parent' => __('Page parent', 'polylang'),
376 + '_wp_page_template' => __('Page template', 'polylang'),
377 + 'menu_order' => __('Page order', 'polylang'),
378 + '_thumbnail_id' => __('Featured image', 'polylang'),
167 379 );
168 380 }
169 -}
381 +
382 +} //class Polylang_Base