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
polylang / include / crud-posts.php

crud-posts.php in Polylang 3.6.7, at include/crud-posts.php

486 lines 15.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 /**
7 * Adds actions and filters related to languages when creating, updating or deleting posts.
8 * Actions and filters triggered when reading posts are handled separately.
9 *
10 * @since 2.4
11 */
12 class PLL_CRUD_Posts {
13 /**
14 * @var PLL_Model
15 */
16 protected $model;
17
18 /**
19 * Preferred language to assign to a new post.
20 *
21 * @var PLL_Language|null
22 */
23 protected $pref_lang;
24
25 /**
26 * Current language.
27 *
28 * @var PLL_Language|null
29 */
30 protected $curlang;
31
32 /**
33 * Reference to the Polylang options array.
34 *
35 * @var array
36 */
37 protected $options;
38
39 /**
40 * Constructor
41 *
42 * @since 2.4
43 *
44 * @param object $polylang The Polylang object.
45 */
46 public function __construct( &$polylang ) {
47 $this->options = &$polylang->options;
48 $this->model = &$polylang->model;
49 $this->pref_lang = &$polylang->pref_lang;
50 $this->curlang = &$polylang->curlang;
51
52 add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
53 add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 );
54 add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 2 );
55 add_action( 'before_delete_post', array( $this, 'delete_post' ) );
56 add_action( 'post_updated', array( $this, 'force_tags_translation' ), 10, 3 );
57
58 // Specific for media
59 if ( $polylang->options['media_support'] ) {
60 add_action( 'add_attachment', array( $this, 'set_default_language' ) );
61 add_action( 'delete_attachment', array( $this, 'delete_post' ) );
62 add_filter( 'wp_delete_file', array( $this, 'wp_delete_file' ) );
63 }
64 }
65
66 /**
67 * Allows to set a language by default for posts if it has no language yet.
68 *
69 * @since 1.5
70 *
71 * @param int $post_id Post ID.
72 * @return void
73 */
74 public function set_default_language( $post_id ) {
75 if ( ! $this->model->post->get_language( $post_id ) ) {
76 if ( ! empty( $_GET['new_lang'] ) && $lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
77 // Defined only on admin.
78 $this->model->post->set_language( $post_id, $lang );
79 } elseif ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
80 // Testing $this->pref_lang makes this test pass only on admin.
81 $this->model->post->set_language( $post_id, $lang );
82 } elseif ( ( $parent_id = wp_get_post_parent_id( $post_id ) ) && $parent_lang = $this->model->post->get_language( $parent_id ) ) {
83 $this->model->post->set_language( $post_id, $parent_lang );
84 } elseif ( isset( $this->pref_lang ) ) {
85 // Always defined on admin, never defined on frontend.
86 $this->model->post->set_language( $post_id, $this->pref_lang );
87 } elseif ( ! empty( $this->curlang ) ) {
88 // Only on frontend due to the previous test always true on admin.
89 $this->model->post->set_language( $post_id, $this->curlang );
90 } else {
91 // In all other cases set to default language.
92 $this->model->post->set_language( $post_id, $this->options['default_lang'] );
93 }
94 }
95 }
96
97 /**
98 * Called when a post ( or page ) is saved, published or updated.
99 *
100 * @since 0.1
101 * @since 2.3 Does not save the language and translations anymore, unless the post has no language yet.
102 *
103 * @param int $post_id Post id of the post being saved.
104 * @param WP_Post $post The post being saved.
105 * @return void
106 */
107 public function save_post( $post_id, $post ) {
108 // Does nothing except on post types which are filterable.
109 if ( $this->model->is_translated_post_type( $post->post_type ) ) {
110 if ( $id = wp_is_post_revision( $post_id ) ) {
111 $post_id = $id;
112 }
113
114 $lang = $this->model->post->get_language( $post_id );
115
116 if ( empty( $lang ) ) {
117 $this->set_default_language( $post_id );
118 }
119
120 /**
121 * Fires after the post language and translations are saved.
122 *
123 * @since 1.2
124 *
125 * @param int $post_id Post id.
126 * @param WP_Post $post Post object.
127 * @param int[] $translations The list of translations post ids.
128 */
129 do_action( 'pll_save_post', $post_id, $post, $this->model->post->get_translations( $post_id ) );
130 }
131 }
132
133 /**
134 * Makes sure that saved terms are in the right language.
135 *
136 * @since 2.3
137 *
138 * @param int $object_id Object ID.
139 * @param int[]|string[] $terms An array of object term IDs or slugs.
140 * @param int[] $tt_ids An array of term taxonomy IDs.
141 * @param string $taxonomy Taxonomy slug.
142 * @return void
143 */
144 public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
145 static $avoid_recursion;
146
147 if ( $avoid_recursion || empty( $terms ) || ! is_array( $terms ) || empty( $tt_ids )
148 || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
149 return;
150 }
151
152 $lang = $this->model->post->get_language( $object_id );
153
154 if ( empty( $lang ) ) {
155 return;
156 }
157
158 // Use the term_taxonomy_ids to get all the requested terms in 1 query.
159 $new_terms = get_terms(
160 array(
161 'taxonomy' => $taxonomy,
162 'term_taxonomy_id' => array_map( 'intval', $tt_ids ),
163 'lang' => '',
164 )
165 );
166
167 if ( empty( $new_terms ) || ! is_array( $new_terms ) ) {
168 // Terms not found.
169 return;
170 }
171
172 $new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang );
173
174 // Query the object's term.
175 $orig_terms = get_terms(
176 array(
177 'taxonomy' => $taxonomy,
178 'object_ids' => $object_id,
179 'lang' => '',
180 )
181 );
182
183 if ( is_array( $orig_terms ) ) {
184 $orig_term_ids = wp_list_pluck( $orig_terms, 'term_id' );
185 $orig_term_ids_translated = $this->translate_terms( $orig_terms, $taxonomy, $lang );
186
187 // Terms that are not in the translated list.
188 $remove_term_ids = array_diff( $orig_term_ids, $orig_term_ids_translated );
189
190 if ( ! empty( $remove_term_ids ) ) {
191 wp_remove_object_terms( $object_id, $remove_term_ids, $taxonomy );
192 }
193 } else {
194 $orig_term_ids = array();
195 $orig_term_ids_translated = array();
196 }
197
198 // Terms to add.
199 $add_term_ids = array_unique( array_merge( $orig_term_ids_translated, $new_term_ids_translated ) );
200 $add_term_ids = array_diff( $add_term_ids, $orig_term_ids );
201
202 if ( ! empty( $add_term_ids ) ) {
203 $avoid_recursion = true;
204 wp_set_object_terms( $object_id, $add_term_ids, $taxonomy, true ); // Append.
205 $avoid_recursion = false;
206 }
207 }
208
209 /**
210 * Make sure that the post parent is in the correct language.
211 *
212 * @since 1.8
213 *
214 * @param int $post_parent Post parent ID.
215 * @param int $post_id Post ID.
216 * @return int
217 */
218 public function wp_insert_post_parent( $post_parent, $post_id ) {
219 $lang = $this->model->post->get_language( $post_id );
220 $parent_post_type = $post_parent > 0 ? get_post_type( $post_parent ) : null;
221 // Dont break the hierarchy in case the post has no language
222 if ( ! empty( $lang ) && ! empty( $parent_post_type ) && $this->model->is_translated_post_type( $parent_post_type ) ) {
223 $post_parent = $this->model->post->get_translation( $post_parent, $lang );
224 }
225
226 return $post_parent;
227 }
228
229 /**
230 * Called when a post, page or media is deleted
231 * Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug
232 * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
233 *
234 * @since 0.1
235 *
236 * @param int $post_id Post ID.
237 * @return void
238 */
239 public function delete_post( $post_id ) {
240 if ( ! wp_is_post_revision( $post_id ) ) {
241 $this->model->post->delete_translation( $post_id );
242 }
243 }
244
245 /**
246 * Prevents WP deleting files when there are still media using them.
247 *
248 * @since 0.9
249 *
250 * @param string $file Path to the file to delete.
251 * @return string Empty or unmodified path.
252 */
253 public function wp_delete_file( $file ) {
254 global $wpdb;
255
256 $uploadpath = wp_upload_dir();
257
258 // Get the main attached file.
259 $attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) );
260 $attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file );
261
262 $ids = $wpdb->get_col(
263 $wpdb->prepare(
264 "SELECT post_id FROM $wpdb->postmeta
265 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
266 $attached_file
267 )
268 );
269
270 if ( ! empty( $ids ) ) {
271 return ''; // Prevent deleting the file.
272 }
273
274 return $file;
275 }
276
277 /**
278 * Creates a media translation
279 *
280 * @since 1.8
281 *
282 * @param int $post_id Original attachment id.
283 * @param string|object $lang New translation language.
284 * @return int Attachment id of the translated media.
285 */
286 public function create_media_translation( $post_id, $lang ) {
287 if ( empty( $post_id ) ) {
288 return 0;
289 }
290
291 $post = get_post( $post_id, ARRAY_A );
292
293 if ( empty( $post ) ) {
294 return 0;
295 }
296
297 $lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug.
298
299 if ( empty( $lang ) ) {
300 return 0;
301 }
302
303 // Create a new attachment ( translate attachment parent if exists ).
304 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
305 unset( $post['ID'] ); // Will force the creation.
306 if ( ! empty( $post['post_parent'] ) ) {
307 $post['post_parent'] = (int) $this->model->post->get_translation( $post['post_parent'], $lang->slug );
308 }
309 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
310 $tr_id = wp_insert_attachment( wp_slash( $post ) );
311 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
312
313 // Copy metadata.
314 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
315 if ( is_array( $data ) ) {
316 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
317 }
318
319 // Copy attached file.
320 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
321 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
322 }
323
324 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
325 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
326 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
327 }
328
329 $this->model->post->set_language( $tr_id, $lang );
330
331 $translations = $this->model->post->get_translations( $post_id );
332 $translations[ $lang->slug ] = $tr_id;
333 $this->model->post->save_translations( $tr_id, $translations );
334
335 /**
336 * Fires after a media translation is created
337 *
338 * @since 1.6.4
339 *
340 * @param int $post_id Post id of the source media.
341 * @param int $tr_id Post id of the new media translation.
342 * @param string $slug Language code of the new translation.
343 */
344 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
345 return $tr_id;
346 }
347
348 /**
349 * Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`.
350 *
351 * @since 3.4.5
352 *
353 * @param int $post_id Post ID, unused.
354 * @param WP_Post $post_after Post object following the update.
355 * @param WP_Post $post_before Post object before the update.
356 * @return void
357 */
358 public function force_tags_translation( $post_id, $post_after, $post_before ) {
359 if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) {
360 return;
361 }
362
363 $terms = get_the_terms( $post_before, 'post_tag' );
364
365 if ( empty( $terms ) || ! is_array( $terms ) ) {
366 return;
367 }
368
369 $term_ids = wp_list_pluck( $terms, 'term_id' );
370
371 // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
372 wp_set_post_terms( $post_id, $term_ids, 'post_tag' );
373 }
374
375 /**
376 * Makes sure that all terms in the given list are in the given language.
377 * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
378 *
379 * @since 3.5
380 *
381 * @param WP_Term[] $terms List of terms to translate.
382 * @param string $taxonomy The terms' taxonomy.
383 * @param PLL_Language $language The language to translate the terms into.
384 * @return int[] List of `term_id`s.
385 *
386 * @phpstan-return array<positive-int>
387 */
388 private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
389 $term_ids_translated = array();
390
391 foreach ( $terms as $term ) {
392 $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
393 }
394
395 return array_filter( $term_ids_translated );
396 }
397
398 /**
399 * Translates the given term into the given language.
400 * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
401 *
402 * @since 3.5
403 *
404 * @param WP_Term $term The term to translate.
405 * @param string $taxonomy The term's taxonomy.
406 * @param PLL_Language $language The language to translate the term into.
407 * @return int A `term_id` on success, `0` on failure.
408 *
409 * @phpstan-return int<0, max>
410 */
411 private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
412 // Check if the term is in the correct language or if a translation exists.
413 $tr_term_id = $this->model->term->get( $term->term_id, $language );
414
415 if ( ! empty( $tr_term_id ) ) {
416 // Already in the correct language.
417 return $tr_term_id;
418 }
419
420 // Or choose the correct language for tags (initially defined by name).
421 $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
422
423 if ( ! empty( $tr_term_id ) ) {
424 return $tr_term_id;
425 }
426
427 // Or create the term in the correct language.
428 $tr_parent_term_id = 0;
429
430 if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
431 $parent = get_term( $term->parent, $taxonomy );
432
433 if ( $parent instanceof WP_Term ) {
434 // Translate the parent recursively.
435 $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
436 }
437 }
438
439 $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
440 if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
441 return $language;
442 }
443 return $lang;
444 };
445 $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
446 if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
447 return $tr_parent_term_id;
448 }
449 return $parent_id;
450 };
451 add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
452 add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
453 $new_term_info = wp_insert_term(
454 $term->name,
455 $taxonomy,
456 array(
457 'parent' => $tr_parent_term_id,
458 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
459 )
460 );
461 remove_filter( 'pll_inserted_term_language', $lang_callback );
462 remove_filter( 'pll_inserted_term_parent', $parent_callback );
463
464 if ( is_wp_error( $new_term_info ) ) {
465 // Term creation failed.
466 return 0;
467 }
468
469 $tr_term_id = max( 0, (int) $new_term_info['term_id'] );
470
471 if ( empty( $tr_term_id ) ) {
472 return 0;
473 }
474
475 $this->model->term->set_language( $tr_term_id, $language );
476
477 $trs = $this->model->term->get_translations( $term->term_id );
478
479 $trs[ $language->slug ] = $tr_term_id;
480
481 $this->model->term->save_translations( $term->term_id, $trs );
482
483 return $tr_term_id;
484 }
485 }
486