PluginProbe
Polylang / 3.8.10
Polylang v3.8.10
3.8.10 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 All 234 releases
polylang / src / translated-post.php

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

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