PluginProbe
Polylang / 3.8.6
Polylang v3.8.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 / src / crud-posts.php

crud-posts.php in Polylang 3.8.6, at src/crud-posts.php

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