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

446 lines 12.8 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 $uploadpath = wp_upload_dir();
273
274 // Get the main attached file.
275 $attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) );
276 $attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file );
277
278 $ids = $wpdb->get_col(
279 $wpdb->prepare(
280 "SELECT post_id FROM $wpdb->postmeta
281 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
282 $attached_file
283 )
284 );
285
286 if ( ! empty( $ids ) ) {
287 return ''; // Prevent deleting the file.
288 }
289
290 return $file;
291 }
292
293 /**
294 * Creates a media translation
295 *
296 * @since 1.8
297 * @since 3.7 Deprecated in favor of PLL_Translated_Post::create_media_translation().
298 *
299 * @param int $post_id Original attachment id.
300 * @param string|PLL_Language $lang New translation language.
301 * @return int Attachment id of the translated media.
302 */
303 public function create_media_translation( $post_id, $lang ) {
304 _deprecated_function( __METHOD__, '3.7', 'PLL_Translated_Post::create_media_translation()' );
305 return $this->model->post->create_media_translation( $post_id, $lang );
306 }
307
308 /**
309 * Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`.
310 *
311 * @since 3.4.5
312 *
313 * @param int $post_id Post ID, unused.
314 * @param WP_Post $post_after Post object following the update.
315 * @param WP_Post $post_before Post object before the update.
316 * @return void
317 */
318 public function force_tags_translation( $post_id, $post_after, $post_before ) {
319 if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) {
320 return;
321 }
322
323 $terms = get_the_terms( $post_before, 'post_tag' );
324
325 if ( empty( $terms ) || ! is_array( $terms ) ) {
326 return;
327 }
328
329 $term_ids = wp_list_pluck( $terms, 'term_id' );
330
331 // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
332 wp_set_post_terms( $post_id, $term_ids, 'post_tag' );
333 }
334
335 /**
336 * Makes sure that all terms in the given list are in the given language.
337 * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
338 *
339 * @since 3.5
340 *
341 * @param WP_Term[] $terms List of terms to translate.
342 * @param string $taxonomy The terms' taxonomy.
343 * @param PLL_Language $language The language to translate the terms into.
344 * @return int[] List of `term_id`s.
345 *
346 * @phpstan-return array<positive-int>
347 */
348 private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
349 $term_ids_translated = array();
350
351 foreach ( $terms as $term ) {
352 $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
353 }
354
355 return array_filter( $term_ids_translated );
356 }
357
358 /**
359 * Translates the given term into the given language.
360 * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
361 *
362 * @since 3.5
363 *
364 * @param WP_Term $term The term to translate.
365 * @param string $taxonomy The term's taxonomy.
366 * @param PLL_Language $language The language to translate the term into.
367 * @return int A `term_id` on success, `0` on failure.
368 *
369 * @phpstan-return int<0, max>
370 */
371 private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
372 // Check if the term is in the correct language or if a translation exists.
373 $tr_term_id = $this->model->term->get( $term->term_id, $language );
374
375 if ( ! empty( $tr_term_id ) ) {
376 // Already in the correct language.
377 return $tr_term_id;
378 }
379
380 // Or choose the correct language for tags (initially defined by name).
381 $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
382
383 if ( ! empty( $tr_term_id ) ) {
384 return $tr_term_id;
385 }
386
387 // Or create the term in the correct language.
388 $tr_parent_term_id = 0;
389
390 if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
391 $parent = get_term( $term->parent, $taxonomy );
392
393 if ( $parent instanceof WP_Term ) {
394 // Translate the parent recursively.
395 $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
396 }
397 }
398
399 $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
400 if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
401 return $language;
402 }
403 return $lang;
404 };
405 $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
406 if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
407 return $tr_parent_term_id;
408 }
409 return $parent_id;
410 };
411 add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
412 add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
413 $new_term_info = wp_insert_term(
414 $term->name,
415 $taxonomy,
416 array(
417 'parent' => $tr_parent_term_id,
418 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
419 )
420 );
421 remove_filter( 'pll_inserted_term_language', $lang_callback );
422 remove_filter( 'pll_inserted_term_parent', $parent_callback );
423
424 if ( is_wp_error( $new_term_info ) ) {
425 // Term creation failed.
426 return 0;
427 }
428
429 $tr_term_id = max( 0, (int) $new_term_info['term_id'] );
430
431 if ( empty( $tr_term_id ) ) {
432 return 0;
433 }
434
435 $this->model->term->set_language( $tr_term_id, $language );
436
437 $trs = $this->model->term->get_translations( $term->term_id );
438
439 $trs[ $language->slug ] = $tr_term_id;
440
441 $this->model->term->save_translations( $term->term_id, $trs );
442
443 return $tr_term_id;
444 }
445 }
446