PluginProbe
Polylang / trunk
Polylang vtrunk
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-base.php

admin-base.php in Polylang trunk, at src/admin/admin-base.php

750 lines 23.9 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 use WP_Syntex\Polylang\Capabilities\Capabilities;
7
8 /**
9 * Setup features available on all admin pages.
10 *
11 * @since 1.8
12 */
13 abstract class PLL_Admin_Base extends PLL_Base {
14 /**
15 * @since 3.8
16 */
17 public const SCREEN_PREFIX = 'languages';
18
19 /**
20 * Current language (used to filter the content).
21 *
22 * @var PLL_Language|null
23 */
24 public $curlang;
25
26 /**
27 * Language selected in the admin language filter.
28 *
29 * @var PLL_Language|null
30 */
31 public $filter_lang;
32
33 /**
34 * Preferred language to assign to new contents.
35 *
36 * @var PLL_Language|null
37 */
38 public $pref_lang;
39
40 /**
41 * @var PLL_Filters_Links|null
42 */
43 public $filters_links;
44
45 /**
46 * @var PLL_Admin_Links|null
47 */
48 public $links;
49
50 /**
51 * @var PLL_Admin_Notices|null
52 */
53 public $notices;
54
55 /**
56 * @var PLL_Admin_Static_Pages|null
57 */
58 public $static_pages;
59
60 /**
61 * @var PLL_Admin_Default_Term|null
62 */
63 public $default_term;
64
65 /**
66 * Setups actions needed on all admin pages.
67 *
68 * @since 1.8
69 *
70 * @param PLL_Links_Model $links_model Reference to the links model.
71 */
72 public function __construct( &$links_model ) {
73 parent::__construct( $links_model );
74
75 // Adds the link to the languages panel in the WordPress admin menu
76 add_action( 'admin_menu', array( $this, 'add_menus' ) );
77
78 add_action( 'admin_menu', array( $this, 'remove_customize_submenu' ) );
79
80 // Setup js scripts and css styles
81 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 0 ); // High priority in case an ajax request is sent by an immediately invoked function
82
83 add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
84
85 // Early instantiated to be able to correctly initialize language properties.
86 $this->static_pages = new PLL_Admin_Static_Pages( $this );
87 $this->model->set_languages_ready();
88 }
89
90 /**
91 * Setups filters and action needed on all admin pages and on plugins page
92 * Loads the settings pages or the filters base on the request
93 *
94 * @since 1.2
95 */
96 public function init() {
97 parent::init();
98
99 $this->notices = new PLL_Admin_Notices( $this );
100
101 $this->default_term = new PLL_Admin_Default_Term( $this );
102 $this->default_term->add_hooks();
103
104 if ( ! $this->model->has_languages() ) {
105 return;
106 }
107
108 $this->links = new PLL_Admin_Links( $this ); // FIXME needed here ?
109 $this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ?
110
111 // Filter admin language for users
112 // We must not call user info before WordPress defines user roles in wp-settings.php
113 add_action( 'setup_theme', array( $this, 'init_user' ) );
114 add_filter( 'request', array( $this, 'request' ) );
115
116 // Adds the languages in admin bar
117 add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 ); // 100 determines the position
118 }
119
120 /**
121 * Adds links to Polylang's admin panels to the WordPress admin menu.
122 *
123 * @since 0.1
124 *
125 * @return void
126 */
127 public function add_menus(): void {
128 global $admin_page_hooks;
129
130 $parent = '';
131 $first_tab = '';
132
133 foreach ( $this->get_menu_items() as $tab => $title ) {
134 $page = self::get_screen_slug( $tab );
135 $capa = $this->get_menu_capability( $tab );
136
137 if ( empty( $parent ) ) {
138 $parent = $page;
139 $first_tab = $tab;
140
141 /*
142 * WP actually doesn't care about the user capability used here, as long as it has sub-menus: it will
143 * use the ones from the sub-menus. See `_wp_menu_output()`.
144 * Ex: a user with `manage_translations` will still be able to access the Translations page, even if the
145 * main menu has `manage_options`.
146 */
147 add_menu_page( $title, __( 'Languages', 'polylang' ), $capa, $parent, '__return_null', 'dashicons-translation' );
148 $admin_page_hooks[ $parent ] = self::SCREEN_PREFIX; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
149 }
150
151 add_submenu_page( $parent, $title, $title, $capa, $page, array( $this, 'languages_page' ) );
152 }
153
154 /*
155 * Get rid of the `toplevel` prefix in hook names.
156 *
157 * In the WP admin, if an admin screen is the first of its menu (like the PLL's "Languages" screen), the hooks
158 * fired in the screen get a `toplevel` prefix (ex: `toplevel_page_mlang`) while all the other screens get a
159 * slug based on the parent screen title (ex: `languages_page_mlang_strings`, where `languages` is the parent
160 * screen's slug). This will not prevent the `toplevel` hooks to fire, but it will fire the `languages` hooks in
161 * addition: this way, screens can be removed or moved around without the need of hooking both prefixes: using
162 * the hooks with the `languages` prefix will work in both cases.
163 *
164 * @see get_plugin_page_hookname()
165 */
166 foreach ( array( 'load-', 'admin_print_styles-', 'admin_print_scripts-', 'admin_head-', '', 'admin_print_footer_scripts-', 'admin_footer-' ) as $prefix ) {
167 add_action(
168 "{$prefix}toplevel_page_{$parent}",
169 static function () use ( $prefix, $first_tab ) {
170 do_action( $prefix . self::get_screen_id( $first_tab ) );
171 }
172 );
173 }
174
175 /*
176 * Ensure a common CSS class to the `<body>` tag.
177 *
178 * Due to the `toplevel` "issue" described earlier, the CSS class `toplevel_page_mlang` (for example) is added
179 * to the body. This adds a class with the `languages` prefix. This ensures we have a common CSS class, even if
180 * the screen is moved to the 1st position in the menu.
181 */
182 add_action(
183 // Target the screen in 1st position only.
184 "admin_head-toplevel_page_{$parent}",
185 static function () use ( $first_tab ) {
186 add_filter(
187 'admin_body_class',
188 static function ( $admin_body_classes ) use ( $first_tab ) {
189 return $admin_body_classes . ' ' . self::get_screen_id( $first_tab );
190 }
191 );
192 }
193 );
194
195 /**
196 * Also modify the screen ID and base.
197 *
198 * Note: the global variables `$page_hook` and `$hook_suffix` are not changed, their value is still
199 * `toplevel_page_mlang`. Changing them breaks things because we can't filter `get_plugin_page_hookname()`.
200 * This is why the above hooks are still needed.
201 */
202 add_action(
203 'current_screen',
204 static function ( $current_screen ) use ( $parent, $first_tab ) {
205 if ( "toplevel_page_{$parent}" !== $current_screen->id ) {
206 return;
207 }
208
209 $current_screen->id = self::get_screen_id( $first_tab );
210 $current_screen->base = self::get_screen_id( $first_tab );
211 }
212 );
213 }
214
215 /**
216 * Dummy method to display the 3 tabs pages: languages, strings translations, settings.
217 * Overwritten in `PLL_Settings`.
218 *
219 * @since 3.7
220 *
221 * @return void
222 */
223 public function languages_page() {}
224
225 /**
226 * Setup js scripts & css styles ( only on the relevant pages )
227 *
228 * @since 0.6
229 *
230 * @return void
231 */
232 public function admin_enqueue_scripts() {
233 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
234
235 wp_enqueue_script( 'pll_admin', plugins_url( "/js/build/admin{$suffix}.js", POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
236 $inline_script = sprintf( 'let pll_admin = %s;', wp_json_encode( array( 'ajax_filter' => $this->get_ajax_filter_data() ) ) );
237 wp_add_inline_script( 'pll_admin', $inline_script, 'before' );
238
239 $screen = get_current_screen();
240 if ( empty( $screen ) ) {
241 return;
242 }
243
244 /*
245 * For each script:
246 * 0 => the pages on which to load the script
247 * 1 => the scripts it needs to work
248 * 2 => true if loaded even if languages have not been defined yet, false otherwise
249 * 3 => true if loaded in footer
250 */
251 $scripts = array(
252 'widgets' => array( array( 'widgets' ), array(), false, true ),
253 );
254
255 $block_screens = array( 'widgets', 'site-editor' );
256
257 if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
258 $scripts['post'] = array( array( 'edit' ), array( 'jquery', 'wp-ajax-response' ), false, true );
259
260 // Classic editor.
261 if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
262 $scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
263 }
264
265 // Block editor with legacy metabox in WP 5.0+.
266 $block_screens[] = 'post';
267 }
268
269 if ( $this->options['media_support'] ) {
270 $scripts['media'] = array( array( 'upload' ), array( 'jquery' ), false, true );
271 }
272
273 if ( $this->is_block_editor( $screen ) ) {
274 $scripts['block-editor'] = array( $block_screens, array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
275 }
276
277 if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
278 $scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), false, true );
279 }
280
281 foreach ( $scripts as $script => $v ) {
282 if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->has_languages() ) ) {
283 wp_enqueue_script( "pll_{$script}", plugins_url( "/js/build/{$script}{$suffix}.js", POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
284 if ( 'classic-editor' === $script || 'block-editor' === $script ) {
285 wp_set_script_translations( "pll_{$script}", 'polylang' );
286 }
287 }
288 }
289
290 pll_enqueue_style( 'admin', array( 'wp-jquery-ui-dialog' ) );
291
292 $this->add_inline_scripts();
293 }
294
295 /**
296 * Tells whether or not the given screen is block editor kind.
297 * e.g. widget, site or post editor.
298 *
299 * @since 3.3
300 *
301 * @param WP_Screen $screen Screen object.
302 * @return bool True if the screen is a block editor, false otherwise.
303 */
304 protected function is_block_editor( $screen ) {
305 return method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin();
306 }
307
308 /**
309 * Enqueue scripts to the WP Customizer.
310 *
311 * @since 2.4.0
312 *
313 * @return void
314 */
315 public function customize_controls_enqueue_scripts() {
316 if ( $this->model->has_languages() ) {
317 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
318 wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
319 $this->add_inline_scripts();
320 }
321 }
322
323 /**
324 * Adds inline scripts to set the default language in JS
325 * and localizes scripts.
326 *
327 * @since 3.3
328 *
329 * @return void
330 */
331 private function add_inline_scripts() {
332 if ( wp_script_is( 'pll_block-editor', 'enqueued' ) ) {
333 $default_lang_script = sprintf( 'const pllDefaultLanguage = %s;', wp_json_encode( $this->options['default_lang'] ) );
334 wp_add_inline_script(
335 'pll_block-editor',
336 $default_lang_script,
337 'before'
338 );
339 }
340 if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
341 wp_localize_script(
342 'pll_widgets',
343 'pll_widgets',
344 array(
345 'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
346 )
347 );
348 }
349 }
350
351 /**
352 * Returns the data to use with the AJAX filter.
353 * The final goal is to detect if an ajax request is made on admin or frontend.
354 *
355 * Takes care to various situations:
356 * - When the AJAX request has no `options.data` thanks to ScreenfeedFr.
357 * See: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected.
358 * - When `options.data` is a JSON string.
359 * See: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels.
360 * - When `options.data` is an empty string (GET request with the method 'load').
361 * See: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation.
362 *
363 * @since 3.7
364 *
365 * @return array
366 */
367 public function get_ajax_filter_data(): array {
368 global $post, $tag;
369
370 $params = array( 'pll_ajax_backend' => 1 );
371 if ( $post instanceof WP_Post && $this->model->post_types->is_translated( $post->post_type ) ) {
372 $params['pll_post_id'] = $post->ID;
373 }
374
375 if ( $tag instanceof WP_Term && $this->model->taxonomies->is_translated( $tag->taxonomy ) ) {
376 $params['pll_term_id'] = $tag->term_id;
377 }
378
379 /**
380 * Filters the list of parameters to add to the admin ajax request.
381 *
382 * @since 3.4.5
383 *
384 * @param array $params List of parameters to add to the admin ajax request.
385 */
386 return (array) apply_filters( 'pll_admin_ajax_params', $params );
387 }
388
389 /**
390 * Sets the admin current language, used to filter the content
391 *
392 * @since 2.0
393 *
394 * @return void
395 */
396 public function set_current_language() {
397 $this->curlang = $this->filter_lang;
398
399 // Edit Post
400 if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
401 $this->curlang = $lang;
402 } 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
403 $this->curlang = $lang;
404 } 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
405 $this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
406 }
407
408 // Edit Term
409 elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
410 $this->curlang = $lang;
411 } 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
412 if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
413 $this->curlang = $lang;
414 } elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
415 $this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
416 } elseif ( empty( $this->curlang ) ) {
417 $this->curlang = $this->pref_lang;
418 }
419 }
420
421 // Ajax
422 if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
423 $this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
424 }
425
426 /**
427 * Filters the current language used by Polylang in the admin context.
428 *
429 * @since 3.2
430 *
431 * @param PLL_Language|false|null $curlang Instance of the current language.
432 * @param PLL_Admin_Base $polylang Instance of the main Polylang's object.
433 */
434 $this->curlang = apply_filters( 'pll_admin_current_language', $this->curlang, $this );
435
436 // Inform that the admin language has been set.
437 if ( $this->curlang instanceof PLL_Language ) {
438 /** This action is documented in src/frontend/choose-lang.php */
439 do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
440 } else {
441 /** This action is documented in src/class-polylang.php */
442 do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
443 }
444 }
445
446 /**
447 * Defines the backend language and the admin language filter based on user preferences.
448 *
449 * @since 1.2.3
450 *
451 * @return void
452 */
453 public function init_user() {
454 /*
455 * $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter.
456 * We intentionally don't use a nonce to update the language filter.
457 */
458 if ( ! wp_doing_ajax() && ! empty( $_GET['lang'] ) && ! is_numeric( sanitize_key( $_GET['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
459 $user_id = get_current_user_id();
460 if ( current_user_can( 'edit_user', $user_id ) ) {
461 $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
462 update_user_meta( $user_id, 'pll_filter_content', $lang ? $lang->slug : '' );
463 }
464 }
465
466 $this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
467
468 // Set preferred language for use when saving posts and terms: must not be empty.
469 $this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_default_language() : $this->filter_lang;
470
471 /**
472 * Filters the preferred language on admin side.
473 * The preferred language is used for example to determine the language of a new post.
474 *
475 * @since 1.2.3
476 *
477 * @param PLL_Language $pref_lang Preferred language.
478 */
479 $this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
480
481 $this->set_current_language();
482 }
483
484 /**
485 * Avoids parsing a tax query when all languages are requested
486 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
487 *
488 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
489 *
490 * @since 1.6.5
491 *
492 * @param array $qvars The array of requested query variables.
493 * @return array
494 */
495 public function request( $qvars ) {
496 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
497 unset( $qvars['lang'] );
498 }
499
500 return $qvars;
501 }
502
503 /**
504 * Adds the languages list in admin bar for the admin languages filter.
505 *
506 * @since 0.9
507 *
508 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
509 * @return void
510 */
511 public function admin_bar_menu( $wp_admin_bar ) {
512 $all_item = (object) array(
513 'slug' => 'all',
514 'name' => __( 'Show all languages', 'polylang' ),
515 'flag' => '<span class="ab-icon"></span>',
516 );
517
518 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
519
520 $title = sprintf(
521 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
522 $selected instanceof PLL_Language ? sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ) : '',
523 __( 'Filters content by language', 'polylang' ),
524 esc_html( $selected->name )
525 );
526
527 $all_items = array_merge( array( $all_item ), $this->model->get_languages_list() );
528 $items = $all_items;
529
530 if ( $this->should_hide_admin_bar_menu() ) {
531 $items = array();
532 }
533
534 /**
535 * Filters the admin bar language filter submenu items.
536 *
537 * @since 2.6
538 * @since 3.8 Added `$all_items` parameter.
539 *
540 * @param array $items The items of the admin languages filter to display (may be empty if menu hidden).
541 * @param array $all_items Complete unfiltered list of all available language items.
542 */
543 $items = apply_filters( 'pll_admin_languages_filter', $items, $all_items );
544
545 if ( empty( $items ) ) {
546 return;
547 }
548
549 $wp_admin_bar->add_menu(
550 array(
551 'id' => 'languages',
552 'title' => $selected->flag . $title,
553 'href' => esc_url( $this->get_admin_bar_menu_url( $selected ) ),
554 'meta' => array(
555 'title' => __( 'Filters content by language', 'polylang' ),
556 'class' => 'all' === $selected->slug ? '' : 'pll-filtered-languages',
557 ),
558 )
559 );
560
561 foreach ( $items as $lang ) {
562 if ( $selected->slug === $lang->slug ) {
563 continue;
564 }
565
566 $wp_admin_bar->add_menu(
567 array(
568 'parent' => 'languages',
569 'id' => $lang->slug,
570 'title' => sprintf( '%s %s', 'all' === $lang->slug ? $lang->flag : $lang->get_admin_flag( 'aria-hidden' ), esc_html( $lang->name ) ),
571 'href' => esc_url( $this->get_admin_bar_menu_url( $lang ) ),
572 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
573 )
574 );
575 }
576 }
577
578 /**
579 * Returns the admin language filter url for a given language.
580 *
581 * @since 3.9
582 *
583 * @param object $language The language or an object for all languages.
584 * @return string
585 *
586 * @phpstan-param object{'slug': string} $language
587 */
588 protected function get_admin_bar_menu_url( $language ): string {
589 global $pagenow;
590
591 $url = add_query_arg( 'lang', $language->slug, remove_query_arg( 'paged' ) );
592
593 if ( 'edit.php' !== $pagenow || ! $language instanceof PLL_Language ) {
594 return $url;
595 }
596
597 // Attempt to translate the category (taxonomy) filter if present.
598 $post_type = get_post_type();
599
600 if ( ! $post_type ) {
601 return $url;
602 }
603
604 foreach ( get_object_taxonomies( $post_type, 'objects' ) as $tax ) {
605 if ( ! $this->model->is_translated_taxonomy( $tax->name ) ) {
606 continue;
607 }
608
609 $query_var = (string) $tax->query_var;
610 $qv = get_query_var( $query_var );
611 if ( empty( $qv ) || ! is_string( $qv ) ) {
612 continue;
613 }
614
615 $qv = $this->model->term->get_by( 'slug', $qv, $language, $tax->name );
616
617 if ( ! empty( $qv ) ) {
618 $url = add_query_arg( $query_var, $qv, remove_query_arg( $query_var, $url ) );
619 }
620
621 // For categories, we translated 'category_name'. We must remove 'cat' to avoid conflicts.
622 if ( 'category' === $tax->name ) {
623 $url = remove_query_arg( 'cat', $url );
624 }
625 }
626
627 return $url;
628 }
629
630 /**
631 * Remove the customize submenu when using a block theme.
632 *
633 * WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
634 * As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
635 * unless another plugin than Polylang interacts with the Customizer.
636 *
637 * @since 3.2
638 *
639 * @return void
640 */
641 public function remove_customize_submenu() {
642 if ( ! $this->should_customize_menu_be_removed() ) {
643 return;
644 }
645
646 global $submenu;
647
648 if ( ! empty( $submenu['themes.php'] ) ) {
649 foreach ( $submenu['themes.php'] as $submenu_item ) {
650 if ( 'customize' === $submenu_item[1] ) {
651 remove_submenu_page( 'themes.php', $submenu_item[2] );
652 }
653 }
654 }
655 }
656
657 /**
658 * Tells if the Polylang's admin bar menu should be hidden for the current page.
659 * Conventionally, it should be hidden on edition pages, term edit pages and Site Editor pages.
660 *
661 * @since 3.8
662 *
663 * @return bool
664 */
665 public function should_hide_admin_bar_menu(): bool {
666 global $pagenow;
667
668 return in_array( $pagenow, array( 'post.php', 'post-new.php', 'site-editor.php', 'term.php' ), true );
669 }
670
671 /**
672 * Returns the ID of a Polylang's settings screen.
673 *
674 * @since 3.8
675 *
676 * @param string $tab The name of the screen (`lang`, `strings`, `settings`).
677 * @return string
678 *
679 * @phpstan-return non-empty-string
680 */
681 public static function get_screen_id( string $tab ): string {
682 return sprintf( '%s_page_%s', self::SCREEN_PREFIX, self::get_screen_slug( $tab ) );
683 }
684
685 /**
686 * Returns the slug of a Polylang's settings screen, as seen in the URL.
687 *
688 * @since 3.8
689 *
690 * @param string $tab The name of the screen (`lang`, `strings`, `settings`).
691 * @return string
692 *
693 * @phpstan-return non-empty-string
694 */
695 public static function get_screen_slug( string $tab ): string {
696 return 'lang' === $tab ? 'mlang' : "mlang_$tab";
697 }
698
699 /**
700 * Returns the list of sub-menu items.
701 *
702 * @since 3.8
703 *
704 * @return string[] List of sub-menu items with page slugs as array keys, and sub-menu titles as array values.
705 *
706 * @phpstan-return array<non-empty-string, string>
707 */
708 protected function get_menu_items(): array {
709 $tabs = array(
710 'lang' => __( 'Languages', 'polylang' ),
711 );
712
713 // Only if at least one language has been created.
714 if ( ! empty( $this->model->languages->filter( 'translator' )->get_list() ) ) {
715 $tabs['strings'] = __( 'Translations', 'polylang' );
716 }
717
718 $tabs['settings'] = __( 'Settings', 'polylang' );
719
720 /**
721 * Filter the list of sub-menu items in Polylang settings.
722 *
723 * @since 1.5.1
724 *
725 * @param string[] $tabs List of sub-menu items with page slugs as array keys and titles as array values.
726 */
727 return (array) apply_filters( 'pll_settings_tabs', $tabs );
728 }
729
730 /**
731 * Returns the user capability required to access the given menu page.
732 *
733 * @since 3.8
734 *
735 * @param string $menu Menu slug.
736 * @return string
737 */
738 protected function get_menu_capability( string $menu ): string {
739 switch ( $menu ) {
740 case 'lang':
741 return Capabilities::LANGUAGES;
742
743 case 'strings':
744 return Capabilities::TRANSLATIONS;
745 }
746
747 return 'manage_options';
748 }
749 }
750