PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
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 3.8.6, at src/admin/admin-filters-columns.php

496 lines 15.2 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 $columns[ 'language_' . $language->slug ] = $this->links->get_flag_html( $language ) . '<span class="screen-reader-text">' . esc_html( $language->name ) . '</span>';
89 }
90
91 return isset( $end ) ? array_merge( $columns, $end ) : $columns;
92 }
93
94 /**
95 * Returns the first language column in posts, pages, media, categories and tags tables.
96 *
97 * @since 0.9
98 *
99 * @return string First language column name.
100 */
101 protected function get_first_language_column(): string {
102 $languages = $this->model->get_languages_list();
103 $language = reset( $languages );
104 return ! empty( $language ) ? "language_{$language->slug}" : '';
105 }
106
107 /**
108 * Hides the column for the filtered language.
109 *
110 * @since 2.7
111 *
112 * @param string[] $hidden Array of hidden columns.
113 * @return string[]
114 */
115 public function hidden_columns( $hidden ) {
116 if ( ! empty( $this->filter_lang ) ) {
117 $hidden[] = 'language_' . $this->filter_lang->slug;
118 }
119 return $hidden;
120 }
121
122 /**
123 * Adds the language and translations columns ( before the comments column ) in the posts, pages and media library tables.
124 *
125 * @since 0.1
126 *
127 * @param string[] $columns List of posts table columns.
128 * @return string[] Modified list of columns.
129 */
130 public function add_post_column( $columns ) {
131 return $this->add_column( (array) $columns, 'comments' );
132 }
133
134 /**
135 * Fills the language and translations columns in the posts, pages and media library tables
136 * take care that when doing ajax inline edit, the post may not be updated in database yet.
137 *
138 * @since 0.1
139 *
140 * @param string $column Column name.
141 * @param int $post_id Post ID.
142 * @return void
143 */
144 public function post_column( $column, $post_id ): void {
145 if ( ! str_starts_with( $column, 'language_' ) ) {
146 return;
147 }
148
149 $post = get_post( (int) $post_id );
150
151 if ( ! $post instanceof WP_Post ) {
152 // Should not happen.
153 return;
154 }
155
156 $inline = isset( $_POST['inline_lang_choice'], $_REQUEST['_inline_edit'] ) && is_string( $_REQUEST['_inline_edit'] ) && wp_verify_nonce( $_REQUEST['_inline_edit'], 'inlineeditnonce' );
157 $lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->post->get_language( $post->ID );
158
159 if ( empty( $lang ) ) {
160 return;
161 }
162
163 $language = $this->model->get_language( substr( $column, 9 ) );
164
165 if ( empty( $language ) ) {
166 return;
167 }
168
169 // Hidden field containing the post language for quick edit.
170 if ( $column === $this->get_first_language_column() ) {
171 printf( '<div class="hidden" id="lang_%d">%s</div>', (int) $post->ID, esc_html( $lang->slug ) );
172 }
173
174 $tr_id = $this->model->post->get( $post->ID, $language );
175 $tr_post = $tr_id ? get_post( $tr_id ) : null;
176
177 if ( ! $tr_post instanceof WP_Post ) {
178 // Link to add a new translation: no translation for this language yet, or it doesn't exist anymore.
179 echo $this->links->get_new_post_link_html( $post, $language ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
180 return;
181 }
182
183 // Link to edit (or not) the post or a translation.
184 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
185 }
186
187 /**
188 * Quick edit & bulk edit.
189 *
190 * @since 0.9
191 *
192 * @param string $column Column name.
193 * @return string Unmodified column.
194 */
195 public function quick_edit_custom_box( $column ) {
196 if ( $column !== $this->get_first_language_column() ) {
197 return $column;
198 }
199
200 $elements = $this->model->languages->filter( 'translator' )->get_list();
201 if ( current_filter() == 'bulk_edit_custom_box' ) {
202 array_unshift( $elements, (object) array( 'slug' => -1, 'name' => __( '&mdash; No Change &mdash;', 'polylang' ) ) );
203 }
204
205 $dropdown = new PLL_Walker_Dropdown();
206 printf(
207 '<fieldset class="inline-edit-col-left">
208 <div class="inline-edit-col">
209 <label class="alignleft">
210 <span class="title">%s</span>
211 %s
212 </label>
213 </div>
214 </fieldset>',
215 esc_html__( 'Language', 'polylang' ),
216 $dropdown->walk( $elements, -1, array( 'name' => 'inline_lang_choice', 'id' => '' ) ) // phpcs:ignore WordPress.Security.EscapeOutput
217 );
218
219 return $column;
220 }
221
222 /**
223 * Adds the language column (before the posts column) in the 'Categories' or 'Post Tags' table.
224 *
225 * @since 0.1
226 *
227 * @param string[] $columns List of terms table columns.
228 * @return string[] modified List of columns.
229 */
230 public function add_term_column( $columns ) {
231 $screen = get_current_screen();
232
233 // Avoid displaying languages in screen options when editing a term.
234 if ( $screen instanceof WP_Screen && 'term' === $screen->base ) {
235 return $columns;
236 }
237
238 return $this->add_column( (array) $columns, 'posts' );
239 }
240
241 /**
242 * Fills the language column in the taxonomy terms list table.
243 *
244 * @since 0.1
245 *
246 * @param string $out Column output.
247 * @param string $column Column name.
248 * @param int $term_id Term ID.
249 * @return string
250 */
251 public function term_column( $out, $column, $term_id ) {
252 if ( ! str_starts_with( $column, 'language_' ) ) {
253 return $out;
254 }
255
256 $term_id = (int) $term_id;
257 $inline = isset( $_POST['inline_lang_choice'], $_REQUEST['_inline_edit'] ) && is_string( $_REQUEST['_inline_edit'] ) && wp_verify_nonce( $_REQUEST['_inline_edit'], 'taxinlineeditnonce' );
258 $lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->term->get_language( $term_id );
259
260 if ( empty( $lang ) ) {
261 return $out;
262 }
263
264 if ( isset( $GLOBALS['post_type'] ) ) {
265 $post_type = $GLOBALS['post_type'];
266 } elseif ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
267 $post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification
268 }
269
270 if ( isset( $GLOBALS['taxonomy'] ) ) {
271 $taxonomy = $GLOBALS['taxonomy'];
272 } elseif ( isset( $_REQUEST['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
273 $taxonomy = sanitize_key( $_REQUEST['taxonomy'] ); // phpcs:ignore WordPress.Security.NonceVerification
274 }
275
276 if ( ! isset( $taxonomy, $post_type ) || ! is_string( $post_type ) || ! is_string( $taxonomy ) || ! post_type_exists( $post_type ) || ! taxonomy_exists( $taxonomy ) ) {
277 return $out;
278 }
279
280 $language = $this->model->get_language( substr( $column, 9 ) );
281
282 if ( empty( $language ) ) {
283 return $out;
284 }
285
286 $term = get_term( $term_id, $taxonomy );
287
288 if ( ! $term instanceof WP_Term ) {
289 // Should not happen.
290 return $out;
291 }
292
293 $tr_id = $this->model->term->get( $term->term_id, $language );
294 $tr_term = $tr_id ? get_term( $tr_id, $taxonomy ) : null;
295
296 if ( ! $tr_term instanceof WP_Term ) {
297 // Link to add a new translation: no translation for this language yet, or it doesn't exist anymore.
298 $out .= $this->links->get_new_term_link_html( $term, $post_type, $language );
299 } else {
300 // Link to edit (or not) the term or a translation.
301 $out .= $this->links->get_edit_term_link_html( $tr_term, $post_type, $tr_term->term_id === $term->term_id ? 'list_current' : 'list_translation' );
302 }
303
304 if ( $this->get_first_language_column() === $column ) {
305 // Hidden field containing the term language for quick edit.
306 $out .= sprintf( '<div class="hidden" id="lang_%d">%s</div>', $term->term_id, esc_html( $lang->slug ) );
307
308 /**
309 * Filters the output of the first language column in the terms list table.
310 *
311 * @since 3.7
312 *
313 * @param string $output First language column output.
314 * @param int $term_id Term ID.
315 * @param string $lang Language code.
316 */
317 $out = apply_filters( 'pll_first_language_term_column', $out, $term->term_id, $lang->slug );
318 }
319
320 return $out;
321 }
322
323 /**
324 * Update rows of translated posts when the language is modified in quick edit.
325 *
326 * @since 1.7
327 *
328 * @return void
329 *
330 * @phpstan-return never
331 */
332 public function ajax_update_post_rows(): void {
333 check_ajax_referer( 'inlineeditnonce', '_pll_nonce' );
334
335 if ( ! isset( $_POST['post_type'], $_POST['post_id'], $_POST['screen'] ) ) {
336 wp_die( 0 );
337 }
338
339 if ( ! is_numeric( $_POST['post_id'] ) ) {
340 wp_die( 0 );
341 }
342
343 $post_type_object = get_post_type_object( sanitize_key( $_POST['post_type'] ) );
344
345 if ( empty( $post_type_object ) || ! $this->model->is_translated_post_type( $post_type_object->name ) ) {
346 wp_die( 0 );
347 }
348
349 $wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
350
351 if ( empty( $wp_list_table ) ) {
352 wp_die( 0 );
353 }
354
355 $response = new WP_Ajax_Response();
356
357 // Collect old translations.
358 if ( ! empty( $_POST['translations'] ) && is_string( $_POST['translations'] ) ) {
359 $translations = explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
360 $translations = array_filter( $translations, 'is_numeric' );
361 $translations = array_map( 'intval', $translations );
362 $translations = array_filter( $translations );
363 } else {
364 $translations = array();
365 }
366
367 $translations = array_merge( $translations, $this->model->post->get_translations( (int) $_POST['post_id'] ) );
368 $translations = array_unique( $translations );
369
370 if ( empty( $translations ) ) { // May occur if the modified post has no language.
371 $response->send();
372 }
373
374 /** @var WP_Post[] */
375 $posts = ( new WP_Query() )->query(
376 array(
377 // Do not add `post_status`, as this allows `WP_Query` to handle user capabilities to read private posts.
378 'post__in' => $translations,
379 'post_type' => $post_type_object->name,
380 'posts_per_page' => count( $translations ),
381 'no_found_rows' => true,
382 'orderby' => 'ID',
383 'lang' => '',
384 )
385 );
386
387 foreach ( $posts as $post ) {
388 if ( $post_type_object->hierarchical ) {
389 $level = count( get_ancestors( $post->ID, $post->post_type, 'post_type' ) );
390 } else {
391 $level = 0;
392 }
393
394 ob_start();
395 $wp_list_table->single_row( $post, $level );
396 $data = (string) ob_get_clean();
397
398 $response->add(
399 array(
400 'what' => 'row',
401 'data' => $data,
402 'supplemental' => array( 'post_id' => $post->ID ),
403 )
404 );
405 }
406
407 $response->send();
408 }
409
410 /**
411 * Update rows of translated terms when adding / deleting a translation
412 * or when the language is modified in quick edit.
413 *
414 * @since 1.7
415 *
416 * @return void
417 *
418 * @phpstan-return never
419 */
420 public function ajax_update_term_rows(): void {
421 check_ajax_referer( 'pll_language', '_pll_nonce' );
422
423 if ( ! isset( $_POST['taxonomy'], $_POST['term_id'], $_POST['screen'] ) ) {
424 wp_die( 0 );
425 }
426
427 if ( ! is_numeric( $_POST['term_id'] ) ) {
428 wp_die( 0 );
429 }
430
431 $taxonomy_object = get_taxonomy( sanitize_key( $_POST['taxonomy'] ) );
432
433 if ( empty( $taxonomy_object ) || ! $this->model->is_translated_taxonomy( $taxonomy_object->name ) ) {
434 wp_die( 0 );
435 }
436
437 $wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
438
439 if ( empty( $wp_list_table ) ) {
440 wp_die( 0 );
441 }
442
443 $response = new WP_Ajax_Response();
444
445 // Collect old translations.
446 if ( ! empty( $_POST['translations'] ) && is_string( $_POST['translations'] ) ) {
447 $translations = explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
448 $translations = array_filter( $translations, 'is_numeric' );
449 $translations = array_map( 'intval', $translations );
450 $translations = array_filter( $translations );
451 } else {
452 $translations = array();
453 }
454
455 $translations = array_merge( $translations, $this->model->term->get_translations( (int) $_POST['term_id'] ) );
456 $translations = array_unique( $translations );
457
458 if ( empty( $translations ) ) { // May occur if the modfied term has no language.
459 $response->send();
460 }
461
462 /** @var WP_Term[] */
463 $terms = ( new WP_Term_Query() )->query(
464 array(
465 'include' => $translations,
466 'taxonomy' => $taxonomy_object->name,
467 'hide_empty' => false,
468 'orderby' => 'term_id',
469 'lang' => '',
470 )
471 );
472
473 foreach ( $terms as $term ) {
474 if ( $taxonomy_object->hierarchical ) {
475 $level = count( get_ancestors( $term->term_id, $taxonomy_object->name, 'taxonomy' ) );
476 } else {
477 $level = 0;
478 }
479
480 ob_start();
481 $wp_list_table->single_row( $term, $level );
482 $data = (string) ob_get_clean();
483
484 $response->add(
485 array(
486 'what' => 'row',
487 'data' => $data,
488 'supplemental' => array( 'term_id' => $term->term_id ),
489 )
490 );
491 }
492
493 $response->send();
494 }
495 }
496