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