PluginProbe
Polylang / 3.3.2
Polylang v3.3.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 / admin / admin-base.php

admin-base.php in Polylang 3.3.2, at admin/admin-base.php

567 lines 19.5 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 * Setup features available on all admin pages.
8 *
9 * @since 1.8
10 */
11 abstract class PLL_Admin_Base extends PLL_Base {
12 /**
13 * Current language (used to filter the content).
14 *
15 * @var PLL_Language|null
16 */
17 public $curlang;
18
19 /**
20 * Language selected in the admin language filter.
21 *
22 * @var PLL_Language|null
23 */
24 public $filter_lang;
25
26 /**
27 * Preferred language to assign to new contents.
28 *
29 * @var PLL_Language|null
30 */
31 public $pref_lang;
32
33 /**
34 * @var PLL_Filters_Links|null
35 */
36 public $filters_links;
37
38 /**
39 * @var PLL_Admin_Links|null
40 */
41 public $links;
42
43 /**
44 * @var PLL_Admin_Notices|null
45 */
46 public $notices;
47
48 /**
49 * @var PLL_Admin_Static_Pages|null
50 */
51 public $static_pages;
52
53 /**
54 * @var PLL_Admin_Default_Term|null
55 */
56 public $default_term;
57
58 /**
59 * Setups actions needed on all admin pages.
60 *
61 * @since 1.8
62 *
63 * @param PLL_Links_Model $links_model Reference to the links model.
64 */
65 public function __construct( &$links_model ) {
66 parent::__construct( $links_model );
67
68 // Adds the link to the languages panel in the WordPress admin menu
69 add_action( 'admin_menu', array( $this, 'add_menus' ) );
70
71 add_action( 'admin_menu', array( $this, 'remove_customize_submenu' ) );
72
73 // Setup js scripts and css styles
74 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
75 add_action( 'admin_print_footer_scripts', array( $this, 'admin_print_footer_scripts' ), 0 ); // High priority in case an ajax request is sent by an immediately invoked function
76
77 add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
78 }
79
80 /**
81 * Setups filters and action needed on all admin pages and on plugins page
82 * Loads the settings pages or the filters base on the request
83 *
84 * @since 1.2
85 */
86 public function init() {
87 parent::init();
88
89 $this->notices = new PLL_Admin_Notices( $this );
90
91 $this->default_term = new PLL_Admin_Default_Term( $this );
92 $this->default_term->add_hooks();
93
94 if ( ! $this->model->get_languages_list() ) {
95 return;
96 }
97
98 $this->links = new PLL_Admin_Links( $this ); // FIXME needed here ?
99 $this->static_pages = new PLL_Admin_Static_Pages( $this ); // FIXME needed here ?
100 $this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ?
101
102 // Filter admin language for users
103 // We must not call user info before WordPress defines user roles in wp-settings.php
104 add_action( 'setup_theme', array( $this, 'init_user' ) );
105 add_filter( 'request', array( $this, 'request' ) );
106
107 // Adds the languages in admin bar
108 add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 ); // 100 determines the position
109 }
110
111 /**
112 * Adds the link to the languages panel in the WordPress admin menu
113 *
114 * @since 0.1
115 *
116 * @return void
117 */
118 public function add_menus() {
119 global $admin_page_hooks;
120
121 // Prepare the list of tabs
122 $tabs = array( 'lang' => __( 'Languages', 'polylang' ) );
123
124 // Only if at least one language has been created
125 if ( $this->model->get_languages_list() ) {
126 $tabs['strings'] = __( 'Translations', 'polylang' );
127 }
128
129 $tabs['settings'] = __( 'Settings', 'polylang' );
130
131 /**
132 * Filter the list of tabs in Polylang settings
133 *
134 * @since 1.5.1
135 *
136 * @param array $tabs list of tab names
137 */
138 $tabs = apply_filters( 'pll_settings_tabs', $tabs );
139
140 $parent = '';
141
142 foreach ( $tabs as $tab => $title ) {
143 $page = 'lang' === $tab ? 'mlang' : "mlang_$tab";
144 if ( empty( $parent ) ) {
145 $parent = $page;
146 add_menu_page( $title, __( 'Languages', 'polylang' ), 'manage_options', $page, '__return_null', 'dashicons-translation' );
147 $admin_page_hooks[ $page ] = 'languages'; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
148 }
149
150 add_submenu_page( $parent, $title, $title, 'manage_options', $page, array( $this, 'languages_page' ) );
151 }
152 }
153
154 /**
155 * Setup js scripts & css styles ( only on the relevant pages )
156 *
157 * @since 0.6
158 *
159 * @return void
160 */
161 public function admin_enqueue_scripts() {
162 $screen = get_current_screen();
163 if ( empty( $screen ) ) {
164 return;
165 }
166
167 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
168
169 /*
170 * For each script:
171 * 0 => the pages on which to load the script
172 * 1 => the scripts it needs to work
173 * 2 => 1 if loaded even if languages have not been defined yet, 0 otherwise
174 * 3 => 1 if loaded in footer
175 */
176 $scripts = array(
177 'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), 0, 0 ),
178 'widgets' => array( array( 'widgets' ), array( 'jquery' ), 0, 0 ),
179 );
180
181 $block_screens = array( 'widgets', 'site-editor' );
182
183 if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
184 $scripts['post'] = array( array( 'edit', 'upload' ), array( 'jquery', 'wp-ajax-response' ), 0, 1 );
185
186 // Classic editor.
187 if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
188 $scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
189 }
190
191 // Block editor with legacy metabox in WP 5.0+.
192 $block_screens[] = 'post';
193 }
194
195 if ( $this->is_block_editor( $screen ) ) {
196 $scripts['block-editor'] = array( $block_screens, array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
197 }
198
199 if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
200 $scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), 0, 1 );
201 }
202
203 foreach ( $scripts as $script => $v ) {
204 if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->get_languages_list() ) ) {
205 wp_enqueue_script( 'pll_' . $script, plugins_url( '/js/build/' . $script . $suffix . '.js', POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
206 if ( 'classic-editor' === $script || 'block-editor' === $script ) {
207 wp_set_script_translations( 'pll_' . $script, 'polylang' );
208 }
209 }
210 }
211
212 wp_register_style( 'polylang_admin', plugins_url( '/css/build/admin' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
213 wp_enqueue_style( 'polylang_dialog', plugins_url( '/css/build/dialog' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'polylang_admin' ), POLYLANG_VERSION );
214
215 $this->add_inline_scripts();
216 }
217
218 /**
219 * Tells whether or not the given screen is block editor kind.
220 * e.g. widget, site or post editor.
221 *
222 * @since 3.3
223 *
224 * @param WP_Screen $screen Screen object.
225 * @return bool True if the screen is a block editor, false otherwise.
226 */
227 protected function is_block_editor( $screen ) {
228 return method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin();
229 }
230
231 /**
232 * Enqueue scripts to the WP Customizer.
233 *
234 * @since 2.4.0
235 *
236 * @return void
237 */
238 public function customize_controls_enqueue_scripts() {
239 if ( $this->model->get_languages_list() ) {
240 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
241 wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
242 $this->add_inline_scripts();
243 }
244 }
245
246 /**
247 * Adds inline scripts to set the default language in JS
248 * and localizes scripts.
249 *
250 * @since 3.3
251 *
252 * @return void
253 */
254 private function add_inline_scripts() {
255 if ( wp_script_is( 'pll_block-editor', 'enqueued' ) ) {
256 $default_lang_script = 'const pllDefaultLanguage = "' . $this->options['default_lang'] . '";';
257 wp_add_inline_script(
258 'pll_block-editor',
259 $default_lang_script,
260 'before'
261 );
262 }
263 if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
264 wp_localize_script(
265 'pll_widgets',
266 'pll_widgets',
267 array(
268 'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
269 )
270 );
271 }
272 }
273
274 /**
275 * Sets pll_ajax_backend on all backend ajax request
276 * The final goal is to detect if an ajax request is made on admin or frontend
277 *
278 * Takes care to various situations:
279 * when the ajax request has no options.data thanks to ScreenfeedFr
280 * see: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected
281 * when options.data is a json string
282 * see: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels
283 * when options.data is an empty string (GET request with the method 'load')
284 * see: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation
285 *
286 * @since 1.4
287 *
288 * @return void
289 */
290 public function admin_print_footer_scripts() {
291 global $post_ID, $tag_ID;
292
293 $params = array( 'pll_ajax_backend' => 1 );
294 if ( ! empty( $post_ID ) ) {
295 $params = array_merge( $params, array( 'pll_post_id' => (int) $post_ID ) );
296 }
297
298 if ( ! empty( $tag_ID ) ) {
299 $params = array_merge( $params, array( 'pll_term_id' => (int) $tag_ID ) );
300 }
301
302 $str = http_build_query( $params );
303 $arr = wp_json_encode( $params );
304 ?>
305 <script type="text/javascript">
306 if (typeof jQuery != 'undefined') {
307 jQuery(
308 function( $ ){
309 $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) {
310 if ( -1 != options.url.indexOf( ajaxurl ) || -1 != ajaxurl.indexOf( options.url ) ) {
311
312 function addPolylangParametersAsString() {
313 if ( 'undefined' === typeof options.data || '' === options.data.trim() ) {
314 // Only Polylang data need to be send. So it could be as a simple query string.
315 options.data = '<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
316 } else {
317 /*
318 * In some cases data could be a JSON string like in third party plugins.
319 * So we need not to break their process by adding polylang parameters as valid JSON datas.
320 */
321 try {
322 options.data = JSON.stringify( Object.assign( JSON.parse( options.data ), <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> ) );
323 } catch( exception ) {
324 // Add Polylang data to the existing query string.
325 options.data = options.data + '&<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
326 }
327 }
328 }
329
330 /*
331 * options.processData set to true is the default jQuery process where the data is converted in a query string by using jQuery.param().
332 * This step is done before applying filters. Thus here the options.data is already a string in this case.
333 * @See https://github.com/jquery/jquery/blob/3.5.1/src/ajax.js#L563-L569 jQuery ajax function.
334 * It is the most case WordPress send ajax request this way however third party plugins or themes could be send JSON string.
335 * Use JSON format is recommended in jQuery.param() documentation to be able to send complex data structures.
336 * @See https://api.jquery.com/jquery.param/ jQuery param function.
337 */
338 if ( options.processData ) {
339 addPolylangParametersAsString();
340 } else {
341 /*
342 * If options.processData is set to false data could be undefined or pass as a string.
343 * So data as to be processed as if options.processData is set to true.
344 */
345 if ( 'undefined' === typeof options.data || 'string' === typeof options.data ) {
346 addPolylangParametersAsString();
347 } else {
348 // Otherwise options.data is probably an object.
349 options.data = Object.assign( options.data || {} , <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> );
350 }
351 }
352 }
353 });
354 }
355 );
356 }
357 </script>
358 <?php
359 }
360
361 /**
362 * Sets the admin current language, used to filter the content
363 *
364 * @since 2.0
365 *
366 * @return void
367 */
368 public function set_current_language() {
369 $this->curlang = $this->filter_lang;
370
371 // Edit Post
372 if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
373 $this->curlang = $lang;
374 } elseif ( 'post.php' === $GLOBALS['pagenow'] && isset( $_GET['post'] ) && $this->model->is_translated_post_type( get_post_type( (int) $_GET['post'] ) ) && $lang = $this->model->post->get_language( (int) $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
375 $this->curlang = $lang;
376 } elseif ( 'post-new.php' === $GLOBALS['pagenow'] && ( empty( $_GET['post_type'] ) || $this->model->is_translated_post_type( sanitize_key( $_GET['post_type'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
377 $this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
378 }
379
380 // Edit Term
381 elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
382 $this->curlang = $lang;
383 } elseif ( in_array( $GLOBALS['pagenow'], array( 'edit-tags.php', 'term.php' ) ) && isset( $_GET['taxonomy'] ) && $this->model->is_translated_taxonomy( sanitize_key( $_GET['taxonomy'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
384 if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
385 $this->curlang = $lang;
386 } elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
387 $this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
388 } elseif ( empty( $this->curlang ) ) {
389 $this->curlang = $this->pref_lang;
390 }
391 }
392
393 // Ajax
394 if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
395 $this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
396 }
397
398 /**
399 * Filters the current language used by Polylang in the admin context.
400 *
401 * @since 3.2
402 *
403 * @param PLL_Language|false|null $curlang Instance of the current language.
404 * @param PLL_Admin_Base $polylang Instance of the main Polylang's object.
405 */
406 $this->curlang = apply_filters( 'pll_admin_current_language', $this->curlang, $this );
407
408 // Inform that the admin language has been set.
409 if ( $this->curlang instanceof PLL_Language ) {
410 /** This action is documented in frontend/choose-lang.php */
411 do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
412 } else {
413 /** This action is documented in include/class-polylang.php */
414 do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
415 }
416 }
417
418 /**
419 * Defines the backend language and the admin language filter based on user preferences
420 *
421 * @since 1.2.3
422 *
423 * @return void
424 */
425 public function init_user() {
426 // Language for admin language filter: may be empty
427 // $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter
428 // We intentionally don't use a nonce to update the language filter
429 if ( ! wp_doing_ajax() && ! empty( $_GET['lang'] ) && ! is_numeric( sanitize_key( $_GET['lang'] ) ) && current_user_can( 'edit_user', $user_id = get_current_user_id() ) ) { // phpcs:ignore WordPress.Security.NonceVerification
430 update_user_meta( $user_id, 'pll_filter_content', ( $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ) ) ? $lang->slug : '' ); // phpcs:ignore WordPress.Security.NonceVerification
431 }
432
433 $this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
434
435 // Set preferred language for use when saving posts and terms: must not be empty
436 $this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_language( $this->options['default_lang'] ) : $this->filter_lang;
437
438 /**
439 * Filters the preferred language on admin side.
440 * The preferred language is used for example to determine the language of a new post.
441 *
442 * @since 1.2.3
443 *
444 * @param PLL_Language $pref_lang Preferred language.
445 */
446 $this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
447
448 $this->set_current_language();
449
450 // Plugin i18n, only needed for backend.
451 load_plugin_textdomain( 'polylang' );
452 }
453
454 /**
455 * Avoids parsing a tax query when all languages are requested
456 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
457 *
458 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
459 *
460 * @since 1.6.5
461 *
462 * @param array $qvars
463 * @return array
464 */
465 public function request( $qvars ) {
466 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
467 unset( $qvars['lang'] );
468 }
469
470 return $qvars;
471 }
472
473 /**
474 * Adds the languages list in admin bar for the admin languages filter.
475 *
476 * @since 0.9
477 *
478 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
479 * @return void
480 */
481 public function admin_bar_menu( $wp_admin_bar ) {
482 $all_item = (object) array(
483 'slug' => 'all',
484 'name' => __( 'Show all languages', 'polylang' ),
485 'flag' => '<span class="ab-icon"></span>',
486 );
487
488 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
489
490 $title = sprintf(
491 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
492 'all' === $selected->slug ? '' : sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ),
493 __( 'Filters content by language', 'polylang' ),
494 esc_html( $selected->name )
495 );
496
497 /**
498 * Filters the admin languages filter submenu items
499 *
500 * @since 2.6
501 *
502 * @param array $items The admin languages filter submenu items.
503 */
504 $items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
505
506 $menu = array(
507 'id' => 'languages',
508 'title' => $selected->flag . $title,
509 'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
510 'meta' => array(
511 'title' => __( 'Filters content by language', 'polylang' ),
512 ),
513 );
514
515 if ( 'all' !== $selected->slug ) {
516 $menu['meta']['class'] = 'pll-filtered-languages';
517 }
518
519 if ( ! empty( $items ) ) {
520 $wp_admin_bar->add_menu( $menu );
521 }
522
523 foreach ( $items as $lang ) {
524 if ( $selected->slug === $lang->slug ) {
525 continue;
526 }
527
528 $wp_admin_bar->add_menu(
529 array(
530 'parent' => 'languages',
531 'id' => $lang->slug,
532 'title' => $lang->flag . esc_html( $lang->name ),
533 'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
534 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
535 )
536 );
537 }
538 }
539
540 /**
541 * Remove the customize submenu when using a block theme.
542 *
543 * WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
544 * As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
545 * unless another plugin than Polylang interacts with the Customizer.
546 *
547 * @since 3.2
548 *
549 * @return void
550 */
551 public function remove_customize_submenu() {
552 if ( ! $this->should_customize_menu_be_removed() ) {
553 return;
554 }
555
556 global $submenu;
557
558 if ( ! empty( $submenu['themes.php'] ) ) {
559 foreach ( $submenu['themes.php'] as $submenu_item ) {
560 if ( 'customize' === $submenu_item[1] ) {
561 remove_submenu_page( 'themes.php', $submenu_item[2] );
562 }
563 }
564 }
565 }
566 }
567