PluginProbe
Polylang / 3.7.4
Polylang v3.7.4
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 / translated-post.php

translated-post.php in Polylang 3.7.4, at include/translated-post.php

577 lines 17.3 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 * @phpstan-param non-empty-string|non-empty-string[] $object_type
204 */
205 public function is_translated_object_type( $object_type ) {
206 $post_types = $this->get_translated_object_types( false );
207 return ( is_array( $object_type ) && array_intersect( $object_type, $post_types ) ) || in_array( $object_type, $post_types ) || ( 'any' === $object_type && ! empty( $post_types ) );
208 }
209
210 /**
211 * Checks if registered post type must be translated.
212 *
213 * @since 1.2
214 *
215 * @param string $post_type Post type name.
216 * @return void
217 *
218 * @phpstan-param non-empty-string $post_type
219 */
220 public function registered_post_type( $post_type ) {
221 if ( $this->is_translated_object_type( $post_type ) ) {
222 register_taxonomy_for_object_type( $this->tax_language, $post_type );
223 register_taxonomy_for_object_type( $this->tax_translations, $post_type );
224 }
225 }
226
227 /**
228 * Forces calling 'update_object_term_cache' when querying posts or pages.
229 * This is especially useful for nav menus with a lot of pages as, without doing this,
230 * we would have one query per page in the menu to get the page language for the permalink.
231 *
232 * @since 1.8
233 *
234 * @param WP_Query $query Reference to the query object.
235 * @return void
236 */
237 public function pre_get_posts( $query ) {
238 if ( ! empty( $query->query['post_type'] ) && $this->is_translated_object_type( $query->query['post_type'] ) ) {
239 $query->query_vars['update_post_term_cache'] = true;
240 }
241 }
242
243 /**
244 * Checks if the current user can read the post.
245 *
246 * @since 1.5
247 * @since 3.4 Renamed the parameter $post_id into $id.
248 *
249 * @param int $id Post ID
250 * @param string $context Optional, 'edit' or 'view'. Defaults to 'view'.
251 * @return bool
252 *
253 * @phpstan-param non-empty-string $context
254 */
255 public function current_user_can_read( $id, $context = 'view' ) {
256 $id = $this->sanitize_int_id( $id );
257
258 if ( empty( $id ) ) {
259 return false;
260 }
261
262 $post = get_post( $id );
263
264 if ( empty( $post ) ) {
265 return false;
266 }
267
268 if ( 'inherit' === $post->post_status && $post->post_parent ) {
269 $post = get_post( $post->post_parent );
270
271 if ( empty( $post ) ) {
272 return false;
273 }
274 }
275
276 if ( 'inherit' === $post->post_status || in_array( $post->post_status, get_post_stati( array( 'public' => true ) ) ) ) {
277 return true;
278 }
279
280 // Follow WP practices, which shows links to private posts ( when readable ), but not for draft posts ( ex: get_adjacent_post_link() )
281 if ( in_array( $post->post_status, get_post_stati( array( 'private' => true ) ) ) ) {
282 if ( ! is_user_logged_in() ) {
283 return false;
284 }
285
286 $user = wp_get_current_user();
287
288 if ( (int) $user->ID === (int) $post->post_author ) {
289 return true;
290 }
291
292 $post_type_object = get_post_type_object( $post->post_type );
293
294 return ! empty( $post_type_object ) && current_user_can( $post_type_object->cap->read_private_posts );
295 }
296
297 // In edit context, show draft and future posts.
298 if ( 'edit' === $context ) {
299 $states = get_post_stati(
300 array(
301 'protected' => true,
302 'show_in_admin_all_list' => true,
303 )
304 );
305
306 if ( in_array( $post->post_status, $states ) ) {
307 $user = wp_get_current_user();
308 return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || (int) $user->ID === (int) $post->post_author );
309 }
310 }
311
312 return false;
313 }
314
315 /**
316 * Creates a media translation
317 *
318 * @since 1.8
319 * @since 3.7 Moved from PLL_CRUD_Posts.
320 *
321 * @param int $post_id Original attachment id.
322 * @param string|PLL_Language $lang New translation language.
323 * @return int Attachment id of the translated media.
324 */
325 public function create_media_translation( $post_id, $lang ) {
326 if ( empty( $post_id ) ) {
327 return 0;
328 }
329
330 $post = get_post( $post_id, ARRAY_A );
331
332 if ( empty( $post ) ) {
333 return 0;
334 }
335
336 $lang = $this->languages->get( $lang ); // Make sure we get a valid language slug.
337
338 if ( empty( $lang ) ) {
339 return 0;
340 }
341
342 // Create a new attachment ( translate attachment parent if exists ).
343 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
344 unset( $post['ID'] ); // Will force the creation.
345 if ( ! empty( $post['post_parent'] ) ) {
346 $post['post_parent'] = (int) $this->get_translation( $post['post_parent'], $lang->slug );
347 }
348 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
349
350 // Loads the strings translations with the attachment's target language.
351 PLL()->load_strings_translations( $lang->slug );
352
353 $tr_id = wp_insert_attachment( wp_slash( $post ) );
354 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
355
356 // Copy metadata.
357 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
358 if ( is_array( $data ) ) {
359 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
360 }
361
362 // Copy attached file.
363 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
364 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
365 }
366
367 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
368 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
369 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
370 }
371
372 $this->set_language( $tr_id, $lang );
373
374 $translations = $this->get_translations( $post_id );
375 $translations[ $lang->slug ] = $tr_id;
376 $this->save_translations( $tr_id, $translations );
377
378 /**
379 * Fires after a media translation is created
380 *
381 * @since 1.6.4
382 *
383 * @param int $post_id Post id of the source media.
384 * @param int $tr_id Post id of the new media translation.
385 * @param string $slug Language code of the new translation.
386 */
387 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
388
389 // Restores the strings translations with the current language.
390 if ( PLL()->curlang instanceof PLL_Language ) {
391 PLL()->load_strings_translations( PLL()->curlang->slug );
392 }
393
394 return $tr_id;
395 }
396
397 /**
398 * Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in).
399 *
400 * @since 2.6
401 *
402 * @param string $type Post type.
403 * @param PLL_Language $untranslated_in The language the posts must not be translated in.
404 * @param PLL_Language $lang Language of the searched posts.
405 * @param string $search Limit the results to the posts matching this string.
406 * @return WP_Post[] Array of posts.
407 */
408 public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) {
409 global $wpdb;
410
411 $args = array( 'numberposts' => 20 ); // Limit to 20 posts by default.
412 /**
413 * Filters the query args when auto suggesting untranslated posts in the Languages metabox.
414 *
415 * @since 1.7
416 * @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts.
417 * No `WP_Query` is made anymore, a custom one is used instead.
418 *
419 * @param array $args WP_Query arguments
420 */
421 $args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
422
423 $limit = $args['numberposts'];
424 $search_like = '%' . $wpdb->esc_like( $search ) . '%';
425 $untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%';
426 $posts = $wpdb->get_results(
427 $wpdb->prepare(
428 "SELECT * FROM {$wpdb->posts}
429 INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id)
430 AND tr1.term_taxonomy_id IN (%d)
431 AND (({$wpdb->posts}.post_title LIKE %s)
432 OR ({$wpdb->posts}.post_excerpt LIKE %s)
433 OR ({$wpdb->posts}.post_content LIKE %s)
434 )
435 AND {$wpdb->posts}.post_type = %s
436 AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft')
437 WHERE {$wpdb->posts}.ID NOT IN (
438 SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}
439 LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id)
440 INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id)
441 AND (tt.taxonomy = 'post_translations')
442 AND tt.description LIKE %s
443 )
444 LIMIT 0, %d",
445 $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ),
446 $search_like,
447 $search_like,
448 $search_like,
449 $type,
450 $untranslated_like,
451 $limit
452 )
453 );
454
455 foreach ( $posts as $i => $post ) {
456 if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) {
457 unset( $posts[ $i ] );
458 continue;
459 }
460
461 $posts[ $i ] = new WP_Post( $post );
462 }
463
464 return $posts;
465 }
466
467 /**
468 * Returns the description to use for the "language properties" in the REST API.
469 *
470 * @since 3.7
471 * @see WP_Syntex\Polylang\REST\V2\Languages::get_item_schema()
472 *
473 * @return string
474 */
475 public function get_rest_description(): string {
476 return __( 'Language taxonomy properties for post types.', 'polylang' );
477 }
478
479 /**
480 * Returns database-related information that can be used in some of this class methods.
481 * These are specific to the table containing the objects.
482 *
483 * @see PLL_Translatable_Object::join_clause()
484 * @see PLL_Translatable_Object::get_raw_objects_with_no_lang()
485 *
486 * @since 3.4.3
487 *
488 * @return string[] {
489 * @type string $table Name of the table.
490 * @type string $id_column Name of the column containing the object's ID.
491 * @type string $type_column Name of the column containing the object's type.
492 * @type string $default_alias Default alias corresponding to the object's table.
493 * }
494 * @phpstan-return DBInfoWithType
495 */
496 protected function get_db_infos() {
497 return array(
498 'table' => $GLOBALS['wpdb']->posts,
499 'id_column' => 'ID',
500 'type_column' => 'post_type',
501 'default_alias' => $GLOBALS['wpdb']->posts,
502 );
503 }
504
505 /**
506 * Wraps `wp_insert_post` with language feature.
507 *
508 * @since 3.7
509 *
510 * @param array $postarr {
511 * Optional. An array of elements that make up a post to insert.
512 * @See wp_insert_post() for accepted arguments.
513 *
514 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
515 * }
516 * @param PLL_Language $language The post language object.
517 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
518 */
519 public function insert( array $postarr, PLL_Language $language ) {
520 $post_id = wp_insert_post( $postarr, true );
521 if ( is_wp_error( $post_id ) ) {
522 // Something went wrong!
523 return $post_id;
524 }
525
526 $this->set_language( $post_id, $language );
527
528 if ( ! empty( $postarr['translations'] ) ) {
529 $this->save_translations( $post_id, $postarr['translations'] );
530 }
531
532 return $post_id;
533 }
534
535 /**
536 * Wraps `wp_update_post` with language feature.
537 *
538 * @since 3.7
539 *
540 * @param array $postarr {
541 * Optional. An array of elements that make up a post to update.
542 * @See wp_insert_post() for accepted arguments.
543 *
544 * @type PLL_Language|string $lang The post language object or slug.
545 * @type string[] $translations The translation group to assign to the post with language slug as keys and post ID as values.
546 * }
547 * @return int|WP_Error The post ID on success. The value `WP_Error` on failure.
548 */
549 public function update( array $postarr ) {
550 if ( ! empty( $postarr['lang'] ) ) {
551 $post = get_post( $postarr['ID'] );
552 if ( ! $post instanceof WP_Post ) {
553 return new WP_Error( 'invalid_post', __( 'Invalid post ID.', 'polylang' ) );
554 }
555
556 $language = $this->languages->get( $postarr['lang'] );
557 if ( ! $language instanceof PLL_Language ) {
558 return new WP_Error( 'invalid_language', __( 'Please provide a valid language.', 'polylang' ) );
559 }
560
561 $this->set_language( $postarr['ID'], $language );
562 }
563
564 $post_id = wp_update_post( $postarr, true );
565 if ( is_wp_error( $post_id ) ) {
566 // Something went wrong!
567 return $post_id;
568 }
569
570 if ( ! empty( $postarr['translations'] ) ) {
571 $this->save_translations( $postarr['ID'], $postarr['translations'] );
572 }
573
574 return $post_id;
575 }
576 }
577