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
← All changes | include/crud-posts.php +281 -99 2.93.6.7 View file →
@@ -3,31 +3,58 @@
3 3 * @package Polylang
4 4 */
5 5
6 6 /**
7 - * Adds actions and filters related to languages when creating, updating or deleting posts
8 - * Actions a filters used when reaing posts are handled separately
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 9 *
10 10 * @since 2.4
11 11 */
12 12 class PLL_CRUD_Posts {
13 + /**
14 + * @var PLL_Model
15 + */
16 + protected $model;
13 17
14 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 + /**
15 40 * Constructor
16 41 *
17 42 * @since 2.4
18 43 *
19 - * @param object $polylang
44 + * @param object $polylang The Polylang object.
20 45 */
21 46 public function __construct( &$polylang ) {
22 - $this->model = &$polylang->model;
47 + $this->options = &$polylang->options;
48 + $this->model = &$polylang->model;
23 49 $this->pref_lang = &$polylang->pref_lang;
24 - $this->curlang = &$polylang->curlang;
50 + $this->curlang = &$polylang->curlang;
25 51
26 52 add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
27 53 add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 );
28 - add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 4 );
54 + add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 2 );
29 55 add_action( 'before_delete_post', array( $this, 'delete_post' ) );
56 + add_action( 'post_updated', array( $this, 'force_tags_translation' ), 10, 3 );
30 57
31 58 // Specific for media
32 59 if ( $polylang->options['media_support'] ) {
33 60 add_action( 'add_attachment', array( $this, 'set_default_language' ) );
@@ -36,13 +63,14 @@
36 63 }
37 64 }
38 65
39 66 /**
40 - * Allows to set a language by default for posts if it has no language yet
67 + * Allows to set a language by default for posts if it has no language yet.
41 68 *
42 69 * @since 1.5
43 70 *
44 - * @param int $post_id
71 + * @param int $post_id Post ID.
72 + * @return void
45 73 */
46 74 public function set_default_language( $post_id ) {
47 75 if ( ! $this->model->post->get_language( $post_id ) ) {
48 76 if ( ! empty( $_GET['new_lang'] ) && $lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
@@ -53,28 +81,32 @@
53 81 $this->model->post->set_language( $post_id, $lang );
54 82 } elseif ( ( $parent_id = wp_get_post_parent_id( $post_id ) ) && $parent_lang = $this->model->post->get_language( $parent_id ) ) {
55 83 $this->model->post->set_language( $post_id, $parent_lang );
56 84 } elseif ( isset( $this->pref_lang ) ) {
57 - // Always defined on admin, never defined on frontend
85 + // Always defined on admin, never defined on frontend.
58 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 );
59 90 } else {
60 - // Only on frontend due to the previous test always true on admin
61 - $this->model->post->set_language( $post_id, $this->curlang );
91 + // In all other cases set to default language.
92 + $this->model->post->set_language( $post_id, $this->options['default_lang'] );
62 93 }
63 94 }
64 95 }
65 96
66 97 /**
67 - * Called when a post ( or page ) is saved, published or updated
98 + * Called when a post ( or page ) is saved, published or updated.
68 99 *
69 100 * @since 0.1
70 - * @since 2.3 Does not save the language and translations anymore, unless the post has no language yet
101 + * @since 2.3 Does not save the language and translations anymore, unless the post has no language yet.
71 102 *
72 - * @param int $post_id
73 - * @param object $post
103 + * @param int $post_id Post id of the post being saved.
104 + * @param WP_Post $post The post being saved.
105 + * @return void
74 106 */
75 107 public function save_post( $post_id, $post ) {
76 - // Does nothing except on post types which are filterable
108 + // Does nothing except on post types which are filterable.
77 109 if ( $this->model->is_translated_post_type( $post->post_type ) ) {
78 110 if ( $id = wp_is_post_revision( $post_id ) ) {
79 111 $post_id = $id;
80 112 }
@@ -85,15 +117,15 @@
85 117 $this->set_default_language( $post_id );
86 118 }
87 119
88 120 /**
89 - * Fires after the post language and translations are saved
121 + * Fires after the post language and translations are saved.
90 122 *
91 123 * @since 1.2
92 124 *
93 - * @param int $post_id Post id
94 - * @param object $post Post object
95 - * @param array $translations The list of translations post ids
125 + * @param int $post_id Post id.
126 + * @param WP_Post $post Post object.
127 + * @param int[] $translations The list of translations post ids.
96 128 */
97 129 do_action( 'pll_save_post', $post_id, $post, $this->model->post->get_translations( $post_id ) );
98 130 }
99 131 }
@@ -98,107 +130,112 @@
98 130 }
99 131 }
100 132
101 133 /**
102 - * Make sure saved terms are in the right language (especially tags with same name in different languages)
134 + * Makes sure that saved terms are in the right language.
103 135 *
104 136 * @since 2.3
105 137 *
106 - * @param int $object_id Object ID.
107 - * @param array $terms An array of object terms.
108 - * @param array $tt_ids An array of term taxonomy IDs.
109 - * @param string $taxonomy Taxonomy slug.
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
110 143 */
111 144 public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
112 145 static $avoid_recursion;
113 146
114 - if ( ! $avoid_recursion && $this->model->is_translated_taxonomy( $taxonomy ) && ! empty( $terms ) ) {
115 - $lang = $this->model->post->get_language( $object_id );
147 + if ( $avoid_recursion || empty( $terms ) || ! is_array( $terms ) || empty( $tt_ids )
148 + || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
149 + return;
150 + }
116 151
117 - if ( ! empty( $lang ) && is_array( $terms ) ) {
118 - // Convert to term ids if we got tag names
119 - $strings = array_filter( $terms, 'is_string' );
120 - if ( ! empty( $strings ) ) {
121 - $_terms = get_terms( $taxonomy, array( 'name' => $strings, 'object_ids' => $object_id, 'fields' => 'ids' ) );
122 - $terms = array_merge( array_diff( $terms, $strings ), $_terms );
123 - }
152 + $lang = $this->model->post->get_language( $object_id );
124 153
125 - $term_ids = array_combine( $terms, $terms );
126 - $languages = array_map( array( $this->model->term, 'get_language' ), $term_ids );
127 - $languages = array_filter( $languages ); // Remove terms without language.
128 - $languages = wp_list_pluck( $languages, 'slug' );
129 - $wrong_terms = array_diff( $languages, array( $lang->slug ) );
154 + if ( empty( $lang ) ) {
155 + return;
156 + }
130 157
131 - if ( ! empty( $wrong_terms ) ) {
132 - // We got terms in a wrong language
133 - $wrong_term_ids = array_keys( $wrong_terms );
134 - $terms = get_the_terms( $object_id, $taxonomy );
135 - wp_remove_object_terms( $object_id, $wrong_term_ids, $taxonomy );
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 + );
136 166
137 - if ( is_array( $terms ) ) {
138 - $newterms = array();
167 + if ( empty( $new_terms ) || ! is_array( $new_terms ) ) {
168 + // Terms not found.
169 + return;
170 + }
139 171
140 - foreach ( $terms as $term ) {
141 - if ( in_array( $term->term_id, $wrong_term_ids ) ) {
142 - // Check if the term is in the correct language or if a translation exist ( mainly for default category )
143 - if ( $newterm = $this->model->term->get( $term->term_id, $lang ) ) {
144 - $newterms[] = (int) $newterm;
145 - }
172 + $new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang );
146 173
147 - // Or choose the correct language for tags ( initially defined by name )
148 - elseif ( $newterm = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $lang ) ) {
149 - $newterms[] = (int) $newterm; // Cast is important otherwise we get 'numeric' tags
150 - }
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 + );
151 182
152 - // Or create the term in the correct language
153 - elseif ( ! is_wp_error( $term_info = wp_insert_term( $term->name, $taxonomy ) ) ) {
154 - $newterms[] = (int) $term_info['term_id'];
155 - }
156 - }
157 - }
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 );
158 186
159 - $avoid_recursion = true;
160 - wp_set_object_terms( $object_id, array_unique( $newterms ), $taxonomy, true ); // Append
161 - $avoid_recursion = false;
162 - }
163 - }
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 );
164 192 }
193 + } else {
194 + $orig_term_ids = array();
195 + $orig_term_ids_translated = array();
165 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 + }
166 207 }
167 208
168 209 /**
169 - * Make sure that the post parent is in the correct language when using bulk edit
210 + * Make sure that the post parent is in the correct language.
170 211 *
171 212 * @since 1.8
172 213 *
173 - * @param int $post_parent Post parent ID.
174 - * @param int $post_id Post ID.
175 - * @param array $new_postarr Array of parsed post data.
176 - * @param array $postarr Array of sanitized, but otherwise unmodified post data.
214 + * @param int $post_parent Post parent ID.
215 + * @param int $post_id Post ID.
177 216 * @return int
178 217 */
179 - public function wp_insert_post_parent( $post_parent, $post_id, $new_postarr, $postarr ) {
180 - if ( isset( $postarr['bulk_edit'], $postarr['inline_lang_choice'] ) ) {
181 - check_admin_referer( 'bulk-posts' );
182 - $lang = -1 == $postarr['inline_lang_choice'] ?
183 - $this->model->post->get_language( $post_id ) :
184 - $this->model->get_language( $postarr['inline_lang_choice'] );
185 - // Dont break the hierarchy in case the post has no language
186 - if ( ! empty( $lang ) ) {
187 - $post_parent = $this->model->post->get_translation( $post_parent, $lang );
188 - }
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 );
189 224 }
225 +
190 226 return $post_parent;
191 227 }
192 228
193 229 /**
194 230 * Called when a post, page or media is deleted
195 - * Don't delete translations if this is a post revision thanks to AndyDeGroo who catched this bug
231 + * Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug
196 232 * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
197 233 *
198 234 * @since 0.1
199 235 *
200 - * @param int $post_id
236 + * @param int $post_id Post ID.
237 + * @return void
201 238 */
202 239 public function delete_post( $post_id ) {
203 240 if ( ! wp_is_post_revision( $post_id ) ) {
204 241 $this->model->post->delete_translation( $post_id );
@@ -205,10 +242,9 @@
205 242 }
206 243 }
207 244
208 245 /**
209 - * Prevents WP deleting files when there are still media using them
210 - * Thanks to Bruno "Aesqe" Babic and its plugin file gallery in which I took all the ideas for this function
246 + * Prevents WP deleting files when there are still media using them.
211 247 *
212 248 * @since 0.9
213 249 *
214 250 * @param string $file Path to the file to delete.
@@ -218,21 +254,22 @@
218 254 global $wpdb;
219 255
220 256 $uploadpath = wp_upload_dir();
221 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 +
222 262 $ids = $wpdb->get_col(
223 263 $wpdb->prepare(
224 264 "SELECT post_id FROM $wpdb->postmeta
225 265 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
226 - substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) )
266 + $attached_file
227 267 )
228 268 );
229 269
230 270 if ( ! empty( $ids ) ) {
231 - // Regenerate intermediate sizes if it's an image ( since we could not prevent WP deleting them before ).
232 - require_once ABSPATH . 'wp-admin/includes/image.php'; // In case the file is deleted outside admin.
233 - wp_update_attachment_metadata( $ids[0], wp_slash( wp_generate_attachment_metadata( $ids[0], $file ) ) ); // Directly uses update_post_meta, so expects slashed.
234 - return ''; // Prevent deleting the main file.
271 + return ''; // Prevent deleting the file.
235 272 }
236 273
237 274 return $file;
238 275 }
@@ -247,29 +284,36 @@
247 284 * @return int Attachment id of the translated media.
248 285 */
249 286 public function create_media_translation( $post_id, $lang ) {
250 287 if ( empty( $post_id ) ) {
251 - return $post_id;
288 + return 0;
252 289 }
253 290
254 - $post = get_post( $post_id );
291 + $post = get_post( $post_id, ARRAY_A );
255 292
256 293 if ( empty( $post ) ) {
257 - return $post;
294 + return 0;
258 295 }
259 296
260 297 $lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug.
261 298
299 + if ( empty( $lang ) ) {
300 + return 0;
301 + }
302 +
262 303 // Create a new attachment ( translate attachment parent if exists ).
263 304 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
264 - $post->ID = null; // Will force the creation
265 - $post->post_parent = ( $post->post_parent && $tr_parent = $this->model->post->get_translation( $post->post_parent, $lang->slug ) ) ? $tr_parent : 0;
266 - $post->tax_input = array( 'language' => array( $lang->slug ) ); // Assigns the language.
267 - $tr_id = wp_insert_attachment( wp_slash( (array) $post ) );
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 ) );
268 311 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
269 312
270 313 // Copy metadata.
271 - if ( $data = wp_get_attachment_metadata( $post_id, true ) ) { // Unfiltered.
314 + $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
315 + if ( is_array( $data ) ) {
272 316 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
273 317 }
274 318
275 319 // Copy attached file.
@@ -298,6 +342,144 @@
298 342 * @param string $slug Language code of the new translation.
299 343 */
300 344 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
301 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;
302 484 }
303 485 }