PluginProbe
Polylang / trunk
Polylang vtrunk
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 trunk, at src/translated-post.php

572 lines 17.1 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 = pll_sanitize_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 = pll_sanitize_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 return current_user_can( 'read_post', $post->ID );
304 }
305 }
306
307 return false;
308 }
309
310 /**
311 * Creates a media translation
312 *
313 * @since 1.8
314 * @since 3.7 Moved from PLL_CRUD_Posts.
315 *
316 * @param int $post_id Original attachment id.
317 * @param string|PLL_Language $lang New translation language.
318 * @return int Attachment id of the translated media.
319 */
320 public function create_media_translation( $post_id, $lang ) {
321 if ( empty( $post_id ) ) {
322 return 0;
323 }
324
325 $post = get_post( $post_id, ARRAY_A );
326
327 if ( empty( $post ) ) {
328 return 0;
329 }
330
331 $lang = $this->languages->get( $lang ); // Make sure we get a valid language slug.
332
333 if ( empty( $lang ) ) {
334 return 0;
335 }
336
337 // Create a new attachment ( translate attachment parent if exists ).
338 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
339 unset( $post['ID'] ); // Will force the creation.
340 if ( ! empty( $post['post_parent'] ) ) {
341 $post['post_parent'] = (int) $this->get_translation( $post['post_parent'], $lang->slug );
342 }
343 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
344
345 // Loads the strings translations with the attachment's target language.
346 PLL()->load_strings_translations( $lang->slug );
347
348 $tr_id = wp_insert_attachment( wp_slash( $post ) );
349 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
350
351 // Copy metadata.
352 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
353 if ( is_array( $data ) ) {
354 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
355 }
356
357 // Copy attached file.
358 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
359 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
360 }
361
362 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
363 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
364 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
365 }
366
367 $this->set_language( $tr_id, $lang );
368
369 $translations = $this->get_translations( $post_id );
370 $translations[ $lang->slug ] = $tr_id;
371 $this->save_translations( $tr_id, $translations );
372
373 /**
374 * Fires after a media translation is created
375 *
376 * @since 1.6.4
377 *
378 * @param int $post_id Post id of the source media.
379 * @param int $tr_id Post id of the new media translation.
380 * @param string $slug Language code of the new translation.
381 */
382 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
383
384 // Restores the strings translations with the current language.
385 if ( PLL()->curlang instanceof PLL_Language ) {
386 PLL()->load_strings_translations( PLL()->curlang->slug );
387 }
388
389 return $tr_id;
390 }
391
392 /**
393 * Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in).
394 *
395 * @since 2.6
396 *
397 * @param string $type Post type.
398 * @param PLL_Language $untranslated_in The language the posts must not be translated in.
399 * @param PLL_Language $lang Language of the searched posts.
400 * @param string $search Limit the results to the posts matching this string.
401 * @return WP_Post[] Array of posts.
402 */
403 public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) {
404 global $wpdb;
405
406 $args = array( 'numberposts' => 20 ); // Limit to 20 posts by default.
407 /**
408 * Filters the query args when auto suggesting untranslated posts in the Languages metabox.
409 *
410 * @since 1.7
411 * @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts.
412 * No `WP_Query` is made anymore, a custom one is used instead.
413 *
414 * @param array $args WP_Query arguments
415 */
416 $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
417
418 $limit = $args['numberposts'];
419 $search_like = '%' . $wpdb->esc_like( $search ) . '%';
420 $untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%';
421 $posts = $wpdb->get_results(
422 $wpdb->prepare(
423 "SELECT * FROM {$wpdb->posts}
424 INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id)
425 AND tr1.term_taxonomy_id IN (%d)
426 AND (({$wpdb->posts}.post_title LIKE %s)
427 OR ({$wpdb->posts}.post_excerpt LIKE %s)
428 OR ({$wpdb->posts}.post_content LIKE %s)
429 )
430 AND {$wpdb->posts}.post_type = %s
431 AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft')
432 WHERE {$wpdb->posts}.ID NOT IN (
433 SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}
434 LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id)
435 INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id)
436 AND (tt.taxonomy = 'post_translations')
437 AND tt.description LIKE %s
438 )
439 LIMIT 0, %d",
440 $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ),
441 $search_like,
442 $search_like,
443 $search_like,
444 $type,
445 $untranslated_like,
446 $limit
447 )
448 );
449
450 foreach ( $posts as $i => $post ) {
451 if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) {
452 unset( $posts[ $i ] );
453 continue;
454 }
455
456 $posts[ $i ] = new WP_Post( $post );
457 }
458
459 return $posts;
460 }
461
462 /**
463 * Returns the description to use for the "language properties" in the REST API.
464 *
465 * @since 3.7
466 * @see WP_Syntex\Polylang\REST\V2\Languages::get_item_schema()
467 *
468 * @return string
469 */
470 public function get_rest_description(): string {
471 return __( 'Language taxonomy properties for post types.', 'polylang' );
472 }
473
474 /**
475 * Returns database-related information that can be used in some of this class methods.
476 * These are specific to the table containing the objects.
477 *
478 * @see PLL_Translatable_Object::join_clause()
479 * @see PLL_Translatable_Object::get_raw_objects_with_no_lang()
480 *
481 * @since 3.4.3
482 *
483 * @return string[] {
484 * @type string $table Name of the table.
485 * @type string $id_column Name of the column containing the object's ID.
486 * @type string $type_column Name of the column containing the object's type.
487 * @type string $default_alias Default alias corresponding to the object's table.
488 * }
489 * @phpstan-return DBInfoWithType
490 */
491 protected function get_db_infos() {
492 return array(
493 'table' => $GLOBALS['wpdb']->posts,
494 'id_column' => 'ID',
495 'type_column' => 'post_type',
496 'default_alias' => $GLOBALS['wpdb']->posts,
497 );
498 }
499
500 /**
501 * Wraps `wp_insert_post` with language feature.
502 *
503 * @since 3.7
504 *
505 * @param array $postarr {
506 * Optional. An array of elements that make up a post to insert.
507 * @See wp_insert_post() for accepted arguments.
508 *
509 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
510 * }
511 * @param PLL_Language $language The post language object.
512 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
513 */
514 public function insert( array $postarr, PLL_Language $language ) {
515 $post_id = wp_insert_post( $postarr, true );
516 if ( is_wp_error( $post_id ) ) {
517 // Something went wrong!
518 return $post_id;
519 }
520
521 $this->set_language( $post_id, $language );
522
523 if ( ! empty( $postarr['translations'] ) ) {
524 $this->save_translations( $post_id, $postarr['translations'] );
525 }
526
527 return $post_id;
528 }
529
530 /**
531 * Wraps `wp_update_post` with language feature.
532 *
533 * @since 3.7
534 *
535 * @param array $postarr {
536 * Optional. An array of elements that make up a post to update.
537 * @See wp_insert_post() for accepted arguments.
538 *
539 * @type PLL_Language|string $lang The post language object or slug.
540 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
541 * }
542 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
543 */
544 public function update( array $postarr ) {
545 if ( ! empty( $postarr['lang'] ) ) {
546 $post = get_post( $postarr['ID'] );
547 if ( ! $post instanceof WP_Post ) {
548 return new WP_Error( 'invalid_post', __( 'Invalid post ID.', 'polylang' ) );
549 }
550
551 $language = $this->languages->get( $postarr['lang'] );
552 if ( ! $language instanceof PLL_Language ) {
553 return new WP_Error( 'invalid_language', __( 'Please provide a valid language.', 'polylang' ) );
554 }
555
556 $this->set_language( $postarr['ID'], $language );
557 }
558
559 $post_id = wp_update_post( $postarr, true );
560 if ( is_wp_error( $post_id ) ) {
561 // Something went wrong!
562 return $post_id;
563 }
564
565 if ( ! empty( $postarr['translations'] ) ) {
566 $this->save_translations( $postarr['ID'], $postarr['translations'] );
567 }
568
569 return $post_id;
570 }
571 }
572