PluginProbe
Polylang / 3.8.7
Polylang v3.8.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 / src / translated-post.php

translated-post.php in Polylang 3.8.7, at src/translated-post.php

573 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 use WP_Syntex\Polylang\Options\Options;
7
8 defined( 'ABSPATH' ) || exit;
9
10 /**
11 * Sets the posts languages and translations model up.
12 *
13 * @since 1.8
14 *
15 * @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface
16 */
17 class PLL_Translated_Post extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface {
18 use PLL_Translatable_Object_With_Types_Trait;
19
20 /**
21 * Taxonomy name for the languages.
22 *
23 * @var string
24 *
25 * @phpstan-var non-empty-string
26 */
27 protected $tax_language = 'language';
28
29 /**
30 * Identifier that must be unique for each type of content.
31 * Also used when checking capabilities.
32 *
33 * @var string
34 *
35 * @phpstan-var non-empty-string
36 */
37 protected $type = 'post';
38
39 /**
40 * Identifier for each type of content to used for cache type.
41 *
42 * @var string
43 *
44 * @phpstan-var non-empty-string
45 */
46 protected $cache_type = 'posts';
47
48 /**
49 * Taxonomy name for the translation groups.
50 *
51 * @var string
52 *
53 * @phpstan-var non-empty-string
54 */
55 protected $tax_translations = 'post_translations';
56
57 /**
58 * Constructor.
59 *
60 * @since 1.8
61 *
62 * @param PLL_Model $model Instance of `PLL_Model`.
63 */
64 public function __construct( PLL_Model $model ) {
65 parent::__construct( $model );
66
67 // Keep hooks in constructor for backward compatibility.
68 $this->init();
69 }
70
71 /**
72 * Registers the language taxonomy.
73 *
74 * @since 1.2
75 * @since 3.7 Protected and renamed from `register_taxonomy()`.
76 *
77 * @return void
78 */
79 protected function register_language_taxonomy(): void {
80 register_taxonomy(
81 $this->tax_language,
82 (array) $this->get_translated_object_types(),
83 array(
84 'public' => false,
85 'show_ui' => false, // Hide the taxonomy on admin side, needed for WP 4.4.x.
86 'show_in_nav_menus' => false, // No metabox for nav menus, needed for WP 4.4.x.
87 'publicly_queryable' => true, // Since WP 4.5.
88 'query_var' => 'lang', // See `add_language_taxonomy_query_var()`.
89 'rewrite' => false, // Rewrite rules are added through filters when needed.
90 '_pll' => true, // Polylang taxonomy.
91 )
92 );
93
94 add_action( 'setup_theme', array( $this, 'add_language_taxonomy_query_var' ) );
95 }
96
97 /**
98 * Adds the language query var once the global `$wp` is available.
99 *
100 * @since 3.7
101 * @see WP_Taxonomy::add_rewrite_rules()
102 *
103 * @return void
104 */
105 public function add_language_taxonomy_query_var(): void {
106 $GLOBALS['wp']->add_query_var( 'lang' );
107 }
108
109 /**
110 * Adds hooks.
111 *
112 * @since 3.4
113 *
114 * @return static
115 */
116 public function init() {
117 // Setups post types to translate.
118 add_action( 'registered_post_type', array( $this, 'registered_post_type' ) );
119
120 // Forces updating posts cache.
121 add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
122 return parent::init();
123 }
124
125 /**
126 * Deletes a translation of a post.
127 *
128 * @since 0.5
129 *
130 * @param int $id Post ID.
131 * @return void
132 */
133 public function delete_translation( $id ) {
134 $id = $this->sanitize_int_id( $id );
135
136 if ( empty( $id ) ) {
137 return;
138 }
139
140 parent::delete_translation( $id );
141 wp_set_object_terms( $id, array(), $this->tax_translations );
142 }
143
144 /**
145 * Returns object types (post types) that need to be translated.
146 * The post types list is cached for better performance.
147 * The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php.
148 *
149 * @since 3.4
150 *
151 * @param bool $filter True if we should return only valid registered object types.
152 * @return string[] Object type names for which Polylang manages languages.
153 *
154 * @phpstan-return array<non-empty-string, non-empty-string>
155 */
156 public function get_translated_object_types( $filter = true ) {
157 $post_types = $this->cache->get( 'post_types' );
158
159 if ( false === $post_types ) {
160 $post_types = array( 'post' => 'post', 'page' => 'page', 'wp_block' => 'wp_block' );
161
162 if ( ! empty( $this->options['post_types'] ) ) {
163 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
164 }
165
166 if ( empty( $this->options['media_support'] ) ) {
167 // In case the post type attachment is stored in the option.
168 unset( $post_types['attachment'] );
169 } else {
170 $post_types['attachment'] = 'attachment';
171 }
172
173 /**
174 * Filters the list of post types available for translation.
175 * The default are post types which have the parameter ‘public’ set to true.
176 * The filter must be added soon in the WordPress loading process:
177 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
178 *
179 * @since 0.8
180 *
181 * @param string[] $post_types List of post type names (as array keys and values).
182 * @param bool $is_settings True when displaying the list of custom post types in Polylang settings.
183 */
184 $post_types = (array) apply_filters( 'pll_get_post_types', $post_types, false );
185
186 if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) {
187 $this->cache->set( 'post_types', $post_types );
188 }
189 }
190
191 /** @var array<non-empty-string, non-empty-string> $post_types */
192 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
193 }
194
195 /**
196 * Returns true if Polylang manages languages for this object type.
197 *
198 * @since 3.4
199 *
200 * @param string|string[] $object_type Object type (post type) name or array of object type names.
201 * @return bool
202 */
203 public function is_translated_object_type( $object_type ) {
204 $post_types = $this->get_translated_object_types( false );
205 return ( is_array( $object_type ) && array_intersect( $object_type, $post_types ) ) || in_array( $object_type, $post_types ) || ( 'any' === $object_type && ! empty( $post_types ) );
206 }
207
208 /**
209 * Checks if registered post type must be translated.
210 *
211 * @since 1.2
212 *
213 * @param string $post_type Post type name.
214 * @return void
215 */
216 public function registered_post_type( $post_type ) {
217 if ( $this->is_translated_object_type( $post_type ) ) {
218 register_taxonomy_for_object_type( $this->tax_language, $post_type );
219 register_taxonomy_for_object_type( $this->tax_translations, $post_type );
220 }
221 }
222
223 /**
224 * Forces calling 'update_object_term_cache' when querying posts or pages.
225 * This is especially useful for nav menus with a lot of pages as, without doing this,
226 * we would have one query per page in the menu to get the page language for the permalink.
227 *
228 * @since 1.8
229 *
230 * @param WP_Query $query Reference to the query object.
231 * @return void
232 */
233 public function pre_get_posts( $query ) {
234 if ( ! empty( $query->query['post_type'] ) && $this->is_translated_object_type( $query->query['post_type'] ) ) {
235 $query->query_vars['update_post_term_cache'] = true;
236 }
237 }
238
239 /**
240 * Checks if the current user can read the post.
241 *
242 * @since 1.5
243 * @since 3.4 Renamed the parameter $post_id into $id.
244 *
245 * @param int $id Post ID
246 * @param string $context Optional, 'edit' or 'view'. Defaults to 'view'.
247 * @return bool
248 *
249 * @phpstan-param non-empty-string $context
250 */
251 public function current_user_can_read( $id, $context = 'view' ) {
252 $id = $this->sanitize_int_id( $id );
253
254 if ( empty( $id ) ) {
255 return false;
256 }
257
258 $post = get_post( $id );
259
260 if ( empty( $post ) ) {
261 return false;
262 }
263
264 if ( 'inherit' === $post->post_status && $post->post_parent ) {
265 $post = get_post( $post->post_parent );
266
267 if ( empty( $post ) ) {
268 return false;
269 }
270 }
271
272 if ( 'inherit' === $post->post_status || in_array( $post->post_status, get_post_stati( array( 'public' => true ) ) ) ) {
273 return true;
274 }
275
276 // Follow WP practices, which shows links to private posts ( when readable ), but not for draft posts ( ex: get_adjacent_post_link() )
277 if ( in_array( $post->post_status, get_post_stati( array( 'private' => true ) ) ) ) {
278 if ( ! is_user_logged_in() ) {
279 return false;
280 }
281
282 $user = wp_get_current_user();
283
284 if ( (int) $user->ID === (int) $post->post_author ) {
285 return true;
286 }
287
288 $post_type_object = get_post_type_object( $post->post_type );
289
290 return ! empty( $post_type_object ) && current_user_can( $post_type_object->cap->read_private_posts );
291 }
292
293 // In edit context, show draft and future posts.
294 if ( 'edit' === $context ) {
295 $states = get_post_stati(
296 array(
297 'protected' => true,
298 'show_in_admin_all_list' => true,
299 )
300 );
301
302 if ( in_array( $post->post_status, $states ) ) {
303 $user = wp_get_current_user();
304 return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || (int) $user->ID === (int) $post->post_author );
305 }
306 }
307
308 return false;
309 }
310
311 /**
312 * Creates a media translation
313 *
314 * @since 1.8
315 * @since 3.7 Moved from PLL_CRUD_Posts.
316 *
317 * @param int $post_id Original attachment id.
318 * @param string|PLL_Language $lang New translation language.
319 * @return int Attachment id of the translated media.
320 */
321 public function create_media_translation( $post_id, $lang ) {
322 if ( empty( $post_id ) ) {
323 return 0;
324 }
325
326 $post = get_post( $post_id, ARRAY_A );
327
328 if ( empty( $post ) ) {
329 return 0;
330 }
331
332 $lang = $this->languages->get( $lang ); // Make sure we get a valid language slug.
333
334 if ( empty( $lang ) ) {
335 return 0;
336 }
337
338 // Create a new attachment ( translate attachment parent if exists ).
339 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
340 unset( $post['ID'] ); // Will force the creation.
341 if ( ! empty( $post['post_parent'] ) ) {
342 $post['post_parent'] = (int) $this->get_translation( $post['post_parent'], $lang->slug );
343 }
344 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
345
346 // Loads the strings translations with the attachment's target language.
347 PLL()->load_strings_translations( $lang->slug );
348
349 $tr_id = wp_insert_attachment( wp_slash( $post ) );
350 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
351
352 // Copy metadata.
353 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
354 if ( is_array( $data ) ) {
355 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
356 }
357
358 // Copy attached file.
359 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
360 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
361 }
362
363 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
364 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
365 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
366 }
367
368 $this->set_language( $tr_id, $lang );
369
370 $translations = $this->get_translations( $post_id );
371 $translations[ $lang->slug ] = $tr_id;
372 $this->save_translations( $tr_id, $translations );
373
374 /**
375 * Fires after a media translation is created
376 *
377 * @since 1.6.4
378 *
379 * @param int $post_id Post id of the source media.
380 * @param int $tr_id Post id of the new media translation.
381 * @param string $slug Language code of the new translation.
382 */
383 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
384
385 // Restores the strings translations with the current language.
386 if ( PLL()->curlang instanceof PLL_Language ) {
387 PLL()->load_strings_translations( PLL()->curlang->slug );
388 }
389
390 return $tr_id;
391 }
392
393 /**
394 * Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in).
395 *
396 * @since 2.6
397 *
398 * @param string $type Post type.
399 * @param PLL_Language $untranslated_in The language the posts must not be translated in.
400 * @param PLL_Language $lang Language of the searched posts.
401 * @param string $search Limit the results to the posts matching this string.
402 * @return WP_Post[] Array of posts.
403 */
404 public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) {
405 global $wpdb;
406
407 $args = array( 'numberposts' => 20 ); // Limit to 20 posts by default.
408 /**
409 * Filters the query args when auto suggesting untranslated posts in the Languages metabox.
410 *
411 * @since 1.7
412 * @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts.
413 * No `WP_Query` is made anymore, a custom one is used instead.
414 *
415 * @param array $args WP_Query arguments
416 */
417 $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
418
419 $limit = $args['numberposts'];
420 $search_like = '%' . $wpdb->esc_like( $search ) . '%';
421 $untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%';
422 $posts = $wpdb->get_results(
423 $wpdb->prepare(
424 "SELECT * FROM {$wpdb->posts}
425 INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id)
426 AND tr1.term_taxonomy_id IN (%d)
427 AND (({$wpdb->posts}.post_title LIKE %s)
428 OR ({$wpdb->posts}.post_excerpt LIKE %s)
429 OR ({$wpdb->posts}.post_content LIKE %s)
430 )
431 AND {$wpdb->posts}.post_type = %s
432 AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft')
433 WHERE {$wpdb->posts}.ID NOT IN (
434 SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}
435 LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id)
436 INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id)
437 AND (tt.taxonomy = 'post_translations')
438 AND tt.description LIKE %s
439 )
440 LIMIT 0, %d",
441 $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ),
442 $search_like,
443 $search_like,
444 $search_like,
445 $type,
446 $untranslated_like,
447 $limit
448 )
449 );
450
451 foreach ( $posts as $i => $post ) {
452 if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) {
453 unset( $posts[ $i ] );
454 continue;
455 }
456
457 $posts[ $i ] = new WP_Post( $post );
458 }
459
460 return $posts;
461 }
462
463 /**
464 * Returns the description to use for the "language properties" in the REST API.
465 *
466 * @since 3.7
467 * @see WP_Syntex\Polylang\REST\V2\Languages::get_item_schema()
468 *
469 * @return string
470 */
471 public function get_rest_description(): string {
472 return __( 'Language taxonomy properties for post types.', 'polylang' );
473 }
474
475 /**
476 * Returns database-related information that can be used in some of this class methods.
477 * These are specific to the table containing the objects.
478 *
479 * @see PLL_Translatable_Object::join_clause()
480 * @see PLL_Translatable_Object::get_raw_objects_with_no_lang()
481 *
482 * @since 3.4.3
483 *
484 * @return string[] {
485 * @type string $table Name of the table.
486 * @type string $id_column Name of the column containing the object's ID.
487 * @type string $type_column Name of the column containing the object's type.
488 * @type string $default_alias Default alias corresponding to the object's table.
489 * }
490 * @phpstan-return DBInfoWithType
491 */
492 protected function get_db_infos() {
493 return array(
494 'table' => $GLOBALS['wpdb']->posts,
495 'id_column' => 'ID',
496 'type_column' => 'post_type',
497 'default_alias' => $GLOBALS['wpdb']->posts,
498 );
499 }
500
501 /**
502 * Wraps `wp_insert_post` with language feature.
503 *
504 * @since 3.7
505 *
506 * @param array $postarr {
507 * Optional. An array of elements that make up a post to insert.
508 * @See wp_insert_post() for accepted arguments.
509 *
510 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
511 * }
512 * @param PLL_Language $language The post language object.
513 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
514 */
515 public function insert( array $postarr, PLL_Language $language ) {
516 $post_id = wp_insert_post( $postarr, true );
517 if ( is_wp_error( $post_id ) ) {
518 // Something went wrong!
519 return $post_id;
520 }
521
522 $this->set_language( $post_id, $language );
523
524 if ( ! empty( $postarr['translations'] ) ) {
525 $this->save_translations( $post_id, $postarr['translations'] );
526 }
527
528 return $post_id;
529 }
530
531 /**
532 * Wraps `wp_update_post` with language feature.
533 *
534 * @since 3.7
535 *
536 * @param array $postarr {
537 * Optional. An array of elements that make up a post to update.
538 * @See wp_insert_post() for accepted arguments.
539 *
540 * @type PLL_Language|string $lang The post language object or slug.
541 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
542 * }
543 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
544 */
545 public function update( array $postarr ) {
546 if ( ! empty( $postarr['lang'] ) ) {
547 $post = get_post( $postarr['ID'] );
548 if ( ! $post instanceof WP_Post ) {
549 return new WP_Error( 'invalid_post', __( 'Invalid post ID.', 'polylang' ) );
550 }
551
552 $language = $this->languages->get( $postarr['lang'] );
553 if ( ! $language instanceof PLL_Language ) {
554 return new WP_Error( 'invalid_language', __( 'Please provide a valid language.', 'polylang' ) );
555 }
556
557 $this->set_language( $postarr['ID'], $language );
558 }
559
560 $post_id = wp_update_post( $postarr, true );
561 if ( is_wp_error( $post_id ) ) {
562 // Something went wrong!
563 return $post_id;
564 }
565
566 if ( ! empty( $postarr['translations'] ) ) {
567 $this->save_translations( $postarr['ID'], $postarr['translations'] );
568 }
569
570 return $post_id;
571 }
572 }
573