As the sun rises and the forest mist clears, and the clouds return and the caves darken, these changes of light and shadow are the morning and evening in the mountains. Wildflowers bloom with their subtle fragrance, fine trees flourish with their dense shade, the wind and frost are pure and clean, and the water recedes to reveal the rocks—these are the four seasons in the mountains. Going out in the morning and returning in the evening, the scenery of the four seasons is different, and the joy is endless.至于负者歌于途,行者休于树,前者呼,后者应,伛偻提携,往来而不绝者,滁人游也。临溪而渔,溪深而鱼肥,酿泉为酒,泉香而酒洌,山肴野蔌,杂然而前陈者,太守宴也。宴酣之乐,非丝非竹,射者中,弈者胜,觥筹交错,起坐而喧哗者,众宾欢也。苍颜白发,颓然乎其间者,太守醉也。
<?php
/**
* @package Polylang
*/
use WP_Syntex\Polylang\Capabilities\Capabilities;
/**
* Setup features available on all admin pages.
*
* @since 1.8
*/
abstract class PLL_Admin_Base extends PLL_Base {
/**
* @since 3.8
*/
public const SCREEN_PREFIX = 'languages';
/**
* Current language (used to filter the content).
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Language selected in the admin language filter.
*
* @var PLL_Language|null
*/
public $filter_lang;
/**
* Preferred language to assign to new contents.
*
* @var PLL_Language|null
*/
public $pref_lang;
/**
* @var PLL_Filters_Links|null
*/
public $filters_links;
/**
* @var PLL_Admin_Links|null
*/
public $links;
/**
* @var PLL_Admin_Notices|null
*/
public $notices;
/**
* @var PLL_Admin_Static_Pages|null
*/
public $static_pages;
/**
* @var PLL_Admin_Default_Term|null
*/
public $default_term;
/**
* Setups actions needed on all admin pages.
*
* @since 1.8
*
* @param PLL_Links_Model $links_model Reference to the links model.
*/
public function __construct( &$links_model ) {
parent::__construct( $links_model );
// Adds the link to the languages panel in the WordPress admin menu
add_action( 'admin_menu', array( $this, 'add_menus' ) );
add_action( 'admin_menu', array( $this, 'remove_customize_submenu' ) );
// Setup js scripts and css styles
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
add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
// Early instantiated to be able to correctly initialize language properties.
$this->static_pages = new PLL_Admin_Static_Pages( $this );
$this->model->set_languages_ready();
}
/**
* Setups filters and action needed on all admin pages and on plugins page
* Loads the settings pages or the filters base on the request
*
* @since 1.2
*/
public function init() {
parent::init();
$this->notices = new PLL_Admin_Notices( $this );
$this->default_term = new PLL_Admin_Default_Term( $this );
$this->default_term->add_hooks();
if ( ! $this->model->has_languages() ) {
return;
}
$this->links = new PLL_Admin_Links( $this ); // FIXME needed here ?
$this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ?
// Filter admin language for users
// We must not call user info before WordPress defines user roles in wp-settings.php
add_action( 'setup_theme', array( $this, 'init_user' ) );
add_filter( 'request', array( $this, 'request' ) );
// Adds the languages in admin bar
add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 ); // 100 determines the position
}
/**
* Adds links to Polylang's admin panels to the WordPress admin menu.
*
* @since 0.1
*
* @return void
*/
public function add_menus(): void {
global $admin_page_hooks;
$parent = '';
$first_tab = '';
foreach ( $this->get_menu_items() as $tab => $title ) {
$page = self::get_screen_slug( $tab );
$capa = $this->get_menu_capability( $tab );
if ( empty( $parent ) ) {
$parent = $page;
$first_tab = $tab;
/*
* WP actually doesn't care about the user capability used here, as long as it has sub-menus: it will
* use the ones from the sub-menus. See `_wp_menu_output()`.
* Ex: a user with `manage_translations` will still be able to access the Translations page, even if the
* main menu has `manage_options`.
*/
add_menu_page( $title, __( 'Languages', 'polylang' ), $capa, $parent, '__return_null', 'dashicons-translation' );
$admin_page_hooks[ $parent ] = self::SCREEN_PREFIX; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
}
add_submenu_page( $parent, $title, $title, $capa, $page, array( $this, 'languages_page' ) );
}
/*
* Get rid of the `toplevel` prefix in hook names.
*
* In the WP admin, if an admin screen is the first of its menu (like the PLL's "Languages" screen), the hooks
* fired in the screen get a `toplevel` prefix (ex: `toplevel_page_mlang`) while all the other screens get a
* slug based on the parent screen title (ex: `languages_page_mlang_strings`, where `languages` is the parent
* screen's slug). This will not prevent the `toplevel` hooks to fire, but it will fire the `languages` hooks in
* addition: this way, screens can be removed or moved around without the need of hooking both prefixes: using
* the hooks with the `languages` prefix will work in both cases.
*
* @see get_plugin_page_hookname()
*/
foreach ( array( 'load-', 'admin_print_styles-', 'admin_print_scripts-', 'admin_head-', '', 'admin_print_footer_scripts-', 'admin_footer-' ) as $prefix ) {
add_action(
"{$prefix}toplevel_page_{$parent}",
static function () use ( $prefix, $first_tab ) {
do_action( $prefix . self::get_screen_id( $first_tab ) );
}
);
}
/*
* Ensure a common CSS class to the `<body>` tag.
*
* Due to the `toplevel` "issue" described earlier, the CSS class `toplevel_page_mlang` (for example) is added
* to the body. This adds a class with the `languages` prefix. This ensures we have a common CSS class, even if
* the screen is moved to the 1st position in the menu.
*/
add_action(
// Target the screen in 1st position only.
"admin_head-toplevel_page_{$parent}",
static function () use ( $first_tab ) {
add_filter(
'admin_body_class',
static function ( $admin_body_classes ) use ( $first_tab ) {
return $admin_body_classes . ' ' . self::get_screen_id( $first_tab );
}
);
}
);
/**
* Also modify the screen ID and base.
*
* Note: the global variables `$page_hook` and `$hook_suffix` are not changed, their value is still
* `toplevel_page_mlang`. Changing them breaks things because we can't filter `get_plugin_page_hookname()`.
* This is why the above hooks are still needed.
*/
add_action(
'current_screen',
static function ( $current_screen ) use ( $parent, $first_tab ) {
if ( "toplevel_page_{$parent}" !== $current_screen->id ) {
return;
}
$current_screen->id = self::get_screen_id( $first_tab );
$current_screen->base = self::get_screen_id( $first_tab );
}
);
}
/**
* Dummy method to display the 3 tabs pages: languages, strings translations, settings.
* Overwritten in `PLL_Settings`.
*
* @since 3.7
*
* @return void
*/
public function languages_page() {}
/**
* Setup js scripts & css styles ( only on the relevant pages )
*
* @since 0.6
*
* @return void
*/
public function admin_enqueue_scripts() {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script( 'pll_admin', plugins_url( "/js/build/admin{$suffix}.js", POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
$inline_script = sprintf( 'let pll_admin = %s;', wp_json_encode( array( 'ajax_filter' => $this->get_ajax_filter_data() ) ) );
wp_add_inline_script( 'pll_admin', $inline_script, 'before' );
$screen = get_current_screen();
if ( empty( $screen ) ) {
return;
}
/*
* For each script:
* 0 => the pages on which to load the script
* 1 => the scripts it needs to work
* 2 => true if loaded even if languages have not been defined yet, false otherwise
* 3 => true if loaded in footer
*/
$scripts = array(
'widgets' => array( array( 'widgets' ), array( 'jquery' ), false, false ),
);
$block_screens = array( 'widgets', 'site-editor' );
if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
$scripts['post'] = array( array( 'edit' ), array( 'jquery', 'wp-ajax-response' ), false, true );
// Classic editor.
if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
$scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
}
// Block editor with legacy metabox in WP 5.0+.
$block_screens[] = 'post';
}
if ( $this->options['media_support'] ) {
$scripts['media'] = array( array( 'upload' ), array( 'jquery' ), false, true );
}
if ( $this->is_block_editor( $screen ) ) {
$scripts['block-editor'] = array( $block_screens, array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
}
if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
$scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), false, true );
}
foreach ( $scripts as $script => $v ) {
if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->has_languages() ) ) {
wp_enqueue_script( "pll_{$script}", plugins_url( "/js/build/{$script}{$suffix}.js", POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
if ( 'classic-editor' === $script || 'block-editor' === $script ) {
wp_set_script_translations( "pll_{$script}", 'polylang' );
}
}
}
wp_register_style( 'polylang_admin', plugins_url( "/css/build/admin{$suffix}.css", POLYLANG_ROOT_FILE ), array( 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
wp_enqueue_style( 'polylang_dialog', plugins_url( "/css/build/dialog{$suffix}.css", POLYLANG_ROOT_FILE ), array( 'polylang_admin' ), POLYLANG_VERSION );
$this->add_inline_scripts();
}
/**
* Tells whether or not the given screen is block editor kind.
* e.g. widget, site or post editor.
*
* @since 3.3
*
* @param WP_Screen $screen Screen object.
* @return bool True if the screen is a block editor, false otherwise.
*/
protected function is_block_editor( $screen ) {
return method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin();
}
/**
* Enqueue scripts to the WP Customizer.
*
* @since 2.4.0
*
* @return void
*/
public function customize_controls_enqueue_scripts() {
if ( $this->model->has_languages() ) {
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
$this->add_inline_scripts();
}
}
/**
* Adds inline scripts to set the default language in JS
* and localizes scripts.
*
* @since 3.3
*
* @return void
*/
private function add_inline_scripts() {
if ( wp_script_is( 'pll_block-editor', 'enqueued' ) ) {
$default_lang_script = 'const pllDefaultLanguage = "' . $this->options['default_lang'] . '";';
wp_add_inline_script(
'pll_block-editor',
$default_lang_script,
'before'
);
}
if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
wp_localize_script(
'pll_widgets',
'pll_widgets',
array(
'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
)
);
}
}
/**
* Returns the data to use with the AJAX filter.
* The final goal is to detect if an ajax request is made on admin or frontend.
*
* Takes care to various situations:
* - When the AJAX request has no `options.data` thanks to ScreenfeedFr.
* See: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected.
* - When `options.data` is a JSON string.
* See: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels.
* - When `options.data` is an empty string (GET request with the method 'load').
* See: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation.
*
* @since 3.7
*
* @return array
*/
public function get_ajax_filter_data(): array {
global $post, $tag;
$params = array( 'pll_ajax_backend' => 1 );
if ( $post instanceof WP_Post && $this->model->post_types->is_translated( $post->post_type ) ) {
$params['pll_post_id'] = $post->ID;
}
if ( $tag instanceof WP_Term && $this->model->taxonomies->is_translated( $tag->taxonomy ) ) {
$params['pll_term_id'] = $tag->term_id;
}
/**
* Filters the list of parameters to add to the admin ajax request.
*
* @since 3.4.5
*
* @param array $params List of parameters to add to the admin ajax request.
*/
return (array) apply_filters( 'pll_admin_ajax_params', $params );
}
/**
* Sets the admin current language, used to filter the content
*
* @since 2.0
*
* @return void
*/
public function set_current_language() {
$this->curlang = $this->filter_lang;
// Edit Post
if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->curlang = $lang;
} 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
$this->curlang = $lang;
} 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
$this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
// Edit Term
elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->curlang = $lang;
} 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
if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->curlang = $lang;
} elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
} elseif ( empty( $this->curlang ) ) {
$this->curlang = $this->pref_lang;
}
}
// Ajax
if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
/**
* Filters the current language used by Polylang in the admin context.
*
* @since 3.2
*
* @param PLL_Language|false|null $curlang Instance of the current language.
* @param PLL_Admin_Base $polylang Instance of the main Polylang's object.
*/
$this->curlang = apply_filters( 'pll_admin_current_language', $this->curlang, $this );
// Inform that the admin language has been set.
if ( $this->curlang instanceof PLL_Language ) {
/** This action is documented in src/frontend/choose-lang.php */
do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
} else {
/** This action is documented in src/class-polylang.php */
do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
}
}
/**
* Defines the backend language and the admin language filter based on user preferences.
*
* @since 1.2.3
*
* @return void
*/
public function init_user() {
/*
* $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter.
* We intentionally don't use a nonce to update the language filter.
*/
if ( ! wp_doing_ajax() && ! empty( $_GET['lang'] ) && ! is_numeric( sanitize_key( $_GET['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$user_id = get_current_user_id();
if ( current_user_can( 'edit_user', $user_id ) ) {
$lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
update_user_meta( $user_id, 'pll_filter_content', $lang ? $lang->slug : '' );
}
}
$this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
// Set preferred language for use when saving posts and terms: must not be empty.
$this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_default_language() : $this->filter_lang;
/**
* Filters the preferred language on admin side.
* The preferred language is used for example to determine the language of a new post.
*
* @since 1.2.3
*
* @param PLL_Language $pref_lang Preferred language.
*/
$this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
$this->set_current_language();
}
/**
* Avoids parsing a tax query when all languages are requested
* Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
*
* @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
*
* @since 1.6.5
*
* @param array $qvars The array of requested query variables.
* @return array
*/
public function request( $qvars ) {
if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
unset( $qvars['lang'] );
}
return $qvars;
}
/**
* Adds the languages list in admin bar for the admin languages filter.
*
* @since 0.9
*
* @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
* @return void
*/
public function admin_bar_menu( $wp_admin_bar ) {
$all_item = (object) array(
'slug' => 'all',
'name' => __( 'Show all languages', 'polylang' ),
'flag' => '<span class="ab-icon"></span>',
);
$selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
$title = sprintf(
'<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
$selected instanceof PLL_Language ? sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ) : '',
__( 'Filters content by language', 'polylang' ),
esc_html( $selected->name )
);
$all_items = array_merge( array( $all_item ), $this->model->get_languages_list() );
$items = $all_items;
if ( $this->should_hide_admin_bar_menu() ) {
$items = array();
}
/**
* Filters the admin bar language filter submenu items.
*
* @since 2.6
* @since 3.8 Added `$all_items` parameter.
*
* @param array $items The items of the admin languages filter to display (may be empty if menu hidden).
* @param array $all_items Complete unfiltered list of all available language items.
*/
$items = apply_filters( 'pll_admin_languages_filter', $items, $all_items );
if ( empty( $items ) ) {
return;
}
$wp_admin_bar->add_menu(
array(
'id' => 'languages',
'title' => $selected->flag . $title,
'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
'meta' => array(
'title' => __( 'Filters content by language', 'polylang' ),
'class' => 'all' === $selected->slug ? '' : 'pll-filtered-languages',
),
)
);
foreach ( $items as $lang ) {
if ( $selected->slug === $lang->slug ) {
continue;
}
$wp_admin_bar->add_menu(
array(
'parent' => 'languages',
'id' => $lang->slug,
'title' => $lang->flag . esc_html( $lang->name ),
'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
)
);
}
}
/**
* Remove the customize submenu when using a block theme.
*
* WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
* As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
* unless another plugin than Polylang interacts with the Customizer.
*
* @since 3.2
*
* @return void
*/
public function remove_customize_submenu() {
if ( ! $this->should_customize_menu_be_removed() ) {
return;
}
global $submenu;
if ( ! empty( $submenu['themes.php'] ) ) {
foreach ( $submenu['themes.php'] as $submenu_item ) {
if ( 'customize' === $submenu_item[1] ) {
remove_submenu_page( 'themes.php', $submenu_item[2] );
}
}
}
}
/**
* Tells if the Polylang's admin bar menu should be hidden for the current page.
* Conventionally, it should be hidden on edition pages.
*
* @since 3.8
*
* @return bool
*/
public function should_hide_admin_bar_menu(): bool {
global $pagenow, $typenow, $taxnow;
if ( in_array( $pagenow, array( 'post.php', 'post-new.php' ), true ) ) {
return ! empty( $typenow );
}
if ( 'term.php' === $pagenow ) {
return ! empty( $taxnow );
}
return false;
}
/**
* Returns the ID of a Polylang's settings screen.
*
* @since 3.8
*
* @param string $tab The name of the screen (`lang`, `strings`, `settings`).
* @return string
*
* @phpstan-return non-empty-string
*/
public static function get_screen_id( string $tab ): string {
return sprintf( '%s_page_%s', self::SCREEN_PREFIX, self::get_screen_slug( $tab ) );
}
/**
* Returns the slug of a Polylang's settings screen, as seen in the URL.
*
* @since 3.8
*
* @param string $tab The name of the screen (`lang`, `strings`, `settings`).
* @return string
*
* @phpstan-return non-empty-string
*/
public static function get_screen_slug( string $tab ): string {
return 'lang' === $tab ? 'mlang' : "mlang_$tab";
}
/**
* Returns the list of sub-menu items.
*
* @since 3.8
*
* @return string[] List of sub-menu items with page slugs as array keys, and sub-menu titles as array values.
*
* @phpstan-return array<non-empty-string, string>
*/
protected function get_menu_items(): array {
$tabs = array(
'lang' => __( 'Languages', 'polylang' ),
);
// Only if at least one language has been created.
if ( ! empty( $this->model->languages->filter( 'translator' )->get_list() ) ) {
$tabs['strings'] = __( 'Translations', 'polylang' );
}
$tabs['settings'] = __( 'Settings', 'polylang' );
/**
* Filter the list of sub-menu items in Polylang settings.
*
* @since 1.5.1
*
* @param string[] $tabs List of sub-menu items with page slugs as array keys and titles as array values.
*/
return (array) apply_filters( 'pll_settings_tabs', $tabs );
}
/**
* Returns the user capability required to access the given menu page.
*
* @since 3.8
*
* @param string $menu Menu slug.
* @return string
*/
protected function get_menu_capability( string $menu ): string {
switch ( $menu ) {
case 'lang':
return Capabilities::LANGUAGES;
case 'strings':
return Capabilities::TRANSLATIONS;
}
return 'manage_options';
}
}