PluginProbe
Polylang / 3.4.5
Polylang v3.4.5
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.4.5, at admin/admin-base.php

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