PluginProbe
Polylang / 3.2.2
Polylang v3.2.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.2.2, at include/crud-posts.php

326 lines 10.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 /**
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
22 */
23 protected $pref_lang;
24
25 /**
26 * Current language.
27 *
28 * @var PLL_Language
29 */
30 protected $curlang;
31
32 /**
33 * Constructor
34 *
35 * @since 2.4
36 *
37 * @param object $polylang
38 */
39 public function __construct( &$polylang ) {
40 $this->model = &$polylang->model;
41 $this->pref_lang = &$polylang->pref_lang;
42 $this->curlang = &$polylang->curlang;
43
44 add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
45 add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 );
46 add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 2 );
47 add_action( 'before_delete_post', array( $this, 'delete_post' ) );
48
49 // Specific for media
50 if ( $polylang->options['media_support'] ) {
51 add_action( 'add_attachment', array( $this, 'set_default_language' ) );
52 add_action( 'delete_attachment', array( $this, 'delete_post' ) );
53 add_filter( 'wp_delete_file', array( $this, 'wp_delete_file' ) );
54 }
55 }
56
57 /**
58 * Allows to set a language by default for posts if it has no language yet
59 *
60 * @since 1.5
61 *
62 * @param int $post_id
63 * @return void
64 */
65 public function set_default_language( $post_id ) {
66 if ( ! $this->model->post->get_language( $post_id ) ) {
67 if ( ! empty( $_GET['new_lang'] ) && $lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
68 // Defined only on admin.
69 $this->model->post->set_language( $post_id, $lang );
70 } elseif ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
71 // Testing $this->pref_lang makes this test pass only on admin.
72 $this->model->post->set_language( $post_id, $lang );
73 } elseif ( ( $parent_id = wp_get_post_parent_id( $post_id ) ) && $parent_lang = $this->model->post->get_language( $parent_id ) ) {
74 $this->model->post->set_language( $post_id, $parent_lang );
75 } elseif ( isset( $this->pref_lang ) ) {
76 // Always defined on admin, never defined on frontend
77 $this->model->post->set_language( $post_id, $this->pref_lang );
78 } else {
79 // Only on frontend due to the previous test always true on admin
80 $this->model->post->set_language( $post_id, $this->curlang );
81 }
82 }
83 }
84
85 /**
86 * Called when a post ( or page ) is saved, published or updated.
87 *
88 * @since 0.1
89 * @since 2.3 Does not save the language and translations anymore, unless the post has no language yet.
90 *
91 * @param int $post_id Post id of the post being saved.
92 * @param WP_Post $post The post being saved.
93 * @return void
94 */
95 public function save_post( $post_id, $post ) {
96 // Does nothing except on post types which are filterable.
97 if ( $this->model->is_translated_post_type( $post->post_type ) ) {
98 if ( $id = wp_is_post_revision( $post_id ) ) {
99 $post_id = $id;
100 }
101
102 $lang = $this->model->post->get_language( $post_id );
103
104 if ( empty( $lang ) ) {
105 $this->set_default_language( $post_id );
106 }
107
108 /**
109 * Fires after the post language and translations are saved.
110 *
111 * @since 1.2
112 *
113 * @param int $post_id Post id.
114 * @param WP_Post $post Post object.
115 * @param int[] $translations The list of translations post ids.
116 */
117 do_action( 'pll_save_post', $post_id, $post, $this->model->post->get_translations( $post_id ) );
118 }
119 }
120
121 /**
122 * Makes sure that saved terms are in the right language (especially tags with same name in different languages).
123 *
124 * @since 2.3
125 *
126 * @param int $object_id Object ID.
127 * @param WP_Term[] $terms An array of object terms.
128 * @param int[] $tt_ids An array of term taxonomy IDs.
129 * @param string $taxonomy Taxonomy slug.
130 * @return void
131 */
132 public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
133 static $avoid_recursion;
134
135 if ( ! $avoid_recursion && $this->model->is_translated_taxonomy( $taxonomy ) && ! empty( $terms ) ) {
136 $lang = $this->model->post->get_language( $object_id );
137
138 if ( ! empty( $lang ) && is_array( $terms ) ) {
139 // Convert to term ids if we got tag names
140 $strings = array_filter( $terms, 'is_string' );
141 if ( ! empty( $strings ) ) {
142 $_terms = get_terms( $taxonomy, array( 'name' => $strings, 'object_ids' => $object_id, 'fields' => 'ids' ) );
143 $terms = array_merge( array_diff( $terms, $strings ), $_terms );
144 }
145
146 $term_ids = array_combine( $terms, $terms );
147 $languages = array_map( array( $this->model->term, 'get_language' ), $term_ids );
148 $languages = array_filter( $languages ); // Remove terms without language.
149 $languages = wp_list_pluck( $languages, 'slug' );
150 $wrong_terms = array_diff( $languages, array( $lang->slug ) );
151
152 if ( ! empty( $wrong_terms ) ) {
153 // We got terms in a wrong language
154 $wrong_term_ids = array_keys( $wrong_terms );
155 $terms = get_the_terms( $object_id, $taxonomy );
156 wp_remove_object_terms( $object_id, $wrong_term_ids, $taxonomy );
157
158 if ( is_array( $terms ) ) {
159 $newterms = array();
160
161 foreach ( $terms as $term ) {
162 if ( in_array( $term->term_id, $wrong_term_ids ) ) {
163 // Check if the term is in the correct language or if a translation exist ( mainly for default category )
164 if ( $newterm = $this->model->term->get( $term->term_id, $lang ) ) {
165 $newterms[] = (int) $newterm;
166 }
167
168 // Or choose the correct language for tags ( initially defined by name )
169 elseif ( $newterm = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $lang ) ) {
170 $newterms[] = (int) $newterm; // Cast is important otherwise we get 'numeric' tags
171 }
172
173 // Or create the term in the correct language
174 elseif ( ! is_wp_error( $term_info = wp_insert_term( $term->name, $taxonomy ) ) ) {
175 $newterms[] = (int) $term_info['term_id'];
176 }
177 }
178 }
179
180 $avoid_recursion = true;
181 wp_set_object_terms( $object_id, array_unique( $newterms ), $taxonomy, true ); // Append
182 $avoid_recursion = false;
183 }
184 }
185 }
186 }
187 }
188
189 /**
190 * Make sure that the post parent is in the correct language.
191 *
192 * @since 1.8
193 *
194 * @param int $post_parent Post parent ID.
195 * @param int $post_id Post ID.
196 * @return int
197 */
198 public function wp_insert_post_parent( $post_parent, $post_id ) {
199 $lang = $this->model->post->get_language( $post_id );
200 $parent_post_type = $post_parent > 0 ? get_post_type( $post_parent ) : null;
201 // Dont break the hierarchy in case the post has no language
202 if ( ! empty( $lang ) && ! empty( $parent_post_type ) && $this->model->is_translated_post_type( $parent_post_type ) ) {
203 $post_parent = $this->model->post->get_translation( $post_parent, $lang );
204 }
205
206 return $post_parent;
207 }
208
209 /**
210 * Called when a post, page or media is deleted
211 * Don't delete translations if this is a post revision thanks to AndyDeGroo who catched this bug
212 * http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
213 *
214 * @since 0.1
215 *
216 * @param int $post_id
217 * @return void
218 */
219 public function delete_post( $post_id ) {
220 if ( ! wp_is_post_revision( $post_id ) ) {
221 $this->model->post->delete_translation( $post_id );
222 }
223 }
224
225 /**
226 * Prevents WP deleting files when there are still media using them.
227 *
228 * @since 0.9
229 *
230 * @param string $file Path to the file to delete.
231 * @return string Empty or unmodified path.
232 */
233 public function wp_delete_file( $file ) {
234 global $wpdb;
235
236 $uploadpath = wp_upload_dir();
237
238 // Get the main attached file.
239 $attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) );
240 $attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file );
241
242 $ids = $wpdb->get_col(
243 $wpdb->prepare(
244 "SELECT post_id FROM $wpdb->postmeta
245 WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
246 $attached_file
247 )
248 );
249
250 if ( ! empty( $ids ) ) {
251 return ''; // Prevent deleting the file.
252 }
253
254 return $file;
255 }
256
257 /**
258 * Creates a media translation
259 *
260 * @since 1.8
261 *
262 * @param int $post_id Original attachment id.
263 * @param string|object $lang New translation language.
264 * @return int Attachment id of the translated media.
265 */
266 public function create_media_translation( $post_id, $lang ) {
267 if ( empty( $post_id ) ) {
268 return 0;
269 }
270
271 $post = get_post( $post_id, ARRAY_A );
272
273 if ( empty( $post ) ) {
274 return 0;
275 }
276
277 $lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug.
278
279 if ( empty( $lang ) ) {
280 return 0;
281 }
282
283 // Create a new attachment ( translate attachment parent if exists ).
284 add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
285 unset( $post['ID'] ); // Will force the creation.
286 $post['post_parent'] = ( $post['post_parent'] && $tr_parent = $this->model->post->get_translation( $post['post_parent'], $lang->slug ) ) ? $tr_parent : 0;
287 $post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
288 $tr_id = wp_insert_attachment( wp_slash( $post ) );
289 remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
290
291 // Copy metadata.
292 $data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
293 if ( is_array( $data ) ) {
294 wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
295 }
296
297 // Copy attached file.
298 if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
299 update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
300 }
301
302 // Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
303 if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
304 add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
305 }
306
307 $this->model->post->set_language( $tr_id, $lang );
308
309 $translations = $this->model->post->get_translations( $post_id );
310 $translations[ $lang->slug ] = $tr_id;
311 $this->model->post->save_translations( $tr_id, $translations );
312
313 /**
314 * Fires after a media translation is created
315 *
316 * @since 1.6.4
317 *
318 * @param int $post_id Post id of the source media.
319 * @param int $tr_id Post id of the new media translation.
320 * @param string $slug Language code of the new translation.
321 */
322 do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
323 return $tr_id;
324 }
325 }
326