PluginProbe
Polylang / 3.6
Polylang v3.6
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, at include/crud-posts.php

485 lines 15.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 /**
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 ) || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
148 return;
149 }
150
151 $lang = $this->model->post->get_language( $object_id );
152
153 if ( empty( $lang ) ) {
154 return;
155 }
156
157 // Use the term_taxonomy_ids to get all the requested terms in 1 query.
158 $new_terms = get_terms(
159 array(
160 'taxonomy' => $taxonomy,
161 'term_taxonomy_id' => array_map( 'intval', $tt_ids ),
162 'lang' => '',
163 )
164 );
165
166 if ( empty( $new_terms ) || ! is_array( $new_terms ) ) {
167 // Terms not found.
168 return;
169 }
170
171 $new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang );
172
173 // Query the object's term.
174 $orig_terms = get_terms(
175 array(
176 'taxonomy' => $taxonomy,
177 'object_ids' => $object_id,
178 'lang' => '',
179 )
180 );
181
182 if ( is_array( $orig_terms ) ) {
183 $orig_term_ids = wp_list_pluck( $orig_terms, 'term_id' );
184 $orig_term_ids_translated = $this->translate_terms( $orig_terms, $taxonomy, $lang );
185
186 // Terms that are not in the translated list.
187 $remove_term_ids = array_diff( $orig_term_ids, $orig_term_ids_translated );
188
189 if ( ! empty( $remove_term_ids ) ) {
190 wp_remove_object_terms( $object_id, $remove_term_ids, $taxonomy );
191 }
192 } else {
193 $orig_term_ids = array();
194 $orig_term_ids_translated = array();
195 }
196
197 // Terms to add.
198 $add_term_ids = array_unique( array_merge( $orig_term_ids_translated, $new_term_ids_translated ) );
199 $add_term_ids = array_diff( $add_term_ids, $orig_term_ids );
200
201 if ( ! empty( $add_term_ids ) ) {
202 $avoid_recursion = true;
203 wp_set_object_terms( $object_id, $add_term_ids, $taxonomy, true ); // Append.
204 $avoid_recursion = false;
205 }
206 }
207
208 /**
209 * Make sure that the post parent is in the correct language.
210 *
211 * @since 1.8
212 *
213 * @param int $post_parent Post parent ID.
214 * @param int $post_id Post ID.
215 * @return int
216 */
217 public function wp_insert_post_parent( $post_parent, $post_id ) {
218 $lang = $this->model->post->get_language( $post_id );
219 $parent_post_type = $post_parent > 0 ? get_post_type( $post_parent ) : null;
220 // Dont break the hierarchy in case the post has no language
221 if ( ! empty( $lang ) && ! empty( $parent_post_type ) && $this->model->is_translated_post_type( $parent_post_type ) ) {
222 $post_parent = $this->model->post->get_translation( $post_parent, $lang );
223 }
224
225 return $post_parent;
226 }
227
228 /**
229 * Called when a post, page or media is deleted
230 * Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug
231 * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
232 *
233 * @since 0.1
234 *
235 * @param int $post_id Post ID.
236 * @return void
237 */
238 public function delete_post( $post_id ) {
239 if ( ! wp_is_post_revision( $post_id ) ) {
240 $this->model->post->delete_translation( $post_id );
241 }
242 }
243
244 /**
245 * Prevents WP deleting files when there are still media using them.
246 *
247 * @since 0.9
248 *
249 * @param string $file Path to the file to delete.
250 * @return string Empty or unmodified path.
251 */
252 public function wp_delete_file( $file ) {
253 global $wpdb;
254
255 $uploadpath = wp_upload_dir();
256
257 // Get the main attached file.
258 $attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) );
259 $attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file );
260
261 $ids = $wpdb->get_col(
262 $wpdb->prepare(
263 "SELECT post_id FROM $wpdb->postmeta
264 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
265 $attached_file
266 )
267 );
268
269 if ( ! empty( $ids ) ) {
270 return ''; // Prevent deleting the file.
271 }
272
273 return $file;
274 }
275
276 /**
277 * Creates a media translation
278 *
279 * @since 1.8
280 *
281 * @param int $post_id Original attachment id.
282 * @param string|object $lang New translation language.
283 * @return int Attachment id of the translated media.
284 */
285 public function create_media_translation( $post_id, $lang ) {
286 if ( empty( $post_id ) ) {
287 return 0;
288 }
289
290 $post = get_post( $post_id, ARRAY_A );
291
292 if ( empty( $post ) ) {
293 return 0;
294 }
295
296 $lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug.
297
298 if ( empty( $lang ) ) {
299 return 0;
300 }
301
302 // Create a new attachment ( translate attachment parent if exists ).
303 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
304 unset( $post['ID'] ); // Will force the creation.
305 if ( ! empty( $post['post_parent'] ) ) {
306 $post['post_parent'] = (int) $this->model->post->get_translation( $post['post_parent'], $lang->slug );
307 }
308 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
309 $tr_id = wp_insert_attachment( wp_slash( $post ) );
310 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
311
312 // Copy metadata.
313 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
314 if ( is_array( $data ) ) {
315 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
316 }
317
318 // Copy attached file.
319 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
320 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
321 }
322
323 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
324 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
325 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
326 }
327
328 $this->model->post->set_language( $tr_id, $lang );
329
330 $translations = $this->model->post->get_translations( $post_id );
331 $translations[ $lang->slug ] = $tr_id;
332 $this->model->post->save_translations( $tr_id, $translations );
333
334 /**
335 * Fires after a media translation is created
336 *
337 * @since 1.6.4
338 *
339 * @param int $post_id Post id of the source media.
340 * @param int $tr_id Post id of the new media translation.
341 * @param string $slug Language code of the new translation.
342 */
343 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
344 return $tr_id;
345 }
346
347 /**
348 * Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`.
349 *
350 * @since 3.4.5
351 *
352 * @param int $post_id Post ID, unused.
353 * @param WP_Post $post_after Post object following the update.
354 * @param WP_Post $post_before Post object before the update.
355 * @return void
356 */
357 public function force_tags_translation( $post_id, $post_after, $post_before ) {
358 if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) {
359 return;
360 }
361
362 $terms = get_the_terms( $post_before, 'post_tag' );
363
364 if ( empty( $terms ) || ! is_array( $terms ) ) {
365 return;
366 }
367
368 $term_ids = wp_list_pluck( $terms, 'term_id' );
369
370 // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
371 wp_set_post_terms( $post_id, $term_ids, 'post_tag' );
372 }
373
374 /**
375 * Makes sure that all terms in the given list are in the given language.
376 * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
377 *
378 * @since 3.5
379 *
380 * @param WP_Term[] $terms List of terms to translate.
381 * @param string $taxonomy The terms' taxonomy.
382 * @param PLL_Language $language The language to translate the terms into.
383 * @return int[] List of `term_id`s.
384 *
385 * @phpstan-return array<positive-int>
386 */
387 private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
388 $term_ids_translated = array();
389
390 foreach ( $terms as $term ) {
391 $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
392 }
393
394 return array_filter( $term_ids_translated );
395 }
396
397 /**
398 * Translates the given term into the given language.
399 * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
400 *
401 * @since 3.5
402 *
403 * @param WP_Term $term The term to translate.
404 * @param string $taxonomy The term's taxonomy.
405 * @param PLL_Language $language The language to translate the term into.
406 * @return int A `term_id` on success, `0` on failure.
407 *
408 * @phpstan-return int<0, max>
409 */
410 private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
411 // Check if the term is in the correct language or if a translation exists.
412 $tr_term_id = $this->model->term->get( $term->term_id, $language );
413
414 if ( ! empty( $tr_term_id ) ) {
415 // Already in the correct language.
416 return $tr_term_id;
417 }
418
419 // Or choose the correct language for tags (initially defined by name).
420 $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
421
422 if ( ! empty( $tr_term_id ) ) {
423 return $tr_term_id;
424 }
425
426 // Or create the term in the correct language.
427 $tr_parent_term_id = 0;
428
429 if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
430 $parent = get_term( $term->parent, $taxonomy );
431
432 if ( $parent instanceof WP_Term ) {
433 // Translate the parent recursively.
434 $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
435 }
436 }
437
438 $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
439 if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
440 return $language;
441 }
442 return $lang;
443 };
444 $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
445 if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
446 return $tr_parent_term_id;
447 }
448 return $parent_id;
449 };
450 add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
451 add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
452 $new_term_info = wp_insert_term(
453 $term->name,
454 $taxonomy,
455 array(
456 'parent' => $tr_parent_term_id,
457 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
458 )
459 );
460 remove_filter( 'pll_inserted_term_language', $lang_callback );
461 remove_filter( 'pll_inserted_term_parent', $parent_callback );
462
463 if ( is_wp_error( $new_term_info ) ) {
464 // Term creation failed.
465 return 0;
466 }
467
468 $tr_term_id = max( 0, (int) $new_term_info['term_id'] );
469
470 if ( empty( $tr_term_id ) ) {
471 return 0;
472 }
473
474 $this->model->term->set_language( $tr_term_id, $language );
475
476 $trs = $this->model->term->get_translations( $term->term_id );
477
478 $trs[ $language->slug ] = $tr_term_id;
479
480 $this->model->term->save_translations( $term->term_id, $trs );
481
482 return $tr_term_id;
483 }
484 }
485