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

430 lines 13.3 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 ) || empty( $tt_ids )
148 || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
149 return;
150 }
151
152 $lang = $this->model->post->get_language( $object_id );
153
154 if ( empty( $lang ) ) {
155 return;
156 }
157
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 );
166
167 if ( empty( $new_terms ) || ! is_array( $new_terms ) ) {
168 // Terms not found.
169 return;
170 }
171
172 $new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang );
173
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 );
182
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 );
186
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 );
192 }
193 } else {
194 $orig_term_ids = array();
195 $orig_term_ids_translated = array();
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 }
207 }
208
209 /**
210 * Make sure that the post parent is in the correct language.
211 *
212 * @since 1.8
213 *
214 * @param int $post_parent Post parent ID.
215 * @param int $post_id Post ID.
216 * @return int
217 */
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 );
224 }
225
226 return $post_parent;
227 }
228
229 /**
230 * Called when a post, page or media is deleted
231 * Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug
232 * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
233 *
234 * @since 0.1
235 *
236 * @param int $post_id Post ID.
237 * @return void
238 */
239 public function delete_post( $post_id ) {
240 if ( ! wp_is_post_revision( $post_id ) ) {
241 $this->model->post->delete_translation( $post_id );
242 }
243 }
244
245 /**
246 * Prevents WP deleting files when there are still media using them.
247 *
248 * @since 0.9
249 *
250 * @param string $file Path to the file to delete.
251 * @return string Empty or unmodified path.
252 */
253 public function wp_delete_file( $file ) {
254 global $wpdb;
255
256 $uploadpath = wp_upload_dir();
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
262 $ids = $wpdb->get_col(
263 $wpdb->prepare(
264 "SELECT post_id FROM $wpdb->postmeta
265 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
266 $attached_file
267 )
268 );
269
270 if ( ! empty( $ids ) ) {
271 return ''; // Prevent deleting the file.
272 }
273
274 return $file;
275 }
276
277 /**
278 * Creates a media translation
279 *
280 * @since 1.8
281 * @since 3.7 Deprecated in favor of PLL_Translated_Post::create_media_translation().
282 *
283 * @param int $post_id Original attachment id.
284 * @param string|PLL_Language $lang New translation language.
285 * @return int Attachment id of the translated media.
286 */
287 public function create_media_translation( $post_id, $lang ) {
288 _deprecated_function( __METHOD__, '3.7', 'PLL_Translated_Post::create_media_translation()' );
289 return $this->model->post->create_media_translation( $post_id, $lang );
290 }
291
292 /**
293 * Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`.
294 *
295 * @since 3.4.5
296 *
297 * @param int $post_id Post ID, unused.
298 * @param WP_Post $post_after Post object following the update.
299 * @param WP_Post $post_before Post object before the update.
300 * @return void
301 */
302 public function force_tags_translation( $post_id, $post_after, $post_before ) {
303 if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) {
304 return;
305 }
306
307 $terms = get_the_terms( $post_before, 'post_tag' );
308
309 if ( empty( $terms ) || ! is_array( $terms ) ) {
310 return;
311 }
312
313 $term_ids = wp_list_pluck( $terms, 'term_id' );
314
315 // Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
316 wp_set_post_terms( $post_id, $term_ids, 'post_tag' );
317 }
318
319 /**
320 * Makes sure that all terms in the given list are in the given language.
321 * If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
322 *
323 * @since 3.5
324 *
325 * @param WP_Term[] $terms List of terms to translate.
326 * @param string $taxonomy The terms' taxonomy.
327 * @param PLL_Language $language The language to translate the terms into.
328 * @return int[] List of `term_id`s.
329 *
330 * @phpstan-return array<positive-int>
331 */
332 private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
333 $term_ids_translated = array();
334
335 foreach ( $terms as $term ) {
336 $term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
337 }
338
339 return array_filter( $term_ids_translated );
340 }
341
342 /**
343 * Translates the given term into the given language.
344 * If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
345 *
346 * @since 3.5
347 *
348 * @param WP_Term $term The term to translate.
349 * @param string $taxonomy The term's taxonomy.
350 * @param PLL_Language $language The language to translate the term into.
351 * @return int A `term_id` on success, `0` on failure.
352 *
353 * @phpstan-return int<0, max>
354 */
355 private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
356 // Check if the term is in the correct language or if a translation exists.
357 $tr_term_id = $this->model->term->get( $term->term_id, $language );
358
359 if ( ! empty( $tr_term_id ) ) {
360 // Already in the correct language.
361 return $tr_term_id;
362 }
363
364 // Or choose the correct language for tags (initially defined by name).
365 $tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
366
367 if ( ! empty( $tr_term_id ) ) {
368 return $tr_term_id;
369 }
370
371 // Or create the term in the correct language.
372 $tr_parent_term_id = 0;
373
374 if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
375 $parent = get_term( $term->parent, $taxonomy );
376
377 if ( $parent instanceof WP_Term ) {
378 // Translate the parent recursively.
379 $tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
380 }
381 }
382
383 $lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
384 if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
385 return $language;
386 }
387 return $lang;
388 };
389 $parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
390 if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
391 return $tr_parent_term_id;
392 }
393 return $parent_id;
394 };
395 add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
396 add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
397 $new_term_info = wp_insert_term(
398 $term->name,
399 $taxonomy,
400 array(
401 'parent' => $tr_parent_term_id,
402 'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
403 )
404 );
405 remove_filter( 'pll_inserted_term_language', $lang_callback );
406 remove_filter( 'pll_inserted_term_parent', $parent_callback );
407
408 if ( is_wp_error( $new_term_info ) ) {
409 // Term creation failed.
410 return 0;
411 }
412
413 $tr_term_id = max( 0, (int) $new_term_info['term_id'] );
414
415 if ( empty( $tr_term_id ) ) {
416 return 0;
417 }
418
419 $this->model->term->set_language( $tr_term_id, $language );
420
421 $trs = $this->model->term->get_translations( $term->term_id );
422
423 $trs[ $language->slug ] = $tr_term_id;
424
425 $this->model->term->save_translations( $term->term_id, $trs );
426
427 return $tr_term_id;
428 }
429 }
430