PluginProbe
Polylang / 3.6.7
Polylang v3.6.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
← All changes | include/translated-post.php +236 -108 3.03.6.7 View file →
@@ -2,108 +2,178 @@
2 2 /**
3 3 * @package Polylang
4 4 */
5 5
6 +defined( 'ABSPATH' ) || exit;
7 +
6 8 /**
7 - * Setups the posts languages and translations model
9 + * Sets the posts languages and translations model up.
8 10 *
9 11 * @since 1.8
12 + *
13 + * @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface
10 14 */
11 -class PLL_Translated_Post extends PLL_Translated_Object {
15 +class PLL_Translated_Post extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface {
16 + use PLL_Translatable_Object_With_Types_Trait;
12 17
13 18 /**
14 - * Constructor.
19 + * Taxonomy name for the languages.
15 20 *
16 - * @since 1.8
21 + * @var string
17 22 *
18 - * @param PLL_Model $model PLL_Model instance.
23 + * @phpstan-var non-empty-string
19 24 */
20 - public function __construct( &$model ) {
21 - // init properties
22 - $this->object_type = null; // For taxonomies
23 - $this->type = 'post'; // For capabilities
24 - $this->tax_language = 'language';
25 - $this->tax_translations = 'post_translations';
26 - $this->tax_tt = 'term_taxonomy_id';
25 + protected $tax_language = 'language';
27 26
28 - parent::__construct( $model );
27 + /**
28 + * Identifier that must be unique for each type of content.
29 + * Also used when checking capabilities.
30 + *
31 + * @var string
32 + *
33 + * @phpstan-var non-empty-string
34 + */
35 + protected $type = 'post';
29 36
30 - // registers completely the language taxonomy
31 - add_action( 'setup_theme', array( $this, 'register_taxonomy' ), 1 );
37 + /**
38 + * Identifier for each type of content to used for cache type.
39 + *
40 + * @var string
41 + *
42 + * @phpstan-var non-empty-string
43 + */
44 + protected $cache_type = 'posts';
32 45
33 - // setups post types to translate
34 - add_action( 'registered_post_type', array( $this, 'registered_post_type' ) );
46 + /**
47 + * Taxonomy name for the translation groups.
48 + *
49 + * @var string
50 + *
51 + * @phpstan-var non-empty-string
52 + */
53 + protected $tax_translations = 'post_translations';
35 54
36 - // forces updating posts cache
37 - add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
38 - }
39 -
40 55 /**
41 - * Store the post language in the database.
56 + * Constructor.
42 57 *
43 - * @since 0.6
58 + * @since 1.8
44 59 *
45 - * @param int $post_id Post id.
46 - * @param int|string|PLL_Language $lang Language (term_id or slug or object).
47 - * @return void
60 + * @param PLL_Model $model Instance of `PLL_Model`.
48 61 */
49 - public function set_language( $post_id, $lang ) {
50 - $old_lang = $this->get_language( $post_id );
51 - $old_lang = $old_lang ? $old_lang->slug : '';
62 + public function __construct( PLL_Model &$model ) {
63 + parent::__construct( $model );
52 64
53 - $lang = $this->model->get_language( $lang );
54 - $lang = $lang ? $lang->slug : '';
55 -
56 - if ( $old_lang !== $lang ) {
57 - wp_set_post_terms( (int) $post_id, $lang, 'language' );
58 - }
65 + // Keep hooks in constructor for backward compatibility.
66 + $this->init();
59 67 }
60 68
61 69 /**
62 - * Returns the language of a post
70 + * Adds hooks.
63 71 *
64 - * @since 0.1
72 + * @since 3.4
65 73 *
66 - * @param int $post_id post id
67 - * @return PLL_Language|false PLL_Language object, false if no language is associated to that post
74 + * @return static
68 75 */
69 - public function get_language( $post_id ) {
70 - $lang = $this->get_object_term( $post_id, 'language' );
71 - return ( $lang ) ? $this->model->get_language( $lang ) : false;
76 + public function init() {
77 + // Registers completely the language taxonomy.
78 + add_action( 'setup_theme', array( $this, 'register_taxonomy' ), 1 );
79 +
80 + // Setups post types to translate.
81 + add_action( 'registered_post_type', array( $this, 'registered_post_type' ) );
82 +
83 + // Forces updating posts cache.
84 + add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
85 + return parent::init();
72 86 }
73 87
74 88 /**
75 - * Deletes a translation.
89 + * Deletes a translation of a post.
76 90 *
77 91 * @since 0.5
78 92 *
79 - * @param int $id Post id.
93 + * @param int $id Post ID.
80 94 * @return void
81 95 */
82 96 public function delete_translation( $id ) {
97 + $id = $this->sanitize_int_id( $id );
98 +
99 + if ( empty( $id ) ) {
100 + return;
101 + }
102 +
83 103 parent::delete_translation( $id );
84 104 wp_set_object_terms( $id, array(), $this->tax_translations );
85 105 }
86 106
87 107 /**
88 - * A join clause to add to sql queries when filtering by language is needed directly in query
108 + * Returns object types (post types) that need to be translated.
109 + * The post types list is cached for better performance.
110 + * The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php.
89 111 *
90 - * @since 1.2
112 + * @since 3.4
91 113 *
92 - * @param string $alias Alias for $wpdb->posts table
93 - * @return string join clause
114 + * @param bool $filter True if we should return only valid registered object types.
115 + * @return string[] Object type names for which Polylang manages languages.
116 + *
117 + * @phpstan-return array<non-empty-string, non-empty-string>
94 118 */
95 - public function join_clause( $alias = '' ) {
96 - global $wpdb;
97 - if ( empty( $alias ) ) {
98 - $alias = $wpdb->posts;
119 + public function get_translated_object_types( $filter = true ) {
120 + $post_types = $this->model->cache->get( 'post_types' );
121 +
122 + if ( false === $post_types ) {
123 + $post_types = array( 'post' => 'post', 'page' => 'page', 'wp_block' => 'wp_block' );
124 +
125 + if ( ! empty( $this->model->options['post_types'] ) && is_array( $this->model->options['post_types'] ) ) {
126 + $post_types = array_merge( $post_types, array_combine( $this->model->options['post_types'], $this->model->options['post_types'] ) );
127 + }
128 +
129 + if ( empty( $this->model->options['media_support'] ) ) {
130 + // In case the post type attachment is stored in the option.
131 + unset( $post_types['attachment'] );
132 + } else {
133 + $post_types['attachment'] = 'attachment';
134 + }
135 +
136 + /**
137 + * Filters the list of post types available for translation.
138 + * The default are post types which have the parameter ‘public’ set to true.
139 + * The filter must be added soon in the WordPress loading process:
140 + * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
141 + *
142 + * @since 0.8
143 + *
144 + * @param string[] $post_types List of post type names (as array keys and values).
145 + * @param bool $is_settings True when displaying the list of custom post types in Polylang settings.
146 + */
147 + $post_types = (array) apply_filters( 'pll_get_post_types', $post_types, false );
148 +
149 + if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) {
150 + $this->model->cache->set( 'post_types', $post_types );
151 + }
99 152 }
100 - return " INNER JOIN $wpdb->term_relationships AS pll_tr ON pll_tr.object_id = $alias.ID";
153 +
154 + /** @var array<non-empty-string, non-empty-string> $post_types */
155 + return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
101 156 }
102 157
103 158 /**
104 - * Register the language taxonomy
159 + * Returns true if Polylang manages languages for this object type.
105 160 *
161 + * @since 3.4
162 + *
163 + * @param string|string[] $object_type Object type (post type) name or array of object type names.
164 + * @return bool
165 + *
166 + * @phpstan-param non-empty-string|non-empty-string[] $object_type
167 + */
168 + public function is_translated_object_type( $object_type ) {
169 + $post_types = $this->get_translated_object_types( false );
170 + return ( is_array( $object_type ) && array_intersect( $object_type, $post_types ) || in_array( $object_type, $post_types ) || 'any' === $object_type && ! empty( $post_types ) );
171 + }
172 +
173 + /**
174 + * Registers the language taxonomy.
175 + *
106 176 * @since 1.2
107 177 *
108 178 * @return void
109 179 */
@@ -108,39 +178,36 @@
108 178 * @return void
109 179 */
110 180 public function register_taxonomy() {
111 181 register_taxonomy(
112 - 'language',
182 + $this->tax_language,
113 183 $this->model->get_translated_post_types(),
114 184 array(
115 - 'labels' => array(
116 - 'name' => __( 'Languages', 'polylang' ),
117 - 'singular_name' => __( 'Language', 'polylang' ),
118 - 'all_items' => __( 'All languages', 'polylang' ),
119 - ),
120 185 'public' => false,
121 - 'show_ui' => false, // hide the taxonomy on admin side, needed for WP 4.4.x
122 - 'show_in_nav_menus' => false, // no metabox for nav menus, needed for WP 4.4.x
123 - 'publicly_queryable' => true, // since WP 4.5
186 + 'show_ui' => false, // Hide the taxonomy on admin side, needed for WP 4.4.x.
187 + 'show_in_nav_menus' => false, // No metabox for nav menus, needed for WP 4.4.x.
188 + 'publicly_queryable' => true, // Since WP 4.5.
124 189 'query_var' => 'lang',
125 - 'rewrite' => $this->model->options['force_lang'] < 2, // no rewrite for domains and sub-domains
126 - '_pll' => true, // polylang taxonomy
190 + 'rewrite' => false, // Rewrite rules are added through filters when needed.
191 + '_pll' => true, // Polylang taxonomy.
127 192 )
128 193 );
129 194 }
130 195
131 196 /**
132 - * Check if registered post type must be translated
197 + * Checks if registered post type must be translated.
133 198 *
134 199 * @since 1.2
135 200 *
136 - * @param string $post_type post type name
201 + * @param string $post_type Post type name.
137 202 * @return void
203 + *
204 + * @phpstan-param non-empty-string $post_type
138 205 */
139 206 public function registered_post_type( $post_type ) {
140 207 if ( $this->model->is_translated_post_type( $post_type ) ) {
141 - register_taxonomy_for_object_type( 'language', $post_type );
142 - register_taxonomy_for_object_type( 'post_translations', $post_type );
208 + register_taxonomy_for_object_type( $this->tax_language, $post_type );
209 + register_taxonomy_for_object_type( $this->tax_translations, $post_type );
143 210 }
144 211 }
145 212
146 213 /**
@@ -159,19 +226,28 @@
159 226 }
160 227 }
161 228
162 229 /**
163 - * Checks if the current user can read the post
230 + * Checks if the current user can read the post.
164 231 *
165 232 * @since 1.5
233 + * @since 3.4 Renamed the parameter $post_id into $id.
166 234 *
167 - * @param int $post_id Post ID
168 - * @param string $context Optional, 'edit' or 'view', defaults to 'view'.
235 + * @param int $id Post ID
236 + * @param string $context Optional, 'edit' or 'view'. Defaults to 'view'.
169 237 * @return bool
238 + *
239 + * @phpstan-param non-empty-string $context
170 240 */
171 - public function current_user_can_read( $post_id, $context = 'view' ) {
172 - $post = get_post( $post_id );
241 + public function current_user_can_read( $id, $context = 'view' ) {
242 + $id = $this->sanitize_int_id( $id );
173 243
244 + if ( empty( $id ) ) {
245 + return false;
246 + }
247 +
248 + $post = get_post( $id );
249 +
174 250 if ( empty( $post ) ) {
175 251 return false;
176 252 }
177 253
@@ -188,11 +264,21 @@
188 264 }
189 265
190 266 // Follow WP practices, which shows links to private posts ( when readable ), but not for draft posts ( ex: get_adjacent_post_link() )
191 267 if ( in_array( $post->post_status, get_post_stati( array( 'private' => true ) ) ) ) {
268 + if ( ! is_user_logged_in() ) {
269 + return false;
270 + }
271 +
272 + $user = wp_get_current_user();
273 +
274 + if ( (int) $user->ID === (int) $post->post_author ) {
275 + return true;
276 + }
277 +
192 278 $post_type_object = get_post_type_object( $post->post_type );
193 - $user = wp_get_current_user();
194 - return is_user_logged_in() && ( current_user_can( $post_type_object->cap->read_private_posts ) || $user->ID == $post->post_author ); // Comparison must not be strict!
279 +
280 + return ! empty( $post_type_object ) && current_user_can( $post_type_object->cap->read_private_posts );
195 281 }
196 282
197 283 // In edit context, show draft and future posts.
198 284 if ( 'edit' === $context ) {
@@ -204,9 +290,9 @@
204 290 );
205 291
206 292 if ( in_array( $post->post_status, $states ) ) {
207 293 $user = wp_get_current_user();
208 - return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || $user->ID == $post->post_author ); // Comparison must not be strict!
294 + return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || (int) $user->ID === (int) $post->post_author );
209 295 }
210 296 }
211 297
212 298 return false;
@@ -212,10 +298,9 @@
212 298 return false;
213 299 }
214 300
215 301 /**
216 - * Returns a list of posts in a language ( $lang )
217 - * not translated in another language ( $untranslated_in ).
302 + * Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in).
218 303 *
219 304 * @since 2.6
220 305 *
221 306 * @param string $type Post type.
@@ -223,46 +308,89 @@
223 308 * @param PLL_Language $lang Language of the searched posts.
224 309 * @param string $search Limit the results to the posts matching this string.
225 310 * @return WP_Post[] Array of posts.
226 311 */
227 - public function get_untranslated( $type, $untranslated_in, $lang, $search = '' ) {
228 - $return = array();
312 + public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) {
313 + global $wpdb;
229 314
230 - // Don't order by title: see https://wordpress.org/support/topic/find-translated-post-when-10-is-not-enough
231 - $args = array(
232 - 's' => $search,
233 - 'suppress_filters' => 0, // To make the post_fields filter work
234 - 'lang' => 0, // Avoid admin language filter
235 - 'numberposts' => 20, // Limit to 20 posts
236 - 'post_status' => 'any',
237 - 'post_type' => $type,
238 - 'tax_query' => array(
239 - array(
240 - 'taxonomy' => 'language',
241 - 'field' => 'term_taxonomy_id', // WP 3.5+
242 - 'terms' => $lang->term_taxonomy_id,
243 - ),
244 - ),
245 - );
246 -
315 + $args = array( 'numberposts' => 20 ); // Limit to 20 posts by default.
247 316 /**
248 - * Filter the query args when auto suggesting untranslated posts in the Languages metabox
249 - * This should help plugins to fix some edge cases
317 + * Filters the query args when auto suggesting untranslated posts in the Languages metabox.
250 318 *
251 - * @see https://wordpress.org/support/topic/find-translated-post-when-10-is-not-enough
252 - *
253 319 * @since 1.7
320 + * @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts.
321 + * No `WP_Query` is made anymore, a custom one is used instead.
254 322 *
255 323 * @param array $args WP_Query arguments
256 324 */
257 - $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
258 - $posts = get_posts( $args );
325 + $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
259 326
260 - foreach ( $posts as $post ) {
261 - if ( $post instanceof WP_Post && ! $this->get_translation( $post->ID, $untranslated_in ) && $this->current_user_can_read( $post->ID, 'edit' ) ) {
262 - $return[] = $post;
327 + $limit = $args['numberposts'];
328 + $search_like = '%' . $wpdb->esc_like( $search ) . '%';
329 + $untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%';
330 + $posts = $wpdb->get_results(
331 + $wpdb->prepare(
332 + "SELECT * FROM {$wpdb->posts}
333 + INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id)
334 + AND tr1.term_taxonomy_id IN (%d)
335 + AND (({$wpdb->posts}.post_title LIKE %s)
336 + OR ({$wpdb->posts}.post_excerpt LIKE %s)
337 + OR ({$wpdb->posts}.post_content LIKE %s)
338 + )
339 + AND {$wpdb->posts}.post_type = %s
340 + AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft')
341 + WHERE {$wpdb->posts}.ID NOT IN (
342 + SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}
343 + LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id)
344 + INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id)
345 + AND (tt.taxonomy = 'post_translations')
346 + AND tt.description LIKE %s
347 + )
348 + LIMIT 0, %d",
349 + $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ),
350 + $search_like,
351 + $search_like,
352 + $search_like,
353 + $type,
354 + $untranslated_like,
355 + $limit
356 + )
357 + );
358 +
359 + foreach ( $posts as $i => $post ) {
360 + if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) {
361 + unset( $posts[ $i ] );
362 + continue;
263 363 }
364 +
365 + $posts[ $i ] = new WP_Post( $post );
264 366 }
265 367
266 - return $return;
368 + return $posts;
369 + }
370 +
371 + /**
372 + * Returns database-related information that can be used in some of this class methods.
373 + * These are specific to the table containing the objects.
374 + *
375 + * @see PLL_Translatable_Object::join_clause()
376 + * @see PLL_Translatable_Object::get_objects_with_no_lang_sql()
377 + *
378 + * @since 3.4.3
379 + *
380 + * @return string[] {
381 + * @type string $table Name of the table.
382 + * @type string $id_column Name of the column containing the object's ID.
383 + * @type string $type_column Name of the column containing the object's type.
384 + * @type string $default_alias Default alias corresponding to the object's table.
385 + * }
386 + * @phpstan-return DBInfoWithType
387 + */
388 + protected function get_db_infos() {
389 + return array(
390 + 'table' => $GLOBALS['wpdb']->posts,
391 + 'id_column' => 'ID',
392 + 'type_column' => 'post_type',
393 + 'default_alias' => $GLOBALS['wpdb']->posts,
394 + );
267 395 }
268 396 }