PluginProbe
Polylang / 3.6.5
Polylang v3.6.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.6.5, at admin/admin-base.php

576 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 => true if loaded even if languages have not been defined yet, false otherwise
177 * 3 => true if loaded in footer
178 */
179 $scripts = array(
180 'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), false, false ),
181 'widgets' => array( array( 'widgets' ), array( 'jquery' ), false, false ),
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' ), false, true );
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' ), false, true );
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' ), false, true );
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' ), false, true );
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>
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 data.
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
463 /**
464 * Avoids parsing a tax query when all languages are requested
465 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
466 *
467 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
468 *
469 * @since 1.6.5
470 *
471 * @param array $qvars The array of requested query variables.
472 * @return array
473 */
474 public function request( $qvars ) {
475 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
476 unset( $qvars['lang'] );
477 }
478
479 return $qvars;
480 }
481
482 /**
483 * Adds the languages list in admin bar for the admin languages filter.
484 *
485 * @since 0.9
486 *
487 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
488 * @return void
489 */
490 public function admin_bar_menu( $wp_admin_bar ) {
491 $all_item = (object) array(
492 'slug' => 'all',
493 'name' => __( 'Show all languages', 'polylang' ),
494 'flag' => '<span class="ab-icon"></span>',
495 );
496
497 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
498
499 $title = sprintf(
500 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
501 $selected instanceof PLL_Language ? sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ) : '',
502 __( 'Filters content by language', 'polylang' ),
503 esc_html( $selected->name )
504 );
505
506 /**
507 * Filters the admin languages filter submenu items
508 *
509 * @since 2.6
510 *
511 * @param array $items The admin languages filter submenu items.
512 */
513 $items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
514
515 $menu = array(
516 'id' => 'languages',
517 'title' => $selected->flag . $title,
518 'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
519 'meta' => array(
520 'title' => __( 'Filters content by language', 'polylang' ),
521 ),
522 );
523
524 if ( 'all' !== $selected->slug ) {
525 $menu['meta']['class'] = 'pll-filtered-languages';
526 }
527
528 if ( ! empty( $items ) ) {
529 $wp_admin_bar->add_menu( $menu );
530 }
531
532 foreach ( $items as $lang ) {
533 if ( $selected->slug === $lang->slug ) {
534 continue;
535 }
536
537 $wp_admin_bar->add_menu(
538 array(
539 'parent' => 'languages',
540 'id' => $lang->slug,
541 'title' => $lang->flag . esc_html( $lang->name ),
542 'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
543 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
544 )
545 );
546 }
547 }
548
549 /**
550 * Remove the customize submenu when using a block theme.
551 *
552 * WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
553 * As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
554 * unless another plugin than Polylang interacts with the Customizer.
555 *
556 * @since 3.2
557 *
558 * @return void
559 */
560 public function remove_customize_submenu() {
561 if ( ! $this->should_customize_menu_be_removed() ) {
562 return;
563 }
564
565 global $submenu;
566
567 if ( ! empty( $submenu['themes.php'] ) ) {
568 foreach ( $submenu['themes.php'] as $submenu_item ) {
569 if ( 'customize' === $submenu_item[1] ) {
570 remove_submenu_page( 'themes.php', $submenu_item[2] );
571 }
572 }
573 }
574 }
575 }
576