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

477 lines 15.0 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 catched 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 $post_before = $post_before->to_array();
359
360 if ( ! empty( $post_before['tags_input'] ) ) {
361 // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
362 wp_set_post_tags( $post_id, $post_before['tags_input'] );
363 }
364 }
365
366 /**
367 * Makes sure that all terms in the given list are in the given language.
368 * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
369 *
370 * @since 3.5
371 *
372 * @param WP_Term[] $terms List of terms to translate.
373 * @param string $taxonomy The terms' taxonomy.
374 * @param PLL_Language $language The language to translate the terms into.
375 * @return int[] List of `term_id`s.
376 *
377 * @phpstan-return array<positive-int>
378 */
379 private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
380 $term_ids_translated = array();
381
382 foreach ( $terms as $term ) {
383 $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
384 }
385
386 return array_filter( $term_ids_translated );
387 }
388
389 /**
390 * Translates the given term into the given language.
391 * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
392 *
393 * @since 3.5
394 *
395 * @param WP_Term $term The term to translate.
396 * @param string $taxonomy The term's taxonomy.
397 * @param PLL_Language $language The language to translate the term into.
398 * @return int A `term_id` on success, `0` on failure.
399 *
400 * @phpstan-return int<0, max>
401 */
402 private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
403 // Check if the term is in the correct language or if a translation exists.
404 $tr_term_id = $this->model->term->get( $term->term_id, $language );
405
406 if ( ! empty( $tr_term_id ) ) {
407 // Already in the correct language.
408 return $tr_term_id;
409 }
410
411 // Or choose the correct language for tags (initially defined by name).
412 $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
413
414 if ( ! empty( $tr_term_id ) ) {
415 return $tr_term_id;
416 }
417
418 // Or create the term in the correct language.
419 $tr_parent_term_id = 0;
420
421 if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
422 $parent = get_term( $term->parent, $taxonomy );
423
424 if ( $parent instanceof WP_Term ) {
425 // Translate the parent recursively.
426 $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
427 }
428 }
429
430 $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
431 if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
432 return $language;
433 }
434 return $lang;
435 };
436 $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
437 if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
438 return $tr_parent_term_id;
439 }
440 return $parent_id;
441 };
442 add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
443 add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
444 $new_term_info = wp_insert_term(
445 $term->name,
446 $taxonomy,
447 array(
448 'parent' => $tr_parent_term_id,
449 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
450 )
451 );
452 remove_filter( 'pll_inserted_term_language', $lang_callback );
453 remove_filter( 'pll_inserted_term_parent', $parent_callback );
454
455 if ( is_wp_error( $new_term_info ) ) {
456 // Term creation failed.
457 return 0;
458 }
459
460 $tr_term_id = max( 0, (int) $new_term_info['term_id'] );
461
462 if ( empty( $tr_term_id ) ) {
463 return 0;
464 }
465
466 $this->model->term->set_language( $tr_term_id, $language );
467
468 $trs = $this->model->term->get_translations( $term->term_id );
469
470 $trs[ $language->slug ] = $tr_term_id;
471
472 $this->model->term->save_translations( $term->term_id, $trs );
473
474 return $tr_term_id;
475 }
476 }
477