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

541 lines 18.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 * 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
16 */
17 public $curlang;
18
19 /**
20 * Language selected in the admin language filter.
21 *
22 * @var PLL_Language
23 */
24 public $filter_lang;
25
26 /**
27 * Preferred language to assign to new contents.
28 *
29 * @var PLL_Language
30 */
31 public $pref_lang;
32
33 /**
34 * @var PLL_Filters_Links
35 */
36 public $filters_links;
37
38 /**
39 * @var PLL_Admin_Links
40 */
41 public $links;
42
43 /**
44 * @var PLL_Admin_Notices
45 */
46 public $notices;
47
48 /**
49 * @var PLL_Admin_Static_Pages
50 */
51 public $static_pages;
52
53 /**
54 * @var PLL_Admin_Default_Term
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'] = __( '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 if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
182 $scripts['post'] = array( array( 'edit', 'upload' ), array( 'jquery', 'wp-ajax-response' ), 0, 1 );
183
184 // Classic editor.
185 if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
186 $scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
187 }
188
189 // Block editor with legacy metabox in WP 5.0+.
190 if ( method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin() ) {
191 $scripts['block-editor'] = array( array( 'post' ), array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
192 }
193 }
194
195 if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
196 $scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), 0, 1 );
197 }
198
199 foreach ( $scripts as $script => $v ) {
200 if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->get_languages_list() ) ) {
201 wp_enqueue_script( 'pll_' . $script, plugins_url( '/js/build/' . $script . $suffix . '.js', POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
202 if ( 'classic-editor' === $script || 'block-editor' === $script ) {
203 wp_set_script_translations( 'pll_' . $script, 'polylang' );
204 }
205 }
206 }
207
208 wp_register_style( 'polylang_admin', plugins_url( '/css/build/admin' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
209 wp_enqueue_style( 'polylang_dialog', plugins_url( '/css/build/dialog' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'polylang_admin' ), POLYLANG_VERSION );
210
211 $this->localize_scripts();
212 }
213
214 /**
215 * Enqueue scripts to the WP Customizer.
216 *
217 * @since 2.4.0
218 *
219 * @return void
220 */
221 public function customize_controls_enqueue_scripts() {
222 if ( $this->model->get_languages_list() ) {
223 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
224 wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
225 $this->localize_scripts();
226 }
227 }
228
229 /**
230 * Localize scripts.
231 *
232 * @since 2.4.0
233 *
234 * @return void
235 */
236 public function localize_scripts() {
237 if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
238 wp_localize_script(
239 'pll_widgets',
240 'pll_widgets',
241 array(
242 'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
243 )
244 );
245 }
246 }
247
248 /**
249 * Sets pll_ajax_backend on all backend ajax request
250 * The final goal is to detect if an ajax request is made on admin or frontend
251 *
252 * Takes care to various situations:
253 * when the ajax request has no options.data thanks to ScreenfeedFr
254 * see: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected
255 * when options.data is a json string
256 * see: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels
257 * when options.data is an empty string (GET request with the method 'load')
258 * see: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation
259 *
260 * @since 1.4
261 *
262 * @return void
263 */
264 public function admin_print_footer_scripts() {
265 global $post_ID, $tag_ID;
266
267 $params = array( 'pll_ajax_backend' => 1 );
268 if ( ! empty( $post_ID ) ) {
269 $params = array_merge( $params, array( 'pll_post_id' => (int) $post_ID ) );
270 }
271
272 if ( ! empty( $tag_ID ) ) {
273 $params = array_merge( $params, array( 'pll_term_id' => (int) $tag_ID ) );
274 }
275
276 $str = http_build_query( $params );
277 $arr = wp_json_encode( $params );
278 ?>
279 <script type="text/javascript">
280 if (typeof jQuery != 'undefined') {
281 jQuery(
282 function( $ ){
283 $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) {
284 if ( -1 != options.url.indexOf( ajaxurl ) || -1 != ajaxurl.indexOf( options.url ) ) {
285
286 function addPolylangParametersAsString() {
287 if ( 'undefined' === typeof options.data || '' === options.data.trim() ) {
288 // Only Polylang data need to be send. So it could be as a simple query string.
289 options.data = '<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
290 } else {
291 /*
292 * In some cases data could be a JSON string like in third party plugins.
293 * So we need not to break their process by adding polylang parameters as valid JSON datas.
294 */
295 try {
296 options.data = JSON.stringify( Object.assign( JSON.parse( options.data ), <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> ) );
297 } catch( exception ) {
298 // Add Polylang data to the existing query string.
299 options.data = options.data + '&<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
300 }
301 }
302 }
303
304 /*
305 * options.processData set to true is the default jQuery process where the data is converted in a query string by using jQuery.param().
306 * This step is done before applying filters. Thus here the options.data is already a string in this case.
307 * @See https://github.com/jquery/jquery/blob/3.5.1/src/ajax.js#L563-L569 jQuery ajax function.
308 * It is the most case WordPress send ajax request this way however third party plugins or themes could be send JSON string.
309 * Use JSON format is recommended in jQuery.param() documentation to be able to send complex data structures.
310 * @See https://api.jquery.com/jquery.param/ jQuery param function.
311 */
312 if ( options.processData ) {
313 addPolylangParametersAsString();
314 } else {
315 /*
316 * If options.processData is set to false data could be undefined or pass as a string.
317 * So data as to be processed as if options.processData is set to true.
318 */
319 if ( 'undefined' === typeof options.data || 'string' === typeof options.data ) {
320 addPolylangParametersAsString();
321 } else {
322 // Otherwise options.data is probably an object.
323 options.data = Object.assign( options.data || {} , <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> );
324 }
325 }
326 }
327 });
328 }
329 );
330 }
331 </script>
332 <?php
333 }
334
335 /**
336 * Sets the admin current language, used to filter the content
337 *
338 * @since 2.0
339 *
340 * @return void
341 */
342 public function set_current_language() {
343 $this->curlang = $this->filter_lang;
344
345 // Edit Post
346 if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
347 $this->curlang = $lang;
348 } 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
349 $this->curlang = $lang;
350 } 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
351 $this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
352 }
353
354 // Edit Term
355 elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
356 $this->curlang = $lang;
357 } 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
358 if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
359 $this->curlang = $lang;
360 } elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
361 $this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
362 } elseif ( empty( $this->curlang ) ) {
363 $this->curlang = $this->pref_lang;
364 }
365 }
366
367 // Ajax
368 if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
369 $this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
370 }
371
372 /**
373 * Filters the current language used by Polylang in the admin context.
374 *
375 * @since 3.2
376 *
377 * @param PLL_Language|false|null $curlang Instance of the current language.
378 * @param PLL_Admin_Base $polylang Instance of the main Polylang's object.
379 */
380 $this->curlang = apply_filters( 'pll_admin_current_language', $this->curlang, $this );
381
382 // Inform that the admin language has been set.
383 if ( $this->curlang instanceof PLL_Language ) {
384 /** This action is documented in frontend/choose-lang.php */
385 do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
386 } else {
387 /** This action is documented in include/class-polylang.php */
388 do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
389 }
390 }
391
392 /**
393 * Defines the backend language and the admin language filter based on user preferences
394 *
395 * @since 1.2.3
396 *
397 * @return void
398 */
399 public function init_user() {
400 // Language for admin language filter: may be empty
401 // $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter
402 // We intentionally don't use a nonce to update the language filter
403 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
404 update_user_meta( $user_id, 'pll_filter_content', ( $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ) ) ? $lang->slug : '' ); // phpcs:ignore WordPress.Security.NonceVerification
405 }
406
407 $this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
408
409 // Set preferred language for use when saving posts and terms: must not be empty
410 $this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_language( $this->options['default_lang'] ) : $this->filter_lang;
411
412 /**
413 * Filters the preferred language on admin side.
414 * The preferred language is used for example to determine the language of a new post.
415 *
416 * @since 1.2.3
417 *
418 * @param PLL_Language $pref_lang Preferred language.
419 */
420 $this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
421
422 $this->set_current_language();
423
424 // Plugin i18n, only needed for backend.
425 load_plugin_textdomain( 'polylang' );
426 }
427
428 /**
429 * Avoids parsing a tax query when all languages are requested
430 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
431 *
432 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
433 *
434 * @since 1.6.5
435 *
436 * @param array $qvars
437 * @return array
438 */
439 public function request( $qvars ) {
440 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
441 unset( $qvars['lang'] );
442 }
443
444 return $qvars;
445 }
446
447 /**
448 * Adds the languages list in admin bar for the admin languages filter.
449 *
450 * @since 0.9
451 *
452 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
453 * @return void
454 */
455 public function admin_bar_menu( $wp_admin_bar ) {
456 $all_item = (object) array(
457 'slug' => 'all',
458 'name' => __( 'Show all languages', 'polylang' ),
459 'flag' => '<span class="ab-icon"></span>',
460 );
461
462 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
463
464 $title = sprintf(
465 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
466 'all' === $selected->slug ? '' : sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ),
467 __( 'Filters content by language', 'polylang' ),
468 esc_html( $selected->name )
469 );
470
471 /**
472 * Filters the admin languages filter submenu items
473 *
474 * @since 2.6
475 *
476 * @param array $items The admin languages filter submenu items.
477 */
478 $items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
479
480 $menu = array(
481 'id' => 'languages',
482 'title' => $selected->flag . $title,
483 'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
484 'meta' => array(
485 'title' => __( 'Filters content by language', 'polylang' ),
486 ),
487 );
488
489 if ( 'all' !== $selected->slug ) {
490 $menu['meta']['class'] = 'pll-filtered-languages';
491 }
492
493 if ( ! empty( $items ) ) {
494 $wp_admin_bar->add_menu( $menu );
495 }
496
497 foreach ( $items as $lang ) {
498 if ( $selected->slug === $lang->slug ) {
499 continue;
500 }
501
502 $wp_admin_bar->add_menu(
503 array(
504 'parent' => 'languages',
505 'id' => $lang->slug,
506 'title' => $lang->flag . esc_html( $lang->name ),
507 'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
508 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
509 )
510 );
511 }
512 }
513
514 /**
515 * Remove the customize submenu when using a block theme.
516 *
517 * WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
518 * As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
519 * unless another plugin than Polylang interacts with the Customizer.
520 *
521 * @since 3.2
522 *
523 * @return void
524 */
525 public function remove_customize_submenu() {
526 if ( ! $this->should_customize_menu_be_removed() ) {
527 return;
528 }
529
530 global $submenu;
531
532 if ( ! empty( $submenu['themes.php'] ) ) {
533 foreach ( $submenu['themes.php'] as $submenu_item ) {
534 if ( 'customize' === $submenu_item[1] ) {
535 remove_submenu_page( 'themes.php', $submenu_item[2] );
536 }
537 }
538 }
539 }
540 }
541