PluginProbe
Polylang / trunk
Polylang vtrunk
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 / admin / admin-filters-columns.php

admin-filters-columns.php in Polylang trunk, at src/admin/admin-filters-columns.php

507 lines 15.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 /**
7 * Adds the language column in posts and terms list tables.
8 * Manages quick edit and bulk edit as well.
9 *
10 * @since 1.2
11 */
12 class PLL_Admin_Filters_Columns {
13 /**
14 * @var PLL_Model
15 */
16 public $model;
17
18 /**
19 * This class is instantiated on `wp_loaded` (prio 5), see `PLL_Admin::add_filters()`.
20 * `$polylang->links` is instantiated on `wp_loaded` (prio 1), see `PLL_Admin_Base::init()`.
21 * This means `$polylang->links` cannot be `null`.
22 *
23 * @var PLL_Admin_Links
24 */
25 public $links;
26
27 /**
28 * Language selected in the admin language filter.
29 *
30 * @var PLL_Language|null
31 */
32 public $filter_lang;
33
34 /**
35 * Constructor: setups filters and actions.
36 *
37 * @since 1.2
38 *
39 * @param object $polylang The Polylang object.
40 */
41 public function __construct( &$polylang ) {
42 $this->links = &$polylang->links;
43 $this->model = &$polylang->model;
44 $this->filter_lang = &$polylang->filter_lang;
45
46 // Hide the column of the filtered language.
47 add_filter( 'hidden_columns', array( $this, 'hidden_columns' ) ); // Since WP 4.4.
48
49 // Add the language and translations columns in 'All Posts', 'All Pages' and 'Media library' panels.
50 foreach ( $this->model->get_translated_post_types() as $type ) {
51 // Use the latest filter late as some plugins purely overwrite what's done by others :(
52 // Specific case for media.
53 add_filter( 'manage_' . ( 'attachment' == $type ? 'upload' : 'edit-' . $type ) . '_columns', array( $this, 'add_post_column' ), 100 );
54 add_action( 'manage_' . ( 'attachment' == $type ? 'media' : $type . '_posts' ) . '_custom_column', array( $this, 'post_column' ), 10, 2 );
55 }
56
57 // Quick edit and bulk edit.
58 add_filter( 'quick_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );
59 add_filter( 'bulk_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );
60
61 // Adds the language column in the 'Categories' and 'Post Tags' tables.
62 foreach ( $this->model->get_translated_taxonomies() as $tax ) {
63 add_filter( 'manage_edit-' . $tax . '_columns', array( $this, 'add_term_column' ) );
64 add_filter( 'manage_' . $tax . '_custom_column', array( $this, 'term_column' ), 10, 3 );
65 }
66
67 // Ajax responses to update list table rows.
68 add_action( 'wp_ajax_pll_update_post_rows', array( $this, 'ajax_update_post_rows' ) );
69 add_action( 'wp_ajax_pll_update_term_rows', array( $this, 'ajax_update_term_rows' ) );
70 }
71
72 /**
73 * Adds languages and translations columns in posts, pages, media, categories and tags tables.
74 *
75 * @since 0.8.2
76 *
77 * @param string[] $columns List of table columns.
78 * @param string $before The column before which we want to add our languages.
79 * @return string[] Modified list of columns.
80 */
81 protected function add_column( array $columns, string $before ): array {
82 if ( $n = array_search( $before, array_keys( $columns ) ) ) {
83 $end = array_slice( $columns, $n );
84 $columns = array_slice( $columns, 0, $n );
85 }
86
87 foreach ( $this->model->get_languages_list() as $language ) {
88 /*
89 * Hack:
90 * - `WP_Screen::render_list_table_columns_preferences()` applies `wp_strip_all_tags()` in the screen options:
91 * this will remove the flag, and reveal the language name (that looses its `<span>`).
92 * - `WP_List_Table::print_column_headers()` doesn't apply any escaping function in the column headers:
93 * the flag will be displayed, and the language name will still be present but visually hidden.
94 */
95 $columns[ "language_{$language->slug}" ] = sprintf(
96 '%s<span class="screen-reader-text">%s</span>',
97 $language->get_admin_flag( 'aria-hidden' ),
98 esc_html( $language->name )
99 );
100 }
101
102 return isset( $end ) ? array_merge( $columns, $end ) : $columns;
103 }
104
105 /**
106 * Returns the first language column in posts, pages, media, categories and tags tables.
107 *
108 * @since 0.9
109 *
110 * @return string First language column name.
111 */
112 protected function get_first_language_column(): string {
113 $languages = $this->model->get_languages_list();
114 $language = reset( $languages );
115 return ! empty( $language ) ? "language_{$language->slug}" : '';
116 }
117
118 /**
119 * Hides the column for the filtered language.
120 *
121 * @since 2.7
122 *
123 * @param string[] $hidden Array of hidden columns.
124 * @return string[]
125 */
126 public function hidden_columns( $hidden ) {
127 if ( ! empty( $this->filter_lang ) ) {
128 $hidden[] = 'language_' . $this->filter_lang->slug;
129 }
130 return $hidden;
131 }
132
133 /**
134 * Adds the language and translations columns ( before the comments column ) in the posts, pages and media library tables.
135 *
136 * @since 0.1
137 *
138 * @param string[] $columns List of posts table columns.
139 * @return string[] Modified list of columns.
140 */
141 public function add_post_column( $columns ) {
142 return $this->add_column( (array) $columns, 'comments' );
143 }
144
145 /**
146 * Fills the language and translations columns in the posts, pages and media library tables
147 * take care that when doing ajax inline edit, the post may not be updated in database yet.
148 *
149 * @since 0.1
150 *
151 * @param string $column Column name.
152 * @param int $post_id Post ID.
153 * @return void
154 */
155 public function post_column( $column, $post_id ): void {
156 if ( ! str_starts_with( $column, 'language_' ) ) {
157 return;
158 }
159
160 $post = get_post( (int) $post_id );
161
162 if ( ! $post instanceof WP_Post ) {
163 // Should not happen.
164 return;
165 }
166
167 $inline = isset( $_POST['inline_lang_choice'], $_REQUEST['_inline_edit'] ) && is_string( $_REQUEST['_inline_edit'] ) && wp_verify_nonce( $_REQUEST['_inline_edit'], 'inlineeditnonce' );
168 $lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->post->get_language( $post->ID );
169
170 if ( empty( $lang ) ) {
171 return;
172 }
173
174 $language = $this->model->get_language( substr( $column, 9 ) );
175
176 if ( empty( $language ) ) {
177 return;
178 }
179
180 // Hidden field containing the post language for quick edit.
181 if ( $column === $this->get_first_language_column() ) {
182 printf( '<div class="hidden" id="lang_%d">%s</div>', (int) $post->ID, esc_html( $lang->slug ) );
183 }
184
185 $tr_id = $this->model->post->get( $post->ID, $language );
186 $tr_post = $tr_id ? get_post( $tr_id ) : null;
187
188 if ( ! $tr_post instanceof WP_Post ) {
189 // Link to add a new translation: no translation for this language yet, or it doesn't exist anymore.
190 echo $this->links->get_new_post_link_html( $post, $language ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
191 return;
192 }
193
194 // Link to edit (or not) the post or a translation.
195 echo $this->links->get_edit_post_link_html( $tr_post, $tr_post->ID === $post->ID ? 'list_current' : 'list_translation' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
196 }
197
198 /**
199 * Quick edit & bulk edit.
200 *
201 * @since 0.9
202 *
203 * @param string $column Column name.
204 * @return string Unmodified column.
205 */
206 public function quick_edit_custom_box( $column ) {
207 if ( $column !== $this->get_first_language_column() ) {
208 return $column;
209 }
210
211 $elements = $this->model->languages->filter( 'translator' )->get_list();
212 if ( current_filter() == 'bulk_edit_custom_box' ) {
213 array_unshift( $elements, (object) array( 'slug' => -1, 'name' => __( '&mdash; No Change &mdash;', 'polylang' ) ) );
214 }
215
216 $dropdown = new PLL_Walker_Dropdown();
217 printf(
218 '<fieldset class="inline-edit-col-left">
219 <div class="inline-edit-col">
220 <label class="alignleft">
221 <span class="title">%s</span>
222 %s
223 </label>
224 </div>
225 </fieldset>',
226 esc_html__( 'Language', 'polylang' ),
227 $dropdown->walk( $elements, -1, array( 'name' => 'inline_lang_choice', 'id' => '' ) ) // phpcs:ignore WordPress.Security.EscapeOutput
228 );
229
230 return $column;
231 }
232
233 /**
234 * Adds the language column (before the posts column) in the 'Categories' or 'Post Tags' table.
235 *
236 * @since 0.1
237 *
238 * @param string[] $columns List of terms table columns.
239 * @return string[] modified List of columns.
240 */
241 public function add_term_column( $columns ) {
242 $screen = get_current_screen();
243
244 // Avoid displaying languages in screen options when editing a term.
245 if ( $screen instanceof WP_Screen && 'term' === $screen->base ) {
246 return $columns;
247 }
248
249 return $this->add_column( (array) $columns, 'posts' );
250 }
251
252 /**
253 * Fills the language column in the taxonomy terms list table.
254 *
255 * @since 0.1
256 *
257 * @param string $out Column output.
258 * @param string $column Column name.
259 * @param int $term_id Term ID.
260 * @return string
261 */
262 public function term_column( $out, $column, $term_id ) {
263 if ( ! str_starts_with( $column, 'language_' ) ) {
264 return $out;
265 }
266
267 $term_id = (int) $term_id;
268 $inline = isset( $_POST['inline_lang_choice'], $_REQUEST['_inline_edit'] ) && is_string( $_REQUEST['_inline_edit'] ) && wp_verify_nonce( $_REQUEST['_inline_edit'], 'taxinlineeditnonce' );
269 $lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->term->get_language( $term_id );
270
271 if ( empty( $lang ) ) {
272 return $out;
273 }
274
275 if ( isset( $GLOBALS['post_type'] ) ) {
276 $post_type = $GLOBALS['post_type'];
277 } elseif ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
278 $post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification
279 }
280
281 if ( isset( $GLOBALS['taxonomy'] ) ) {
282 $taxonomy = $GLOBALS['taxonomy'];
283 } elseif ( isset( $_REQUEST['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
284 $taxonomy = sanitize_key( $_REQUEST['taxonomy'] ); // phpcs:ignore WordPress.Security.NonceVerification
285 }
286
287 if ( ! isset( $taxonomy, $post_type ) || ! is_string( $post_type ) || ! is_string( $taxonomy ) || ! post_type_exists( $post_type ) || ! taxonomy_exists( $taxonomy ) ) {
288 return $out;
289 }
290
291 $language = $this->model->get_language( substr( $column, 9 ) );
292
293 if ( empty( $language ) ) {
294 return $out;
295 }
296
297 $term = get_term( $term_id, $taxonomy );
298
299 if ( ! $term instanceof WP_Term ) {
300 // Should not happen.
301 return $out;
302 }
303
304 $tr_id = $this->model->term->get( $term->term_id, $language );
305 $tr_term = $tr_id ? get_term( $tr_id, $taxonomy ) : null;
306
307 if ( ! $tr_term instanceof WP_Term ) {
308 // Link to add a new translation: no translation for this language yet, or it doesn't exist anymore.
309 $out .= $this->links->get_new_term_link_html( $term, $post_type, $language );
310 } else {
311 // Link to edit (or not) the term or a translation.
312 $out .= $this->links->get_edit_term_link_html( $tr_term, $post_type, $tr_term->term_id === $term->term_id ? 'list_current' : 'list_translation' );
313 }
314
315 if ( $this->get_first_language_column() === $column ) {
316 // Hidden field containing the term language for quick edit.
317 $out .= sprintf( '<div class="hidden" id="lang_%d">%s</div>', $term->term_id, esc_html( $lang->slug ) );
318
319 /**
320 * Filters the output of the first language column in the terms list table.
321 *
322 * @since 3.7
323 *
324 * @param string $output First language column output.
325 * @param int $term_id Term ID.
326 * @param string $lang Language code.
327 */
328 $out = apply_filters( 'pll_first_language_term_column', $out, $term->term_id, $lang->slug );
329 }
330
331 return $out;
332 }
333
334 /**
335 * Update rows of translated posts when the language is modified in quick edit.
336 *
337 * @since 1.7
338 *
339 * @return void
340 *
341 * @phpstan-return never
342 */
343 public function ajax_update_post_rows(): void {
344 check_ajax_referer( 'inlineeditnonce', '_pll_nonce' );
345
346 if ( ! isset( $_POST['post_type'], $_POST['post_id'], $_POST['screen'] ) ) {
347 wp_die( 0 );
348 }
349
350 if ( ! is_numeric( $_POST['post_id'] ) ) {
351 wp_die( 0 );
352 }
353
354 $post_type_object = get_post_type_object( sanitize_key( $_POST['post_type'] ) );
355
356 if ( empty( $post_type_object ) || ! $this->model->is_translated_post_type( $post_type_object->name ) ) {
357 wp_die( 0 );
358 }
359
360 $wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
361
362 if ( empty( $wp_list_table ) ) {
363 wp_die( 0 );
364 }
365
366 $response = new WP_Ajax_Response();
367
368 // Collect old translations.
369 if ( ! empty( $_POST['translations'] ) && is_string( $_POST['translations'] ) ) {
370 $translations = explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
371 $translations = array_filter( $translations, 'is_numeric' );
372 $translations = array_map( 'intval', $translations );
373 $translations = array_filter( $translations );
374 } else {
375 $translations = array();
376 }
377
378 $translations = array_merge( $translations, $this->model->post->get_translations( (int) $_POST['post_id'] ) );
379 $translations = array_unique( $translations );
380
381 if ( empty( $translations ) ) { // May occur if the modified post has no language.
382 $response->send();
383 }
384
385 /** @var WP_Post[] */
386 $posts = ( new WP_Query() )->query(
387 array(
388 // Do not add `post_status`, as this allows `WP_Query` to handle user capabilities to read private posts.
389 'post__in' => $translations,
390 'post_type' => $post_type_object->name,
391 'posts_per_page' => count( $translations ),
392 'no_found_rows' => true,
393 'orderby' => 'ID',
394 'lang' => '',
395 )
396 );
397
398 foreach ( $posts as $post ) {
399 if ( $post_type_object->hierarchical ) {
400 $level = count( get_ancestors( $post->ID, $post->post_type, 'post_type' ) );
401 } else {
402 $level = 0;
403 }
404
405 ob_start();
406 $wp_list_table->single_row( $post, $level );
407 $data = (string) ob_get_clean();
408
409 $response->add(
410 array(
411 'what' => 'row',
412 'data' => $data,
413 'supplemental' => array( 'post_id' => $post->ID ),
414 )
415 );
416 }
417
418 $response->send();
419 }
420
421 /**
422 * Update rows of translated terms when adding / deleting a translation
423 * or when the language is modified in quick edit.
424 *
425 * @since 1.7
426 *
427 * @return void
428 *
429 * @phpstan-return never
430 */
431 public function ajax_update_term_rows(): void {
432 check_ajax_referer( 'pll_language', '_pll_nonce' );
433
434 if ( ! isset( $_POST['taxonomy'], $_POST['term_id'], $_POST['screen'] ) ) {
435 wp_die( 0 );
436 }
437
438 if ( ! is_numeric( $_POST['term_id'] ) ) {
439 wp_die( 0 );
440 }
441
442 $taxonomy_object = get_taxonomy( sanitize_key( $_POST['taxonomy'] ) );
443
444 if ( empty( $taxonomy_object ) || ! $this->model->is_translated_taxonomy( $taxonomy_object->name ) ) {
445 wp_die( 0 );
446 }
447
448 $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
449
450 if ( empty( $wp_list_table ) ) {
451 wp_die( 0 );
452 }
453
454 $response = new WP_Ajax_Response();
455
456 // Collect old translations.
457 if ( ! empty( $_POST['translations'] ) && is_string( $_POST['translations'] ) ) {
458 $translations = explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
459 $translations = array_filter( $translations, 'is_numeric' );
460 $translations = array_map( 'intval', $translations );
461 $translations = array_filter( $translations );
462 } else {
463 $translations = array();
464 }
465
466 $translations = array_merge( $translations, $this->model->term->get_translations( (int) $_POST['term_id'] ) );
467 $translations = array_unique( $translations );
468
469 if ( empty( $translations ) ) { // May occur if the modified term has no language.
470 $response->send();
471 }
472
473 /** @var WP_Term[] */
474 $terms = ( new WP_Term_Query() )->query(
475 array(
476 'include' => $translations,
477 'taxonomy' => $taxonomy_object->name,
478 'hide_empty' => false,
479 'orderby' => 'term_id',
480 'lang' => '',
481 )
482 );
483
484 foreach ( $terms as $term ) {
485 if ( $taxonomy_object->hierarchical ) {
486 $level = count( get_ancestors( $term->term_id, $taxonomy_object->name, 'taxonomy' ) );
487 } else {
488 $level = 0;
489 }
490
491 ob_start();
492 $wp_list_table->single_row( $term, $level );
493 $data = (string) ob_get_clean();
494
495 $response->add(
496 array(
497 'what' => 'row',
498 'data' => $data,
499 'supplemental' => array( 'term_id' => $term->term_id ),
500 )
501 );
502 }
503
504 $response->send();
505 }
506 }
507