first commit

This commit is contained in:
2025-02-24 22:33:42 +01:00
commit 737c037e85
18358 changed files with 5392983 additions and 0 deletions

View File

@@ -0,0 +1,575 @@
<?php
/**
* @package Polylang
*/
/**
* Setup features available on all admin pages.
*
* @since 1.8
*/
abstract class PLL_Admin_Base extends PLL_Base {
/**
* 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' ) );
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
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 the link to the languages panel in the WordPress admin menu
*
* @since 0.1
*
* @return void
*/
public function add_menus() {
global $admin_page_hooks;
// Prepare the list of tabs
$tabs = array( 'lang' => __( 'Languages', 'polylang' ) );
// Only if at least one language has been created
if ( $this->model->has_languages() ) {
$tabs['strings'] = __( 'Translations', 'polylang' );
}
$tabs['settings'] = __( 'Settings', 'polylang' );
/**
* Filter the list of tabs in Polylang settings
*
* @since 1.5.1
*
* @param array $tabs list of tab names
*/
$tabs = apply_filters( 'pll_settings_tabs', $tabs );
$parent = '';
foreach ( $tabs as $tab => $title ) {
$page = 'lang' === $tab ? 'mlang' : "mlang_$tab";
if ( empty( $parent ) ) {
$parent = $page;
add_menu_page( $title, __( 'Languages', 'polylang' ), 'manage_options', $page, '__return_null', 'dashicons-translation' );
$admin_page_hooks[ $page ] = 'languages'; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
}
add_submenu_page( $parent, $title, $title, 'manage_options', $page, array( $this, 'languages_page' ) );
}
}
/**
* Setup js scripts & css styles ( only on the relevant pages )
*
* @since 0.6
*
* @return void
*/
public function admin_enqueue_scripts() {
$screen = get_current_screen();
if ( empty( $screen ) ) {
return;
}
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
/*
* 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(
'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), false, false ),
'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', 'upload' ), 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->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' ),
)
);
}
}
/**
* Sets pll_ajax_backend on all backend ajax request
* 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 1.4
*
* @return void
*/
public function admin_print_footer_scripts() {
global $post_ID, $tag_ID;
$params = array( 'pll_ajax_backend' => 1 );
if ( ! empty( $post_ID ) ) {
$params = array_merge( $params, array( 'pll_post_id' => (int) $post_ID ) );
}
if ( ! empty( $tag_ID ) ) {
$params = array_merge( $params, array( 'pll_term_id' => (int) $tag_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.
*/
$params = apply_filters( 'pll_admin_ajax_params', $params );
$str = http_build_query( $params );
$arr = wp_json_encode( $params );
?>
<script>
if (typeof jQuery != 'undefined') {
jQuery(
function( $ ){
$.ajaxPrefilter( function ( options, originalOptions, jqXHR ) {
if ( -1 != options.url.indexOf( ajaxurl ) || -1 != ajaxurl.indexOf( options.url ) ) {
function addPolylangParametersAsString() {
if ( 'undefined' === typeof options.data || '' === options.data.trim() ) {
// Only Polylang data need to be send. So it could be as a simple query string.
options.data = '<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
} else {
/*
* In some cases data could be a JSON string like in third party plugins.
* So we need not to break their process by adding polylang parameters as valid JSON data.
*/
try {
options.data = JSON.stringify( Object.assign( JSON.parse( options.data ), <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> ) );
} catch( exception ) {
// Add Polylang data to the existing query string.
options.data = options.data + '&<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
}
}
}
/*
* options.processData set to true is the default jQuery process where the data is converted in a query string by using jQuery.param().
* This step is done before applying filters. Thus here the options.data is already a string in this case.
* @See https://github.com/jquery/jquery/blob/3.5.1/src/ajax.js#L563-L569 jQuery ajax function.
* It is the most case WordPress send ajax request this way however third party plugins or themes could be send JSON string.
* Use JSON format is recommended in jQuery.param() documentation to be able to send complex data structures.
* @See https://api.jquery.com/jquery.param/ jQuery param function.
*/
if ( options.processData ) {
addPolylangParametersAsString();
} else {
/*
* If options.processData is set to false data could be undefined or pass as a string.
* So data as to be processed as if options.processData is set to true.
*/
if ( 'undefined' === typeof options.data || 'string' === typeof options.data ) {
addPolylangParametersAsString();
} else {
// Otherwise options.data is probably an object.
options.data = Object.assign( options.data || {} , <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> );
}
}
}
});
}
);
}
</script>
<?php
}
/**
* 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 frontend/choose-lang.php */
do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
} else {
/** This action is documented in include/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() {
// Language for admin language filter: may be empty
// $_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'] ) ) && current_user_can( 'edit_user', $user_id = get_current_user_id() ) ) { // phpcs:ignore WordPress.Security.NonceVerification
update_user_meta( $user_id, 'pll_filter_content', ( $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ) ) ? $lang->slug : '' ); // phpcs:ignore WordPress.Security.NonceVerification
}
$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 )
);
/**
* Filters the admin languages filter submenu items
*
* @since 2.6
*
* @param array $items The admin languages filter submenu items.
*/
$items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
$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' ),
),
);
if ( 'all' !== $selected->slug ) {
$menu['meta']['class'] = 'pll-filtered-languages';
}
if ( ! empty( $items ) ) {
$wp_admin_bar->add_menu( $menu );
}
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] );
}
}
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to the block editor
*
* @since 2.5
*/
class PLL_Admin_Block_Editor {
/**
* @var PLL_Model
*/
protected $model;
/**
* @var PLL_Filter_REST_Routes
*/
public $filter_rest_routes;
/**
* Constructor: setups filters and actions.
*
* @since 2.5
*
* @param PLL_Admin $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->filter_rest_routes = new PLL_Filter_REST_Routes( $polylang->model );
add_filter( 'block_editor_rest_api_preload_paths', array( $this, 'filter_preload_paths' ), 50, 2 );
add_action( 'admin_enqueue_scripts', array( $this, 'add_block_editor_inline_script' ), 15 ); // After `PLL_Admin_Base::admin_enqueue_scripts()` to ensure `pll_block-editor`script is enqueued.
}
/**
* Filters preload paths based on the context (block editor for posts, site editor or widget editor for instance).
*
* @since 3.5
*
* @param array $preload_paths Preload paths.
* @param WP_Block_Editor_Context $context Editor context.
* @return array Filtered preload paths.
*/
public function filter_preload_paths( $preload_paths, $context ) {
if ( ! $context instanceof WP_Block_Editor_Context ) {
return $preload_paths;
}
// Backward compatibility with WP < 6.0 where `WP_Block_Editor_Context::$name` doesn't exist yet.
if (
( property_exists( $context, 'name' ) && 'core/edit-post' !== $context->name )
|| ! $context->post instanceof WP_Post
) {
// Do nothing if not post editor.
return $preload_paths;
}
if ( ! $this->model->is_translated_post_type( $context->post->post_type ) ) {
return $preload_paths;
}
$language = $this->model->post->get_language( $context->post->ID );
if ( empty( $language ) ) {
return $preload_paths;
}
return $this->filter_rest_routes->add_query_parameters(
$preload_paths,
array(
'lang' => $language->slug,
)
);
}
/**
* Adds inline block editor script for filterable REST routes.
*
* @since 3.5
*
* @return void
*/
public function add_block_editor_inline_script() {
$handle = 'pll_block-editor';
if ( wp_script_is( $handle, 'enqueued' ) ) {
$this->filter_rest_routes->add_inline_script( $handle );
}
}
}

View File

@@ -0,0 +1,371 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to the classic editor
*
* @since 2.4
*/
class PLL_Admin_Classic_Editor {
/**
* @var PLL_Model
*/
public $model;
/**
* @var PLL_Admin_Links|null
*/
public $links;
/**
* Current language (used to filter the content).
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Preferred language to assign to new contents.
*
* @var PLL_Language|null
*/
public $pref_lang;
/**
* Constructor: setups filters and actions.
*
* @since 2.4
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->links = &$polylang->links;
$this->curlang = &$polylang->curlang;
$this->pref_lang = &$polylang->pref_lang;
// Adds the Languages box in the 'Edit Post' and 'Edit Page' panels
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
// Ajax response for changing the language in the post metabox
add_action( 'wp_ajax_post_lang_choice', array( $this, 'post_lang_choice' ) );
add_action( 'wp_ajax_pll_posts_not_translated', array( $this, 'ajax_posts_not_translated' ) );
// Filters the pages by language in the parent dropdown list in the page attributes metabox
add_filter( 'page_attributes_dropdown_pages_args', array( $this, 'page_attributes_dropdown_pages_args' ), 10, 2 );
// Notice
add_action( 'edit_form_top', array( $this, 'edit_form_top' ) );
}
/**
* Adds the Language box in the 'Edit Post' and 'Edit Page' panels ( as well as in custom post types panels )
*
* @since 0.1
*
* @param string $post_type Current post type
* @return void
*/
public function add_meta_boxes( $post_type ) {
if ( $this->model->is_translated_post_type( $post_type ) ) {
add_meta_box(
'ml_box',
__( 'Languages', 'polylang' ),
array( $this, 'post_language' ),
$post_type,
'side',
'high',
array(
'__back_compat_meta_box' => pll_use_block_editor_plugin(),
)
);
}
}
/**
* Displays the Languages metabox in the 'Edit Post' and 'Edit Page' panels
*
* @since 0.1
*
* @return void
*/
public function post_language() {
global $post_ID;
$post_type = get_post_type( $post_ID );
// phpcs:ignore WordPress.Security.NonceVerification, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$from_post_id = isset( $_GET['from_post'] ) ? (int) $_GET['from_post'] : 0;
$lang = ( $lg = $this->model->post->get_language( $post_ID ) ) ? $lg :
( isset( $_GET['new_lang'] ) ? $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification
$this->pref_lang );
$dropdown = new PLL_Walker_Dropdown();
$id = ( 'attachment' === $post_type ) ? sprintf( 'attachments[%d][language]', (int) $post_ID ) : 'post_lang_choice';
$dropdown_html = $dropdown->walk(
$this->model->get_languages_list(),
-1,
array(
'name' => $id,
'class' => 'post_lang_choice tags-input',
'selected' => $lang ? $lang->slug : '',
'flag' => true,
)
);
wp_nonce_field( 'pll_language', '_pll_nonce' );
// NOTE: the class "tags-input" allows to include the field in the autosave $_POST ( see autosave.js )
printf(
'<p><strong>%1$s</strong></p>
<label class="screen-reader-text" for="%2$s">%1$s</label>
<div id="select-%3$s-language">%4$s</div>',
esc_html__( 'Language', 'polylang' ),
esc_attr( $id ),
( 'attachment' === $post_type ? 'media' : 'post' ),
$dropdown_html // phpcs:ignore WordPress.Security.EscapeOutput
);
/**
* Fires before displaying the list of translations in the Languages metabox for posts
*
* @since 1.8
*/
do_action( 'pll_before_post_translations', $post_type );
echo '<div id="post-translations" class="translations">';
if ( $lang ) {
if ( 'attachment' === $post_type ) {
include __DIR__ . '/view-translations-media.php';
} else {
include __DIR__ . '/view-translations-post.php';
}
}
echo '</div>' . "\n";
}
/**
* Ajax response for changing the language in the post metabox
*
* @since 0.2
*
* @return void
*/
public function post_lang_choice() {
check_ajax_referer( 'pll_language', '_pll_nonce' );
if ( ! isset( $_POST['post_id'], $_POST['lang'], $_POST['post_type'] ) ) {
wp_die( 'The request is missing the parameter "post_type", "lang" and/or "post_id".' );
}
global $post_ID; // Obliged to use the global variable for wp_popular_terms_checklist
$post_ID = (int) $_POST['post_id'];
$lang_slug = sanitize_key( $_POST['lang'] );
$lang = $this->model->get_language( $lang_slug );
$post_type = sanitize_key( $_POST['post_type'] );
if ( empty( $lang ) ) {
wp_die( esc_html( "{$lang_slug} is not a valid language code." ) );
}
$post_type_object = get_post_type_object( $post_type );
if ( empty( $post_type_object ) ) {
wp_die( esc_html( "{$post_type} is not a valid post type." ) );
}
if ( ! current_user_can( $post_type_object->cap->edit_post, $post_ID ) ) {
wp_die( 'You are not allowed to edit this post.' );
}
$this->model->post->set_language( $post_ID, $lang );
ob_start();
if ( 'attachment' === $post_type ) {
include __DIR__ . '/view-translations-media.php';
} else {
include __DIR__ . '/view-translations-post.php';
}
$x = new WP_Ajax_Response( array( 'what' => 'translations', 'data' => ob_get_contents() ) );
ob_end_clean();
// Categories
if ( isset( $_POST['taxonomies'] ) ) { // Not set for pages
$supplemental = array();
foreach ( array_map( 'sanitize_key', $_POST['taxonomies'] ) as $taxname ) {
$taxonomy = get_taxonomy( $taxname );
if ( ! empty( $taxonomy ) ) {
ob_start();
$popular_ids = wp_popular_terms_checklist( $taxonomy->name );
$supplemental['populars'] = ob_get_contents();
ob_end_clean();
ob_start();
// Use $post_ID to remember checked terms in case we come back to the original language
wp_terms_checklist( $post_ID, array( 'taxonomy' => $taxonomy->name, 'popular_cats' => $popular_ids ) );
$supplemental['all'] = ob_get_contents();
ob_end_clean();
$supplemental['dropdown'] = wp_dropdown_categories(
array(
'taxonomy' => $taxonomy->name,
'hide_empty' => 0,
'name' => 'new' . $taxonomy->name . '_parent',
'orderby' => 'name',
'hierarchical' => 1,
'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;',
'echo' => 0,
)
);
$x->Add( array( 'what' => 'taxonomy', 'data' => $taxonomy->name, 'supplemental' => $supplemental ) );
}
}
}
// Parent dropdown list ( only for hierarchical post types )
if ( in_array( $post_type, get_post_types( array( 'hierarchical' => true ) ) ) ) {
$post = get_post( $post_ID );
if ( ! empty( $post ) ) {
// Args and filter from 'page_attributes_meta_box' in wp-admin/includes/meta-boxes.php of WP 4.2.1
$dropdown_args = array(
'post_type' => $post->post_type,
'exclude_tree' => $post->ID,
'selected' => $post->post_parent,
'name' => 'parent_id',
'show_option_none' => __( '(no parent)', 'polylang' ),
'sort_column' => 'menu_order, post_title',
'echo' => 0,
);
/** This filter is documented in wp-admin/includes/meta-boxes.php */
$dropdown_args = (array) apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post ); // Since WP 3.3.
$dropdown_args['echo'] = 0; // Make sure to not print it.
/** @var string $data */
$data = wp_dropdown_pages( $dropdown_args ); // phpcs:ignore WordPress.Security.EscapeOutput
$x->Add( array( 'what' => 'pages', 'data' => $data ) );
}
}
// Flag
$x->Add( array( 'what' => 'flag', 'data' => empty( $lang->flag ) ? esc_html( $lang->slug ) : $lang->flag ) );
// Sample permalink
$x->Add( array( 'what' => 'permalink', 'data' => get_sample_permalink_html( $post_ID ) ) );
$x->send();
}
/**
* Ajax response for input in translation autocomplete input box
*
* @since 1.5
*
* @return void
*/
public function ajax_posts_not_translated() {
check_ajax_referer( 'pll_language', '_pll_nonce' );
if ( ! isset( $_GET['post_type'], $_GET['post_language'], $_GET['translation_language'], $_GET['term'], $_GET['pll_post_id'] ) ) {
wp_die( 0 );
}
$post_type = sanitize_key( $_GET['post_type'] );
if ( ! post_type_exists( $post_type ) ) {
wp_die( 0 );
}
$term = wp_unslash( $_GET['term'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$post_language = $this->model->get_language( sanitize_key( $_GET['post_language'] ) );
$translation_language = $this->model->get_language( sanitize_key( $_GET['translation_language'] ) );
$return = array();
$untranslated_posts = $this->model->post->get_untranslated( $post_type, $post_language, $translation_language, $term );
// format output
foreach ( $untranslated_posts as $post ) {
$return[] = array(
'id' => $post->ID,
'value' => $post->post_title,
'link' => $this->links->edit_post_translation_link( $post->ID ),
);
}
// Add current translation in list
if ( $post_id = $this->model->post->get_translation( (int) $_GET['pll_post_id'], $translation_language ) ) {
$post = get_post( $post_id );
if ( ! empty( $post ) ) {
array_unshift(
$return,
array(
'id' => $post_id,
'value' => $post->post_title,
'link' => $this->links->edit_post_translation_link( $post_id ),
)
);
}
}
wp_die( wp_json_encode( $return ) );
}
/**
* Filters the pages by language in the parent dropdown list in the page attributes metabox.
*
* @since 0.6
*
* @param array $dropdown_args Arguments passed to wp_dropdown_pages().
* @param WP_Post $post The page being edited.
* @return array Modified arguments.
*/
public function page_attributes_dropdown_pages_args( $dropdown_args, $post ) {
$language = isset( $_POST['lang'] ) ? $this->model->get_language( sanitize_key( $_POST['lang'] ) ) : $this->model->post->get_language( $post->ID ); // phpcs:ignore WordPress.Security.NonceVerification
if ( empty( $language ) ) {
$language = $this->pref_lang;
}
if ( ! empty( $language ) ) {
$dropdown_args['lang'] = $language->slug;
}
return $dropdown_args;
}
/**
* Displays a notice if the user has not sufficient rights to overwrite synchronized taxonomies and metas.
*
* @since 2.6
*
* @param WP_Post $post the post currently being edited.
* @return void
*/
public function edit_form_top( $post ) {
if ( ! $this->model->post->current_user_can_synchronize( $post->ID ) ) {
?>
<div class="pll-notice notice notice-warning">
<p>
<?php
esc_html_e( 'Some taxonomies or metadata may be synchronized with existing translations that you are not allowed to modify.', 'polylang' );
echo ' ';
esc_html_e( 'If you attempt to modify them anyway, your changes will not be saved.', 'polylang' );
?>
</p>
</div>
<?php
}
}
}

View File

@@ -0,0 +1,264 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to default terms.
*
* @since 3.1
*/
class PLL_Admin_Default_Term {
/**
* A reference to the PLL_Model instance.
*
* @var PLL_Model
*/
protected $model;
/**
* Preferred language to assign to new contents.
*
* @var PLL_Language|null
*/
protected $pref_lang;
/**
* Array of registered taxonomy names for which Polylang manages languages and translations.
*
* @var string[]
*/
protected $taxonomies;
/**
* Constructor: setups properties.
*
* @since 3.1
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->pref_lang = &$polylang->pref_lang;
$this->taxonomies = $this->model->get_translated_taxonomies();
}
/**
* Setups filters and actions needed.
*
* @since 3.1
*
* @return void
*/
public function add_hooks() {
foreach ( $this->taxonomies as $taxonomy ) {
if ( 'category' === $taxonomy ) {
// Allows to get the default terms in all languages
add_filter( 'option_default_' . $taxonomy, array( $this, 'option_default_term' ) );
add_action( 'update_option_default_' . $taxonomy, array( $this, 'update_option_default_term' ), 10, 2 );
// Adds the language column in the 'Terms' table.
add_filter( 'manage_' . $taxonomy . '_custom_column', array( $this, 'term_column' ), 10, 3 );
}
}
add_action( 'pll_add_language', array( $this, 'handle_default_term_on_create_language' ) );
// The default term should be in the default language
add_action( 'pll_update_default_lang', array( $this, 'update_default_term_language' ) );
// Prevents deleting all the translations of the default term
add_filter( 'map_meta_cap', array( $this, 'fix_delete_default_term' ), 10, 4 );
}
/**
* Filters the default term in note below the term list table and in settings->writing dropdown
*
* @since 1.2
*
* @param int $taxonomy_term_id The taxonomy term id.
* @return int A taxonomy term id.
*/
public function option_default_term( $taxonomy_term_id ) {
if ( isset( $this->pref_lang ) && $tr = $this->model->term->get( $taxonomy_term_id, $this->pref_lang ) ) {
$taxonomy_term_id = $tr;
}
return $taxonomy_term_id;
}
/**
* Checks if the new default term is translated in all languages
* If not, create the translations
*
* @since 1.7
*
* @param int $old_value The old option value.
* @param int $value The new option value.
* @return void
*/
public function update_option_default_term( $old_value, $value ) {
$default_cat_lang = $this->model->term->get_language( $value );
// Assign a default language to default term
if ( ! $default_cat_lang ) {
$default_cat_lang = $this->model->get_default_language();
$this->model->term->set_language( (int) $value, $default_cat_lang );
}
if ( empty( $default_cat_lang ) ) {
return;
}
$taxonomy = substr( current_filter(), 22 );
foreach ( $this->model->get_languages_list() as $language ) {
if ( $language->slug != $default_cat_lang->slug && ! $this->model->term->get_translation( $value, $language ) ) {
$this->create_default_term( $language, $taxonomy );
}
}
}
/**
* Create a default term for a language
*
* @since 1.2
*
* @param object|string|int $lang language
* @param string $taxonomy The current taxonomy
* @return void
*/
public function create_default_term( $lang, $taxonomy ) {
$lang = $this->model->get_language( $lang );
// create a new term
// FIXME this is translated in admin language when we would like it in $lang
$cat_name = __( 'Uncategorized', 'polylang' );
$cat_slug = sanitize_title( $cat_name . '-' . $lang->slug );
$cat = wp_insert_term( $cat_name, $taxonomy, array( 'slug' => $cat_slug ) );
// check that the term was not previously created ( in case the language was deleted and recreated )
$cat = isset( $cat->error_data['term_exists'] ) ? $cat->error_data['term_exists'] : $cat['term_id'];
// set language
$this->model->term->set_language( (int) $cat, $lang );
// this is a translation of the default term
$default = (int) get_option( 'default_' . $taxonomy );
$translations = $this->model->term->get_translations( $default );
$this->model->term->save_translations( (int) $cat, $translations );
}
/**
* Manages the default term when new languages are created.
*
* @since 3.1
*
* @param array $args Argument used to create the language. @see PLL_Admin_Model::add_language().
* @return void
*/
public function handle_default_term_on_create_language( $args ) {
foreach ( $this->taxonomies as $taxonomy ) {
if ( 'category' === $taxonomy ) {
$default = (int) get_option( 'default_' . $taxonomy );
// Assign default language to default term
if ( ! $this->model->term->get_language( $default ) ) {
$this->model->term->set_language( $default, $args['slug'] );
} elseif ( empty( $args['no_default_cat'] ) && ! $this->model->term->get( $default, $args['slug'] ) ) {
$this->create_default_term( $args['slug'], $taxonomy );
}
}
}
}
/**
* Identify the default term in the terms list table to disable the language dropdown in js.
*
* @since 3.1
*
* @param string $out The output.
* @param string $column The custom column's name.
* @param int $term_id The term id.
* @return string The HTML string.
*/
public function term_column( $out, $column, $term_id ) {
if ( $column === $this->get_first_language_column() && $this->is_default_term( $term_id ) ) {
$out .= sprintf( '<div class="hidden" id="default_cat_%1$d">%1$d</div>', intval( $term_id ) );
}
return $out;
}
/**
* Returns the first language column in the posts, pages and media library tables
*
* @since 0.9
*
* @return string first language column name
*/
protected function get_first_language_column() {
$columns = array();
foreach ( $this->model->get_languages_list() as $language ) {
$columns[] = 'language_' . $language->slug;
}
return empty( $columns ) ? '' : reset( $columns );
}
/**
* Prevents deleting all the translations of the default term
*
* @since 2.1
*
* @param array $caps The user's actual capabilities.
* @param string $cap Capability name.
* @param int $user_id The user ID.
* @param array $args Adds the context to the cap. The term id.
* @return array
*/
public function fix_delete_default_term( $caps, $cap, $user_id, $args ) {
if ( 'delete_term' === $cap && $this->is_default_term( reset( $args ) ) ) {
$caps[] = 'do_not_allow';
}
return $caps;
}
/**
* Check if the term is the default term.
*
* @since 3.1
*
* @param int $term_id The term id.
* @return bool True if the term is the default term, false otherwise.
*/
public function is_default_term( $term_id ) {
$term = get_term( $term_id );
if ( $term instanceof WP_Term ) {
$default_term_id = get_option( 'default_' . $term->taxonomy );
return $default_term_id && in_array( $default_term_id, $this->model->term->get_translations( $term_id ) );
}
return false;
}
/**
* Updates the default term language.
*
* @since 3.1
*
* @param string $slug Language slug.
* @return void
*/
public function update_default_term_language( $slug ) {
foreach ( $this->taxonomies as $taxonomy ) {
if ( 'category' === $taxonomy ) {
$default_cats = $this->model->term->get_translations( get_option( 'default_' . $taxonomy ) );
if ( isset( $default_cats[ $slug ] ) ) {
update_option( 'default_' . $taxonomy, $default_cats[ $slug ] );
}
}
}
}
}

View File

@@ -0,0 +1,451 @@
<?php
/**
* @package Polylang
*/
/**
* Adds the language column in posts and terms list tables
* Manages quick edit and bulk edit as well
*
* @since 1.2
*/
class PLL_Admin_Filters_Columns {
/**
* @var PLL_Model
*/
public $model;
/**
* @var PLL_Admin_Links|null
*/
public $links;
/**
* Language selected in the admin language filter.
*
* @var PLL_Language|null
*/
public $filter_lang;
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links = &$polylang->links;
$this->model = &$polylang->model;
$this->filter_lang = &$polylang->filter_lang;
// Hide the column of the filtered language.
add_filter( 'hidden_columns', array( $this, 'hidden_columns' ) ); // Since WP 4.4.
// Add the language and translations columns in 'All Posts', 'All Pages' and 'Media library' panels.
foreach ( $this->model->get_translated_post_types() as $type ) {
// Use the latest filter late as some plugins purely overwrite what's done by others :(
// Specific case for media.
add_filter( 'manage_' . ( 'attachment' == $type ? 'upload' : 'edit-' . $type ) . '_columns', array( $this, 'add_post_column' ), 100 );
add_action( 'manage_' . ( 'attachment' == $type ? 'media' : $type . '_posts' ) . '_custom_column', array( $this, 'post_column' ), 10, 2 );
}
// Quick edit and bulk edit.
add_filter( 'quick_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );
add_filter( 'bulk_edit_custom_box', array( $this, 'quick_edit_custom_box' ) );
// Adds the language column in the 'Categories' and 'Post Tags' tables.
foreach ( $this->model->get_translated_taxonomies() as $tax ) {
add_filter( 'manage_edit-' . $tax . '_columns', array( $this, 'add_term_column' ) );
add_filter( 'manage_' . $tax . '_custom_column', array( $this, 'term_column' ), 10, 3 );
}
// Ajax responses to update list table rows.
add_action( 'wp_ajax_pll_update_post_rows', array( $this, 'ajax_update_post_rows' ) );
add_action( 'wp_ajax_pll_update_term_rows', array( $this, 'ajax_update_term_rows' ) );
}
/**
* Adds languages and translations columns in posts, pages, media, categories and tags tables.
*
* @since 0.8.2
*
* @param string[] $columns List of table columns.
* @param string $before The column before which we want to add our languages.
* @return string[] Modified list of columns.
*/
protected function add_column( $columns, $before ) {
if ( $n = array_search( $before, array_keys( $columns ) ) ) {
$end = array_slice( $columns, $n );
$columns = array_slice( $columns, 0, $n );
}
foreach ( $this->model->get_languages_list() as $language ) {
$columns[ 'language_' . $language->slug ] = $this->get_flag_html( $language ) . '<span class="screen-reader-text">' . esc_html( $language->name ) . '</span>';
}
return isset( $end ) ? array_merge( $columns, $end ) : $columns;
}
/**
* Returns the first language column in the posts, pages and media library tables
*
* @since 0.9
*
* @return string first language column name
*/
protected function get_first_language_column() {
$columns = array();
foreach ( $this->model->get_languages_list() as $language ) {
$columns[] = 'language_' . $language->slug;
}
return empty( $columns ) ? '' : reset( $columns );
}
/**
* Hides the column for the filtered language.
*
* @since 2.7
*
* @param string[] $hidden Array of hidden columns.
* @return string[]
*/
public function hidden_columns( $hidden ) {
if ( ! empty( $this->filter_lang ) ) {
$hidden[] = 'language_' . $this->filter_lang->slug;
}
return $hidden;
}
/**
* Adds the language and translations columns ( before the comments column ) in the posts, pages and media library tables.
*
* @since 0.1
*
* @param string[] $columns List of posts table columns.
* @return string[] Modified list of columns.
*/
public function add_post_column( $columns ) {
return $this->add_column( $columns, 'comments' );
}
/**
* Fills the language and translations columns in the posts, pages and media library tables
* take care that when doing ajax inline edit, the post may not be updated in database yet
*
* @since 0.1
*
* @param string $column Column name.
* @param int $post_id Post ID.
* @return void
*/
public function post_column( $column, $post_id ) {
$inline = wp_doing_ajax() && isset( $_REQUEST['action'], $_POST['inline_lang_choice'] ) && 'inline-save' === $_REQUEST['action']; // phpcs:ignore WordPress.Security.NonceVerification
$lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->post->get_language( $post_id ); // phpcs:ignore WordPress.Security.NonceVerification
if ( false === strpos( $column, 'language_' ) || ! $lang ) {
return;
}
$language = $this->model->get_language( substr( $column, 9 ) );
if ( empty( $language ) ) {
return;
}
// Hidden field containing the post language for quick edit
if ( $column == $this->get_first_language_column() ) {
printf( '<div class="hidden" id="lang_%d">%s</div>', intval( $post_id ), esc_html( $lang->slug ) );
}
// Link to edit post ( or a translation )
if ( $id = $this->model->post->get( $post_id, $language ) ) {
// get_edit_post_link returns nothing if the user cannot edit the post
// Thanks to Solinx. See http://wordpress.org/support/topic/feature-request-incl-code-check-for-capabilities-in-admin-screens
if ( $link = get_edit_post_link( $id ) ) {
$flag = '';
if ( $id === $post_id ) {
$flag = $this->get_flag_html( $language );
$class = 'pll_column_flag';
/* translators: accessibility text, %s is a native language name */
$s = sprintf( __( 'Edit this item in %s', 'polylang' ), $language->name );
} else {
$class = esc_attr( 'pll_icon_edit translation_' . $id );
/* translators: accessibility text, %s is a native language name */
$s = sprintf( __( 'Edit the translation in %s', 'polylang' ), $language->name );
}
$post = get_post( $id );
if ( ! empty( $post ) ) {
printf(
'<a class="%1$s" title="%2$s" href="%3$s"><span class="screen-reader-text">%4$s</span>%5$s</a>',
esc_attr( $class ),
esc_attr( $post->post_title ),
esc_url( $link ),
esc_html( $s ),
$flag // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
} elseif ( $id === $post_id ) {
printf(
'<span class="pll_column_flag" style=""><span class="screen-reader-text">%1$s</span>%2$s</span>',
/* translators: accessibility text, %s is a native language name */
esc_html( sprintf( __( 'This item is in %s', 'polylang' ), $language->name ) ),
$this->get_flag_html( $language ) // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
}
// Link to add a new translation
else {
echo $this->links->new_post_translation_link( $post_id, $language ); // phpcs:ignore WordPress.Security.EscapeOutput
}
}
/**
* Quick edit & bulk edit
*
* @since 0.9
*
* @param string $column column name
* @return string unmodified $column
*/
public function quick_edit_custom_box( $column ) {
if ( $column == $this->get_first_language_column() ) {
$elements = $this->model->get_languages_list();
if ( current_filter() == 'bulk_edit_custom_box' ) {
array_unshift( $elements, (object) array( 'slug' => -1, 'name' => __( '&mdash; No Change &mdash;', 'polylang' ) ) );
}
$dropdown = new PLL_Walker_Dropdown();
// The hidden field 'old_lang' allows to pass the old language to ajax request
printf(
'<fieldset class="inline-edit-col-left">
<div class="inline-edit-col">
<label class="alignleft">
<span class="title">%s</span>
%s
</label>
</div>
</fieldset>',
esc_html__( 'Language', 'polylang' ),
$dropdown->walk( $elements, -1, array( 'name' => 'inline_lang_choice', 'id' => '' ) ) // phpcs:ignore WordPress.Security.EscapeOutput
);
}
return $column;
}
/**
* Adds the language column ( before the posts column ) in the 'Categories' or 'Post Tags' table.
*
* @since 0.1
*
* @param string[] $columns List of terms table columns.
* @return string[] modified List of columns.
*/
public function add_term_column( $columns ) {
$screen = get_current_screen();
// Avoid displaying languages in screen options when editing a term.
if ( $screen instanceof WP_Screen && 'term' === $screen->base ) {
return $columns;
}
return $this->add_column( $columns, 'posts' );
}
/**
* Fills the language column in the taxonomy terms list table.
*
* @since 0.1
*
* @param string $out Column output.
* @param string $column Column name.
* @param int $term_id Term ID.
* @return string
*/
public function term_column( $out, $column, $term_id ) {
$inline = wp_doing_ajax() && isset( $_REQUEST['action'], $_POST['inline_lang_choice'] ) && 'inline-save-tax' === $_REQUEST['action']; // phpcs:ignore WordPress.Security.NonceVerification
if ( false === strpos( $column, 'language_' ) || ! ( $lang = $inline ? $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) : $this->model->term->get_language( $term_id ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return $out;
}
if ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $GLOBALS['post_type'] ) ) {
$post_type = $GLOBALS['post_type'];
}
if ( isset( $_REQUEST['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$taxonomy = sanitize_key( $_REQUEST['taxonomy'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $GLOBALS['taxonomy'] ) ) {
$taxonomy = $GLOBALS['taxonomy'];
}
if ( ! isset( $taxonomy, $post_type ) || ! post_type_exists( $post_type ) || ! taxonomy_exists( $taxonomy ) ) {
return $out;
}
$term_id = (int) $term_id;
$language = $this->model->get_language( substr( $column, 9 ) );
if ( empty( $language ) ) {
return $out;
}
if ( $column == $this->get_first_language_column() ) {
$out .= sprintf( '<div class="hidden" id="lang_%d">%s</div>', intval( $term_id ), esc_html( $lang->slug ) );
}
// Link to edit term ( or a translation )
if ( ( $id = $this->model->term->get( $term_id, $language ) ) && $term = get_term( $id, $taxonomy ) ) {
if ( $term instanceof WP_Term && $link = get_edit_term_link( $id, $taxonomy, $post_type ) ) {
$flag = '';
if ( $id === $term_id ) {
$flag = $this->get_flag_html( $language );
$class = 'pll_column_flag';
/* translators: accessibility text, %s is a native language name */
$s = sprintf( __( 'Edit this item in %s', 'polylang' ), $language->name );
} else {
$class = esc_attr( 'pll_icon_edit translation_' . $id );
/* translators: accessibility text, %s is a native language name */
$s = sprintf( __( 'Edit the translation in %s', 'polylang' ), $language->name );
}
$out .= sprintf(
'<a class="%1$s" title="%2$s" href="%3$s"><span class="screen-reader-text">%4$s</span>%5$s</a>',
$class,
esc_attr( $term->name ),
esc_url( $link ),
esc_html( $s ),
$flag // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
);
} elseif ( $id === $term_id ) {
$out .= sprintf(
'<span class="pll_column_flag"><span class="screen-reader-text">%1$s</span>%2$s</span>',
/* translators: accessibility text, %s is a native language name */
esc_html( sprintf( __( 'This item is in %s', 'polylang' ), $language->name ) ),
$this->get_flag_html( $language ) // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
}
// Link to add a new translation
else {
$out .= $this->links->new_term_translation_link( $term_id, $taxonomy, $post_type, $language );
}
return $out;
}
/**
* Update rows of translated posts when the language is modified in quick edit
*
* @since 1.7
*
* @return void
*/
public function ajax_update_post_rows() {
check_ajax_referer( 'inlineeditnonce', '_pll_nonce' );
if ( ! isset( $_POST['post_type'], $_POST['post_id'], $_POST['screen'] ) ) {
wp_die( 0 );
}
$post_type = sanitize_key( $_POST['post_type'] );
if ( ! post_type_exists( $post_type ) || ! $this->model->is_translated_post_type( $post_type ) ) {
wp_die( 0 );
}
/** @var WP_Posts_List_Table $wp_list_table */
$wp_list_table = _get_list_table( 'WP_Posts_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
$x = new WP_Ajax_Response();
// Collect old translations
$translations = empty( $_POST['translations'] ) ? array() : explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$translations = array_map( 'intval', $translations );
$translations = array_merge( $translations, array( (int) $_POST['post_id'] ) ); // Add current post
foreach ( $translations as $post_id ) {
$level = is_post_type_hierarchical( $post_type ) ? count( get_ancestors( $post_id, $post_type ) ) : 0;
if ( $post = get_post( $post_id ) ) {
ob_start();
$wp_list_table->single_row( $post, $level );
$data = ob_get_clean();
$x->add( array( 'what' => 'row', 'data' => $data, 'supplemental' => array( 'post_id' => $post_id ) ) );
}
}
$x->send();
}
/**
* Update rows of translated terms when adding / deleting a translation or when the language is modified in quick edit
*
* @since 1.7
*
* @return void
*/
public function ajax_update_term_rows() {
check_ajax_referer( 'pll_language', '_pll_nonce' );
if ( ! isset( $_POST['taxonomy'], $_POST['term_id'], $_POST['screen'] ) ) {
wp_die( 0 );
}
$taxonomy = sanitize_key( $_POST['taxonomy'] );
if ( ! taxonomy_exists( $taxonomy ) || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
wp_die( 0 );
}
/** @var WP_Terms_List_Table $wp_list_table */
$wp_list_table = _get_list_table( 'WP_Terms_List_Table', array( 'screen' => sanitize_key( $_POST['screen'] ) ) );
$x = new WP_Ajax_Response();
// Collect old translations
$translations = empty( $_POST['translations'] ) ? array() : explode( ',', $_POST['translations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$translations = array_map( 'intval', $translations );
$translations = array_merge( $translations, $this->model->term->get_translations( (int) $_POST['term_id'] ) ); // Add current translations
$translations = array_unique( $translations ); // Remove duplicates
foreach ( $translations as $term_id ) {
$level = is_taxonomy_hierarchical( $taxonomy ) ? count( get_ancestors( $term_id, $taxonomy ) ) : 0;
$tag = get_term( $term_id, $taxonomy );
if ( ! $tag instanceof WP_Term ) {
continue;
}
ob_start();
$wp_list_table->single_row( $tag, $level );
$data = ob_get_clean();
$x->add( array( 'what' => 'row', 'data' => $data, 'supplemental' => array( 'term_id' => $term_id ) ) );
}
$x->send();
}
/**
* Returns the language flag or the language slug if there is no flag.
*
* @since 2.8
*
* @param PLL_Language $language PLL_Language object.
* @return string
*/
protected function get_flag_html( $language ) {
return $language->flag ? $language->flag : sprintf( '<abbr>%s</abbr>', esc_html( $language->slug ) );
}
}

View File

@@ -0,0 +1,132 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to media on admin side
* Capability to edit / create media is checked before loading this class
*
* @since 1.2
*/
class PLL_Admin_Filters_Media extends PLL_Admin_Filters_Post_Base {
/**
* @var PLL_CRUD_Posts|null
*/
public $posts;
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->posts = &$polylang->posts;
// Adds the language field and translations tables in the 'Edit Media' panel
add_filter( 'attachment_fields_to_edit', array( $this, 'attachment_fields_to_edit' ), 10, 2 );
// Adds actions related to languages when creating, saving or deleting media
add_filter( 'attachment_fields_to_save', array( $this, 'save_media' ), 10, 2 );
// Creates a media translation
if ( isset( $_GET['action'], $_GET['new_lang'], $_GET['from_media'] ) && 'translate_media' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification
add_action( 'admin_init', array( $this, 'translate_media' ) );
}
}
/**
* Adds the language field and translations tables in the 'Edit Media' panel.
* Needs WP 3.5+
*
* @since 0.9
*
* @param array $fields List of form fields.
* @param WP_Post $post The attachment being edited.
* @return array Modified list of form fields.
*/
public function attachment_fields_to_edit( $fields, $post ) {
if ( 'post.php' == $GLOBALS['pagenow'] ) {
return $fields; // Don't add anything on edit media panel for WP 3.5+ since we have the metabox
}
$post_id = $post->ID;
$lang = $this->model->post->get_language( $post_id );
$dropdown = new PLL_Walker_Dropdown();
$fields['language'] = array(
'label' => __( 'Language', 'polylang' ),
'input' => 'html',
'html' => $dropdown->walk(
$this->model->get_languages_list(),
-1,
array(
'name' => sprintf( 'attachments[%d][language]', $post_id ),
'class' => 'media_lang_choice',
'selected' => $lang ? $lang->slug : '',
)
),
);
return $fields;
}
/**
* Creates a media translation
*
* @since 0.9
*
* @return void
*/
public function translate_media() {
if ( isset( $_GET['from_media'], $_GET['new_lang'] ) ) {
// Security check
check_admin_referer( 'translate_media' );
$post_id = (int) $_GET['from_media'];
// Bails if the translations already exists
// See https://wordpress.org/support/topic/edit-translation-in-media-attachments?#post-7322303
// Or if the source media does not exist
if ( $this->model->post->get_translation( $post_id, sanitize_key( $_GET['new_lang'] ) ) || ! get_post( $post_id ) ) {
wp_safe_redirect( wp_get_referer() );
exit;
}
$tr_id = $this->posts->create_media_translation( $post_id, sanitize_key( $_GET['new_lang'] ) );
wp_safe_redirect( admin_url( sprintf( 'post.php?post=%d&action=edit', $tr_id ) ) ); // WP 3.5+
exit;
}
}
/**
* Called when a media is saved
* Saves language and translations
*
* @since 0.9
*
* @param array $post An array of post data.
* @param array $attachment An array of attachment metadata.
* @return array Unmodified $post
*/
public function save_media( $post, $attachment ) {
// Language is filled in attachment by the function applying the filter 'attachment_fields_to_save'
// All security checks have been done by functions applying this filter
if ( empty( $attachment['language'] ) || ! current_user_can( 'edit_post', $post['ID'] ) ) {
return $post;
}
$language = $this->model->get_language( $attachment['language'] );
if ( empty( $language ) ) {
return $post;
}
$this->model->post->set_language( $post['ID'], $language );
return $post;
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* @package Polylang
*/
/**
* Some common code for PLL_Admin_Filters_Post and PLL_Admin_Filters_Media
*
* @since 1.5
*/
abstract class PLL_Admin_Filters_Post_Base {
/**
* @var PLL_Model
*/
public $model;
/**
* @var PLL_Links|null
*/
public $links;
/**
* 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;
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links = &$polylang->links;
$this->model = &$polylang->model;
$this->pref_lang = &$polylang->pref_lang;
}
/**
* Save translations from the languages metabox.
*
* @since 1.5
*
* @param int $post_id Post id of the post being saved.
* @param int[] $arr An array with language codes as key and post id as value.
* @return int[] The array of translated post ids.
*/
protected function save_translations( $post_id, $arr ) {
// Security check as 'wp_insert_post' can be called from outside WP admin.
check_admin_referer( 'pll_language', '_pll_nonce' );
$translations = $this->model->post->save_translations( $post_id, $arr );
return $translations;
}
}

View File

@@ -0,0 +1,223 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to posts on admin side
*
* @since 1.2
*/
class PLL_Admin_Filters_Post extends PLL_Admin_Filters_Post_Base {
/**
* Current language (used to filter the content).
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->curlang = &$polylang->curlang;
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
// Filters posts, pages and media by language
add_action( 'parse_query', array( $this, 'parse_query' ) );
// Adds actions and filters related to languages when creating, saving or deleting posts and pages
add_action( 'load-post.php', array( $this, 'edit_post' ) );
add_action( 'load-edit.php', array( $this, 'bulk_edit_posts' ) );
add_action( 'wp_ajax_inline-save', array( $this, 'inline_edit_post' ), 0 ); // Before WordPress
// Sets the language in Tiny MCE
add_filter( 'tiny_mce_before_init', array( $this, 'tiny_mce_before_init' ) );
}
/**
* Outputs a javascript list of terms ordered by language and hierarchical taxonomies
* to filter the category checklist per post language in quick edit
* Outputs a javascript list of pages ordered by language
* to filter the parent dropdown per post language in quick edit
*
* @since 1.7
*
* @return void
*/
public function admin_enqueue_scripts() {
$screen = get_current_screen();
if ( empty( $screen ) ) {
return;
}
// Hierarchical taxonomies
if ( 'edit' == $screen->base && $taxonomies = get_object_taxonomies( $screen->post_type, 'objects' ) ) {
// Get translated hierarchical taxonomies
$hierarchical_taxonomies = array();
foreach ( $taxonomies as $taxonomy ) {
if ( $taxonomy->hierarchical && $taxonomy->show_in_quick_edit && $this->model->is_translated_taxonomy( $taxonomy->name ) ) {
$hierarchical_taxonomies[] = $taxonomy->name;
}
}
if ( ! empty( $hierarchical_taxonomies ) ) {
$terms = get_terms( array( 'taxonomy' => $hierarchical_taxonomies, 'get' => 'all' ) );
$term_languages = array();
if ( is_array( $terms ) ) {
foreach ( $terms as $term ) {
if ( $lang = $this->model->term->get_language( $term->term_id ) ) {
$term_languages[ $lang->slug ][ $term->taxonomy ][] = $term->term_id;
}
}
}
// Send all these data to javascript
if ( ! empty( $term_languages ) ) {
wp_localize_script( 'pll_post', 'pll_term_languages', $term_languages );
}
}
}
// Hierarchical post types
if ( 'edit' == $screen->base && is_post_type_hierarchical( $screen->post_type ) ) {
$pages = get_pages( array( 'sort_column' => 'menu_order, post_title' ) ); // Same arguments as the parent pages dropdown to avoid an extra query.
update_post_caches( $pages, $screen->post_type, true, false );
$page_languages = array();
foreach ( $pages as $page ) {
if ( $lang = $this->model->post->get_language( $page->ID ) ) {
$page_languages[ $lang->slug ][] = $page->ID;
}
}
// Send all these data to javascript
if ( ! empty( $page_languages ) ) {
wp_localize_script( 'pll_post', 'pll_page_languages', $page_languages );
}
}
}
/**
* Filters posts, pages and media by language.
*
* @since 0.1
*
* @param WP_Query $query WP_Query object.
* @return void
*/
public function parse_query( $query ) {
$pll_query = new PLL_Query( $query, $this->model );
$pll_query->filter_query( $this->curlang );
}
/**
* Save language and translation when editing a post (post.php)
*
* @since 2.3
*
* @return void
*/
public function edit_post() {
if ( isset( $_POST['post_lang_choice'], $_POST['post_ID'] ) && $post_id = (int) $_POST['post_ID'] ) { // phpcs:ignore WordPress.Security.NonceVerification
check_admin_referer( 'pll_language', '_pll_nonce' );
$post = get_post( $post_id );
if ( empty( $post ) ) {
return;
}
$post_type_object = get_post_type_object( $post->post_type );
if ( empty( $post_type_object ) ) {
return;
}
if ( ! current_user_can( $post_type_object->cap->edit_post, $post_id ) ) {
return;
}
$language = $this->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) );
if ( empty( $language ) ) {
return;
}
$this->model->post->set_language( $post_id, $language );
if ( ! isset( $_POST['post_tr_lang'] ) ) {
return;
}
$this->save_translations( $post_id, array_map( 'absint', $_POST['post_tr_lang'] ) );
}
}
/**
* Save language when bulk editing a post
*
* @since 2.3
*
* @return void
*/
public function bulk_edit_posts() {
if ( isset( $_GET['bulk_edit'], $_GET['inline_lang_choice'], $_REQUEST['post'] ) && -1 !== $_GET['inline_lang_choice'] ) { // phpcs:ignore WordPress.Security.NonceVerification
check_admin_referer( 'bulk-posts' );
if ( $lang = $this->model->get_language( sanitize_key( $_GET['inline_lang_choice'] ) ) ) {
$post_ids = array_map( 'intval', (array) $_REQUEST['post'] );
foreach ( $post_ids as $post_id ) {
if ( current_user_can( 'edit_post', $post_id ) ) {
$this->model->post->set_language( $post_id, $lang );
}
}
}
}
}
/**
* Save language when inline editing a post
*
* @since 2.3
*
* @return void
*/
public function inline_edit_post() {
check_admin_referer( 'inlineeditnonce', '_inline_edit' );
if ( isset( $_POST['post_ID'], $_POST['inline_lang_choice'] ) ) {
$post_id = (int) $_POST['post_ID'];
$lang = $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) );
if ( $post_id && $lang && current_user_can( 'edit_post', $post_id ) ) {
$this->model->post->set_language( $post_id, $lang );
}
}
}
/**
* Sets the language attribute and text direction for Tiny MCE
*
* @since 2.2
*
* @param array $mce_init TinyMCE config
* @return array
*/
public function tiny_mce_before_init( $mce_init ) {
if ( ! empty( $this->curlang ) ) {
$mce_init['wp_lang_attr'] = $this->curlang->get_locale( 'display' );
$mce_init['directionality'] = $this->curlang->is_rtl ? 'rtl' : 'ltr';
}
return $mce_init;
}
}

View File

@@ -0,0 +1,716 @@
<?php
/**
* @package Polylang
*/
/**
* Manages filters and actions related to terms on admin side
*
* @since 1.2
*/
class PLL_Admin_Filters_Term {
/**
* @var PLL_Model
*/
public $model;
/**
* @var PLL_Admin_Links|null
*/
public $links;
/**
* Language selected in the admin language filter.
*
* @var PLL_Language|null
*/
public $filter_lang;
/**
* Preferred language to assign to the new terms.
*
* @var PLL_Language|null
*/
public $pref_lang;
/**
* Stores the current post_id when bulk editing posts.
*
* @var int
*/
protected $post_id = 0;
/**
* A reference to the PLL_Admin_Default_Term instance.
*
* @since 2.8
*
* @var PLL_Admin_Default_Term|null
*/
protected $default_term;
/**
* Constructor: setups filters and actions.
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links = &$polylang->links;
$this->model = &$polylang->model;
$this->pref_lang = &$polylang->pref_lang;
$this->default_term = &$polylang->default_term;
foreach ( $this->model->get_translated_taxonomies() as $tax ) {
// Adds the language field in the 'Categories' and 'Post Tags' panels
add_action( $tax . '_add_form_fields', array( $this, 'add_term_form' ) );
// Adds the language field and translations tables in the 'Edit Category' and 'Edit Tag' panels
add_action( $tax . '_edit_form_fields', array( $this, 'edit_term_form' ) );
}
// Adds actions related to languages when creating or saving categories and post tags
add_filter( 'wp_dropdown_cats', array( $this, 'wp_dropdown_cats' ) );
add_action( 'create_term', array( $this, 'save_term' ), 900, 3 );
add_action( 'edit_term', array( $this, 'save_term' ), 900, 3 ); // Late as it may conflict with other plugins, see http://wordpress.org/support/topic/polylang-and-wordpress-seo-by-yoast
add_action( 'pre_post_update', array( $this, 'pre_post_update' ) );
add_filter( 'pll_inserted_term_language', array( $this, 'get_inserted_term_language' ) );
add_filter( 'pll_inserted_term_parent', array( $this, 'get_inserted_term_parent' ), 10, 2 );
// Ajax response for edit term form
add_action( 'wp_ajax_term_lang_choice', array( $this, 'term_lang_choice' ) );
add_action( 'wp_ajax_pll_terms_not_translated', array( $this, 'ajax_terms_not_translated' ) );
// Updates the translations term ids when splitting a shared term
add_action( 'split_shared_term', array( $this, 'split_shared_term' ), 10, 4 ); // WP 4.2
}
/**
* Adds the language field in the 'Categories' and 'Post Tags' panels
*
* @since 0.1
*
* @return void
*/
public function add_term_form() {
if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$taxonomy = sanitize_key( $_GET['taxonomy'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $GLOBALS['post_type'] ) ) {
$post_type = $GLOBALS['post_type'];
}
if ( ! isset( $taxonomy, $post_type ) || ! taxonomy_exists( $taxonomy ) || ! post_type_exists( $post_type ) ) {
return;
}
$from_term_id = isset( $_GET['from_tag'] ) ? (int) $_GET['from_tag'] : 0; // phpcs:ignore WordPress.Security.NonceVerification
$lang = isset( $_GET['new_lang'] ) ? $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) : $this->pref_lang; // phpcs:ignore WordPress.Security.NonceVerification
$dropdown = new PLL_Walker_Dropdown();
$dropdown_html = $dropdown->walk(
$this->model->get_languages_list(),
-1,
array(
'name' => 'term_lang_choice',
'value' => 'term_id',
'selected' => $lang ? $lang->term_id : '',
'flag' => true,
)
);
wp_nonce_field( 'pll_language', '_pll_nonce' );
printf(
'<div class="form-field">
<label for="term_lang_choice">%s</label>
<div id="select-add-term-language">%s</div>
<p>%s</p>
</div>',
esc_html__( 'Language', 'polylang' ),
$dropdown_html, // phpcs:ignore
esc_html__( 'Sets the language', 'polylang' )
);
if ( ! empty( $from_term_id ) ) {
printf( '<input type="hidden" name="from_tag" value="%d" />', (int) $from_term_id );
}
// Adds translation fields
echo '<div id="term-translations" class="form-field">';
if ( $lang ) {
include __DIR__ . '/view-translations-term.php';
}
echo '</div>' . "\n";
}
/**
* Adds the language field and translations tables in the 'Edit Category' and 'Edit Tag' panels.
*
* @since 0.1
*
* @param WP_Term $tag The term being edited.
* @return void
*/
public function edit_term_form( $tag ) {
if ( isset( $_REQUEST['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$post_type = sanitize_key( $_REQUEST['post_type'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $GLOBALS['post_type'] ) ) {
$post_type = $GLOBALS['post_type'];
}
if ( ! isset( $post_type ) || ! post_type_exists( $post_type ) ) {
return;
}
$term_id = $tag->term_id;
$taxonomy = $tag->taxonomy; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$lang = $this->model->term->get_language( $term_id );
$lang = empty( $lang ) ? $this->pref_lang : $lang;
// Disable the language dropdown and the translations input fields for default terms to prevent removal
$disabled = $this->default_term->is_default_term( $term_id );
$dropdown = new PLL_Walker_Dropdown();
$dropdown_html = $dropdown->walk(
$this->model->get_languages_list(),
-1,
array(
'name' => 'term_lang_choice',
'value' => 'term_id',
'selected' => $lang->term_id,
'disabled' => $disabled,
'flag' => true,
)
);
wp_nonce_field( 'pll_language', '_pll_nonce' );
printf(
'<tr class="form-field">
<th scope="row">
<label for="term_lang_choice">%s</label>
</th>
<td id="select-edit-term-language">
%s<br />
<p class="description">%s</p>
</td>
</tr>',
esc_html__( 'Language', 'polylang' ),
$dropdown_html, // phpcs:ignore
esc_html__( 'Sets the language', 'polylang' )
);
echo '<tr id="term-translations" class="form-field">';
include __DIR__ . '/view-translations-term.php';
echo '</tr>' . "\n";
}
/**
* Translates term parent if exists when using "Add new" ( translation )
*
* @since 0.7
*
* @param string $output html markup for dropdown list of categories
* @return string modified html
*/
public function wp_dropdown_cats( $output ) {
if ( isset( $_GET['taxonomy'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$taxonomy = sanitize_key( $_GET['taxonomy'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
if ( isset( $taxonomy, $_GET['from_tag'], $_GET['new_lang'] ) && taxonomy_exists( $taxonomy ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$term = get_term( (int) $_GET['from_tag'], $taxonomy ); // phpcs:ignore WordPress.Security.NonceVerification
if ( $term instanceof WP_Term && $id = $term->parent ) {
$lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
if ( $parent = $this->model->term->get_translation( $id, $lang ) ) {
return str_replace( '"' . $parent . '"', '"' . $parent . '" selected="selected"', $output );
}
}
}
return $output;
}
/**
* Stores the current post_id when bulk editing posts for use in save_language and get_inserted_term_language.
*
* @since 1.7
*
* @param int $post_id Post ID.
* @return void
*/
public function pre_post_update( $post_id ) {
if ( isset( $_GET['bulk_edit'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->post_id = $post_id;
}
}
/**
* Saves the language of a term.
*
* @since 1.5
*
* @param int $term_id Term ID.
* @param string $taxonomy Taxonomy name.
* @return void
*/
protected function save_language( $term_id, $taxonomy ) {
global $wpdb;
// Security checks are necessary to accept language modifications
// as 'wp_update_term' can be called from outside WP admin
// Edit tags
if ( isset( $_POST['term_lang_choice'] ) ) {
if ( isset( $_POST['action'] ) && sanitize_key( $_POST['action'] ) === 'add-' . $taxonomy ) { // phpcs:ignore WordPress.Security.NonceVerification
check_ajax_referer( 'add-' . $taxonomy, '_ajax_nonce-add-' . $taxonomy ); // Category metabox
} else {
check_admin_referer( 'pll_language', '_pll_nonce' ); // Edit tags or tags metabox
}
$language = $this->model->get_language( sanitize_key( $_POST['term_lang_choice'] ) );
if ( ! empty( $language ) ) {
$this->model->term->set_language( $term_id, $language );
}
}
// *Post* bulk edit, in case a new term is created
elseif ( isset( $_GET['bulk_edit'], $_GET['inline_lang_choice'] ) ) {
check_admin_referer( 'bulk-posts' );
// Bulk edit does not modify the language
// So we possibly create a tag in several languages
if ( -1 === (int) $_GET['inline_lang_choice'] ) {
// The language of the current term is set a according to the language of the current post.
$language = $this->model->post->get_language( $this->post_id );
if ( empty( $language ) ) {
return;
}
$this->model->term->set_language( $term_id, $language );
$term = get_term( $term_id, $taxonomy );
$terms = array();
// Get all terms with the same name
// FIXME backward compatibility WP < 4.2
// No WP function to get all terms with the exact same name so let's use a custom query
// $terms = get_terms( $taxonomy, array( 'name' => $term->name, 'hide_empty' => false, 'fields' => 'ids' ) ); should be OK in 4.2
// I may need to rework the loop below
if ( $term instanceof WP_Term ) {
$terms = $wpdb->get_results(
$wpdb->prepare(
"SELECT t.term_id FROM $wpdb->terms AS t
INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = %s AND t.name = %s",
$taxonomy,
$term->name
)
);
}
// If we have several terms with the same name, they are translations of each other
if ( count( $terms ) > 1 ) {
$translations = array();
foreach ( $terms as $term ) {
$translations[ $this->model->term->get_language( $term->term_id )->slug ] = $term->term_id;
}
$this->model->term->save_translations( $term_id, $translations );
}
}
elseif ( current_user_can( 'edit_term', $term_id ) ) {
$this->model->term->set_language( $term_id, $this->model->get_language( sanitize_key( $_GET['inline_lang_choice'] ) ) );
}
}
// Quick edit
elseif ( isset( $_POST['inline_lang_choice'] ) ) {
check_ajax_referer(
isset( $_POST['action'] ) && 'inline-save' == $_POST['action'] ? 'inlineeditnonce' : 'taxinlineeditnonce', // Post quick edit or tag quick edit ?
'_inline_edit'
);
$lang = $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) );
$this->model->term->set_language( $term_id, $lang );
}
// Edit post
elseif ( isset( $_POST['post_lang_choice'] ) ) { // FIXME should be useless now
check_admin_referer( 'pll_language', '_pll_nonce' );
$language = $this->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) );
if ( ! empty( $language ) ) {
$this->model->term->set_language( $term_id, $language );
}
}
}
/**
* Save translations from our form.
*
* @since 1.5
*
* @param int $term_id The term id of the term being saved.
* @return int[] The array of translated term ids.
*/
protected function save_translations( $term_id ) {
// Security check as 'wp_update_term' can be called from outside WP admin.
check_admin_referer( 'pll_language', '_pll_nonce' );
$translations = array();
// Save translations after checking the translated term is in the right language ( as well as cast id to int ).
if ( isset( $_POST['term_tr_lang'] ) ) {
foreach ( array_map( 'absint', $_POST['term_tr_lang'] ) as $lang => $tr_id ) {
$tr_lang = $this->model->term->get_language( $tr_id );
$translations[ $lang ] = $tr_lang && $tr_lang->slug == $lang ? $tr_id : 0;
}
}
$this->model->term->save_translations( $term_id, $translations );
return $translations;
}
/**
* Called when a category or post tag is created or edited
* Saves language and translations
*
* @since 0.1
*
* @param int $term_id Term ID.
* @param int $tt_id Term taxonomy ID.
* @param string $taxonomy Taxonomy name.
* @return void
*/
public function save_term( $term_id, $tt_id, $taxonomy ) {
// Does nothing except on taxonomies which are filterable
if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return;
}
$tax = get_taxonomy( $taxonomy );
if ( empty( $tax ) ) {
return;
}
// Capability check
// As 'wp_update_term' can be called from outside WP admin
// 2nd test for creating tags when creating / editing a post
if ( current_user_can( $tax->cap->edit_terms ) || ( isset( $_POST['tax_input'][ $taxonomy ] ) && current_user_can( $tax->cap->assign_terms ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->save_language( $term_id, $taxonomy );
if ( isset( $_POST['term_tr_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->save_translations( $term_id );
}
}
}
/**
* Ajax response for edit term form
*
* @since 0.2
*
* @return void
*/
public function term_lang_choice() {
check_ajax_referer( 'pll_language', '_pll_nonce' );
if ( ! isset( $_POST['taxonomy'], $_POST['post_type'], $_POST['lang'] ) ) {
wp_die( 0 );
}
$lang = $this->model->get_language( sanitize_key( $_POST['lang'] ) );
$term_id = isset( $_POST['term_id'] ) ? (int) $_POST['term_id'] : null; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$taxonomy = sanitize_key( $_POST['taxonomy'] );
$post_type = sanitize_key( $_POST['post_type'] );
if ( empty( $lang ) || ! post_type_exists( $post_type ) || ! taxonomy_exists( $taxonomy ) ) {
wp_die( 0 );
}
ob_start();
include __DIR__ . '/view-translations-term.php';
$x = new WP_Ajax_Response( array( 'what' => 'translations', 'data' => ob_get_contents() ) );
ob_end_clean();
// Parent dropdown list ( only for hierarchical taxonomies )
// $args copied from edit_tags.php except echo
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
$args = array(
'hide_empty' => 0,
'hide_if_empty' => false,
'taxonomy' => $taxonomy,
'name' => 'parent',
'orderby' => 'name',
'hierarchical' => true,
'show_option_none' => __( 'None', 'polylang' ),
'echo' => 0,
);
$x->Add( array( 'what' => 'parent', 'data' => wp_dropdown_categories( $args ) ) );
}
// Tag cloud
// Tests copied from edit_tags.php
else {
$tax = get_taxonomy( $taxonomy );
if ( ! empty( $tax ) && ! is_null( $tax->labels->popular_items ) ) {
$args = array( 'taxonomy' => $taxonomy, 'echo' => false );
if ( current_user_can( $tax->cap->edit_terms ) ) {
$args = array_merge( $args, array( 'link' => 'edit' ) );
}
$tag_cloud = wp_tag_cloud( $args );
if ( ! empty( $tag_cloud ) ) {
/** @phpstan-var non-falsy-string $tag_cloud */
$html = sprintf( '<div class="tagcloud"><h2>%1$s</h2>%2$s</div>', esc_html( $tax->labels->popular_items ), $tag_cloud );
$x->Add( array( 'what' => 'tag_cloud', 'data' => $html ) );
}
}
}
// Flag
$x->Add( array( 'what' => 'flag', 'data' => empty( $lang->flag ) ? esc_html( $lang->slug ) : $lang->flag ) );
$x->send();
}
/**
* Ajax response for input in translation autocomplete input box.
*
* @since 1.5
*
* @return void
*/
public function ajax_terms_not_translated() {
check_ajax_referer( 'pll_language', '_pll_nonce' );
if ( ! isset( $_GET['term'], $_GET['post_type'], $_GET['taxonomy'], $_GET['term_language'], $_GET['translation_language'] ) ) {
wp_die( 0 );
}
/** @var string */
$s = wp_unslash( $_GET['term'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$post_type = sanitize_key( $_GET['post_type'] );
$taxonomy = sanitize_key( $_GET['taxonomy'] );
if ( ! post_type_exists( $post_type ) || ! taxonomy_exists( $taxonomy ) ) {
wp_die( 0 );
}
$term_language = $this->model->get_language( sanitize_key( $_GET['term_language'] ) );
$translation_language = $this->model->get_language( sanitize_key( $_GET['translation_language'] ) );
$terms = array();
$return = array();
// Add current translation in list.
// Not in add term as term_id is not set.
if ( isset( $_GET['term_id'] ) && 'undefined' !== $_GET['term_id'] && $term_id = $this->model->term->get_translation( (int) $_GET['term_id'], $translation_language ) ) {
$terms = array( get_term( $term_id, $taxonomy ) );
}
// It is more efficient to use one common query for all languages as soon as there are more than 2.
$all_terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'lang' => '', 'name__like' => $s ) );
if ( is_array( $all_terms ) ) {
foreach ( $all_terms as $term ) {
$lang = $this->model->term->get_language( $term->term_id );
if ( $lang && $lang->slug == $translation_language->slug && ! $this->model->term->get_translation( $term->term_id, $term_language ) ) {
$terms[] = $term;
}
}
}
// Format the ajax response.
foreach ( $terms as $term ) {
if ( ! $term instanceof WP_Term ) {
continue;
}
$parents_list = get_term_parents_list(
$term->term_id,
$term->taxonomy,
array(
'separator' => ' > ',
'link' => false,
)
);
if ( ! is_string( $parents_list ) ) {
continue;
}
$return[] = array(
'id' => $term->term_id,
'value' => rtrim( $parents_list, ' >' ), // Trim the separator added at the end by WP.
'link' => $this->links->edit_term_translation_link( $term->term_id, $term->taxonomy, $post_type ),
);
}
wp_die( wp_json_encode( $return ) );
}
/**
* Updates the translations term ids when splitting a shared term
* Splits translations if these are shared terms too
*
* @since 1.7
*
* @param int $term_id ID of the formerly shared term.
* @param int $new_term_id ID of the new term created for the $term_taxonomy_id.
* @param int $term_taxonomy_id ID for the term_taxonomy row affected by the split.
* @param string $taxonomy Taxonomy name.
* @return void
*/
public function split_shared_term( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return;
}
// Avoid recursion
static $avoid_recursion = false;
if ( $avoid_recursion ) {
return;
}
$lang = $this->model->term->get_language( $term_id );
if ( empty( $lang ) ) {
return;
}
$avoid_recursion = true;
$translations = array();
foreach ( $this->model->term->get_translations( $term_id ) as $key => $tr_id ) {
if ( $lang->slug == $key ) {
$translations[ $key ] = $new_term_id;
}
else {
$tr_term = get_term( $tr_id, $taxonomy );
if ( ! $tr_term instanceof WP_Term ) {
continue;
}
$split_term_id = _split_shared_term( $tr_id, $tr_term->term_taxonomy_id );
if ( is_int( $split_term_id ) ) {
$translations[ $key ] = $split_term_id;
} else {
$translations[ $key ] = $tr_id;
}
// Hack translation ids sent by the form to avoid overwrite in PLL_Admin_Filters_Term::save_translations
if ( isset( $_POST['term_tr_lang'][ $key ] ) && $_POST['term_tr_lang'][ $key ] == $tr_id ) { // phpcs:ignore WordPress.Security.NonceVerification
$_POST['term_tr_lang'][ $key ] = $translations[ $key ];
}
}
$this->model->term->set_language( $translations[ $key ], $key );
}
$this->model->term->save_translations( $new_term_id, $translations );
$avoid_recursion = false;
}
/**
* Returns the language for subsequently inserted term in admin.
*
* @since 3.3
*
* @param PLL_Language|null $lang Term language object if found, null otherwise.
* @return PLL_Language|null Language object, null if none found.
*/
public function get_inserted_term_language( $lang ) {
if ( $lang instanceof PLL_Language ) {
return $lang;
}
if ( ! empty( $_POST['term_lang_choice'] ) && is_string( $_POST['term_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang_slug = sanitize_key( $_POST['term_lang_choice'] ); // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->get_language( $lang_slug );
return $lang instanceof PLL_Language ? $lang : null;
}
if ( ! empty( $_POST['inline_lang_choice'] ) && is_string( $_POST['inline_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang_slug = sanitize_key( $_POST['inline_lang_choice'] ); // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->get_language( $lang_slug );
return $lang instanceof PLL_Language ? $lang : null;
}
// *Post* bulk edit, in case a new term is created
if ( isset( $_GET['bulk_edit'], $_GET['inline_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// Bulk edit does not modify the language
if ( -1 === (int) $_GET['inline_lang_choice'] ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->post->get_language( $this->post_id );
return $lang instanceof PLL_Language ? $lang : null;
} elseif ( is_string( $_GET['inline_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang_slug = sanitize_key( $_GET['inline_lang_choice'] ); // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->get_language( $lang_slug );
return $lang instanceof PLL_Language ? $lang : null;
}
}
// Special cases for default categories as the select is disabled.
$default_term = get_option( 'default_category' );
if ( ! is_numeric( $default_term ) ) {
return null;
}
if ( ! empty( $_POST['tag_ID'] ) && in_array( (int) $default_term, $this->model->term->get_translations( (int) $_POST['tag_ID'] ), true ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->term->get_language( (int) $_POST['tag_ID'] ); // phpcs:ignore WordPress.Security.NonceVerification
return $lang instanceof PLL_Language ? $lang : null;
}
if ( ! empty( $_POST['tax_ID'] ) && in_array( (int) $default_term, $this->model->term->get_translations( (int) $_POST['tax_ID'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$lang = $this->model->term->get_language( (int) $_POST['tax_ID'] ); // phpcs:ignore WordPress.Security.NonceVerification
return $lang instanceof PLL_Language ? $lang : null;
}
return null;
}
/**
* Filters the subsequently inserted term parent in admin.
*
* @since 3.3
*
* @param int $parent Parent term ID, 0 if none found.
* @param string $taxonomy Term taxonomy.
* @return int Parent term ID if found, 0 otherwise.
*/
public function get_inserted_term_parent( $parent, $taxonomy ) {
if ( $parent ) {
return $parent;
}
if ( isset( $_POST['parent'], $_POST['term_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$parent = intval( $_POST['parent'] ); // phpcs:ignore WordPress.Security.NonceVerification
} elseif ( isset( $_POST[ "new{$taxonomy}_parent" ], $_POST['term_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$parent = intval( $_POST[ "new{$taxonomy}_parent" ] ); // phpcs:ignore WordPress.Security.NonceVerification
}
return $parent;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* @package Polylang
*/
/**
* Class PLL_Widgets_Filters
*
* @since 3.0
*
* Adds new options to {@see https://developer.wordpress.org/reference/classes/wp_widget/ WP_Widget} and saves them.
*/
class PLL_Admin_Filters_Widgets_Options extends PLL_Filters_Widgets_Options {
/**
* Modifies the widgets forms to add our language dropdown list.
*
* @since 0.3
* @since 3.0 Moved from PLL_Admin_Filters
*
* @param WP_Widget $widget Widget instance.
* @param null $return Not used.
* @param array $instance Widget settings.
* @return void
*/
public function in_widget_form( $widget, $return, $instance ) {
$screen = get_current_screen();
// Test the Widgets screen and the Customizer to avoid displaying the option in page builders
// Saving the widget reloads the form. And curiously the action is in $_REQUEST but neither in $_POST, nor in $_GET.
if ( ( isset( $screen ) && 'widgets' === $screen->base ) || ( isset( $_REQUEST['action'] ) && 'save-widget' === $_REQUEST['action'] ) || isset( $GLOBALS['wp_customize'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
parent::in_widget_form( $widget, $return, $instance );
}
}
}

View File

@@ -0,0 +1,127 @@
<?php
/**
* @package Polylang
*/
/**
* Setup miscellaneous admin filters as well as filters common to admin and frontend
*
* @since 1.2
*/
class PLL_Admin_Filters extends PLL_Filters {
/**
* Constructor: setups filters and actions.
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
// Language management for users
add_action( 'personal_options_update', array( $this, 'personal_options_update' ) );
add_action( 'edit_user_profile_update', array( $this, 'personal_options_update' ) );
add_action( 'personal_options', array( $this, 'personal_options' ) );
// Upgrades plugins and themes translations files
add_filter( 'themes_update_check_locales', array( $this, 'update_check_locales' ) );
add_filter( 'plugins_update_check_locales', array( $this, 'update_check_locales' ) );
add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
// Add post state for translations of the privacy policy page
add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 );
}
/**
* Updates the user biographies.
*
* @since 0.4
*
* @param int $user_id User ID.
* @return void
*/
public function personal_options_update( $user_id ) {
// Biography translations
foreach ( $this->model->get_languages_list() as $lang ) {
$meta = $lang->is_default ? 'description' : 'description_' . $lang->slug;
$description = empty( $_POST[ 'description_' . $lang->slug ] ) ? '' : trim( $_POST[ 'description_' . $lang->slug ] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
/** This filter is documented in wp-includes/user.php */
$description = apply_filters( 'pre_user_description', $description ); // Applies WP default filter wp_filter_kses
update_user_meta( $user_id, $meta, $description );
}
}
/**
* Outputs hidden information to modify the biography form with js.
*
* @since 0.4
*
* @param WP_User $profileuser The current WP_User object.
* @return void
*/
public function personal_options( $profileuser ) {
foreach ( $this->model->get_languages_list() as $lang ) {
$meta = $lang->is_default ? 'description' : 'description_' . $lang->slug;
$description = get_user_meta( $profileuser->ID, $meta, true );
printf(
'<input type="hidden" class="biography" name="%s___%s" value="%s" />',
esc_attr( $lang->slug ),
esc_attr( $lang->name ),
sanitize_user_field( 'description', $description, $profileuser->ID, 'edit' )
);
}
}
/**
* Allows to update translations files for plugins and themes.
*
* @since 1.6
*
* @param string[] $locales List of locales to update for plugins and themes.
* @return string[]
*/
public function update_check_locales( $locales ) {
return array_merge( $locales, $this->model->get_languages_list( array( 'fields' => 'locale' ) ) );
}
/**
* Adds custom classes to the body
*
* @since 2.2 Adds a text direction dependent class to the body.
* @since 3.4 Adds a language dependent class to the body.
*
* @param string $classes Space-separated list of CSS classes.
* @return string
*/
public function admin_body_class( $classes ) {
if ( ! empty( $this->curlang ) ) {
$classes .= ' pll-dir-' . ( $this->curlang->is_rtl ? 'rtl' : 'ltr' );
$classes .= ' pll-lang-' . $this->curlang->slug;
}
return $classes;
}
/**
* Adds post state for translations of the privacy policy page.
*
* @since 2.7
*
* @param string[] $post_states An array of post display states.
* @param WP_Post $post The current post object.
* @return string[]
*/
public function display_post_states( $post_states, $post ) {
$page_for_privacy_policy = get_option( 'wp_page_for_privacy_policy' );
if ( $page_for_privacy_policy && in_array( $post->ID, $this->model->post->get_translations( $page_for_privacy_policy ) ) ) {
$post_states['page_for_privacy_policy'] = __( 'Privacy Policy Page', 'polylang' );
}
return $post_states;
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* @package Polylang
*/
/**
* Manages links related functions.
*
* @since 1.8
*/
class PLL_Admin_Links extends PLL_Links {
/**
* Returns the html markup for a new translation link.
*
* @since 2.6
*
* @param string $link The new translation link.
* @param PLL_Language $language The language of the new translation.
* @return string
*/
protected function new_translation_link( $link, $language ) {
$str = '';
if ( $link ) {
/* translators: accessibility text, %s is a native language name */
$hint = sprintf( __( 'Add a translation in %s', 'polylang' ), $language->name );
$str = sprintf(
'<a href="%1$s" title="%2$s" class="pll_icon_add"><span class="screen-reader-text">%3$s</span></a>',
esc_url( $link ),
esc_attr( $hint ),
esc_html( $hint )
);
}
return $str;
}
/**
* Returns the html markup for a translation link.
*
* @since 2.6
*
* @param string $link The translation link.
* @param PLL_Language $language The language of the translation.
* @return string
*/
public function edit_translation_link( $link, $language ) {
return $link ? sprintf(
'<a href="%1$s" class="pll_icon_edit"><span class="screen-reader-text">%2$s</span></a>',
esc_url( $link ),
/* translators: accessibility text, %s is a native language name */
esc_html( sprintf( __( 'Edit the translation in %s', 'polylang' ), $language->name ) )
) : '';
}
/**
* Get the link to create a new post translation.
*
* @since 1.5
*
* @param int $post_id The source post id.
* @param PLL_Language $language The language of the new translation.
* @param string $context Optional. Defaults to 'display' which encodes '&' to '&amp;'.
* Otherwise, preserves '&'.
* @return string
*/
public function get_new_post_translation_link( $post_id, $language, $context = 'display' ) {
$post_type = get_post_type( $post_id );
$post_type_object = get_post_type_object( get_post_type( $post_id ) );
if ( empty( $post_type_object ) || ! current_user_can( $post_type_object->cap->create_posts ) ) {
return '';
}
// Special case for the privacy policy page which is associated to a specific capability
if ( 'page' === $post_type_object->name && ! current_user_can( 'manage_privacy_options' ) ) {
$privacy_page = get_option( 'wp_page_for_privacy_policy' );
if ( $privacy_page && in_array( $post_id, $this->model->post->get_translations( $privacy_page ) ) ) {
return '';
}
}
if ( 'attachment' === $post_type ) {
$args = array(
'action' => 'translate_media',
'from_media' => $post_id,
'new_lang' => $language->slug,
);
$link = add_query_arg( $args, admin_url( 'admin.php' ) );
// Add nonce for media as we will directly publish a new attachment from a click on this link
if ( 'display' === $context ) {
$link = wp_nonce_url( $link, 'translate_media' );
} else {
$link = add_query_arg( '_wpnonce', wp_create_nonce( 'translate_media' ), $link );
}
} else {
$args = array(
'post_type' => $post_type,
'from_post' => $post_id,
'new_lang' => $language->slug,
);
$link = add_query_arg( $args, admin_url( 'post-new.php' ) );
if ( 'display' === $context ) {
$link = wp_nonce_url( $link, 'new-post-translation' );
} else {
$link = add_query_arg( '_wpnonce', wp_create_nonce( 'new-post-translation' ), $link );
}
}
/**
* Filters the new post translation link.
*
* @since 1.8
*
* @param string $link The new post translation link.
* @param PLL_Language $language The language of the new translation.
* @param int $post_id The source post id.
*/
return apply_filters( 'pll_get_new_post_translation_link', $link, $language, $post_id );
}
/**
* Returns the html markup for a new post translation link.
*
* @since 1.8
*
* @param int $post_id The source post id.
* @param PLL_Language $language The language of the new translation.
* @return string
*/
public function new_post_translation_link( $post_id, $language ) {
$link = $this->get_new_post_translation_link( $post_id, $language );
return $this->new_translation_link( $link, $language );
}
/**
* Returns the html markup for a post translation link.
*
* @since 1.4
*
* @param int $post_id The translation post id.
* @return string
*/
public function edit_post_translation_link( $post_id ) {
$link = get_edit_post_link( $post_id );
$language = $this->model->post->get_language( $post_id );
return $this->edit_translation_link( $link, $language );
}
/**
* Get the link to create a new term translation.
*
* @since 1.5
*
* @param int $term_id Source term id.
* @param string $taxonomy Taxonomy name.
* @param string $post_type Post type name.
* @param PLL_Language $language The language of the new translation.
* @return string
*/
public function get_new_term_translation_link( $term_id, $taxonomy, $post_type, $language ) {
$tax = get_taxonomy( $taxonomy );
if ( ! $tax || ! current_user_can( $tax->cap->edit_terms ) ) {
return '';
}
$args = array(
'taxonomy' => $taxonomy,
'post_type' => $post_type,
'from_tag' => $term_id,
'new_lang' => $language->slug,
);
$link = add_query_arg( $args, admin_url( 'edit-tags.php' ) );
/**
* Filters the new term translation link.
*
* @since 1.8
*
* @param string $link The new term translation link.
* @param PLL_Language $language The language of the new translation.
* @param int $term_id The source term id.
* @param string $taxonomy Taxonomy name.
* @param string $post_type Post type name.
*/
return apply_filters( 'pll_get_new_term_translation_link', $link, $language, $term_id, $taxonomy, $post_type );
}
/**
* Returns the html markup for a new term translation.
*
* @since 1.8
*
* @param int $term_id Source term id.
* @param string $taxonomy Taxonomy name.
* @param string $post_type Post type name.
* @param PLL_Language $language The language of the new translation.
* @return string
*/
public function new_term_translation_link( $term_id, $taxonomy, $post_type, $language ) {
$link = $this->get_new_term_translation_link( $term_id, $taxonomy, $post_type, $language );
return $this->new_translation_link( $link, $language );
}
/**
* Returns the html markup for a term translation link.
*
* @since 1.4
*
* @param int $term_id Translation term id.
* @param string $taxonomy Taxonomy name.
* @param string $post_type Post type name.
* @return string
*/
public function edit_term_translation_link( $term_id, $taxonomy, $post_type ) {
$link = get_edit_term_link( $term_id, $taxonomy, $post_type );
$language = $this->model->term->get_language( $term_id );
return $this->edit_translation_link( $link, $language );
}
}

View File

@@ -0,0 +1,540 @@
<?php
/**
* @package Polylang
*/
/**
* Extends the PLL_Model class with methods needed only in Polylang settings pages.
*
* @since 1.2
*/
class PLL_Admin_Model extends PLL_Model {
/**
* Adds a new language
* and creates a default category for this language.
*
* @since 1.2
*
* @param array $args {
* @type string $name Language name (used only for display).
* @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
* @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
* not be loaded...
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type int $term_group Language order when displayed.
* @type string $no_default_cat Optional, if set, no default category will be created for this language.
* @type string $flag Optional, country code, {@see settings/flags.php}.
* }
* @return WP_Error|true true if success / WP_Error if failed.
*/
public function add_language( $args ) {
$errors = $this->validate_lang( $args );
if ( $errors->has_errors() ) {
return $errors;
}
// First the language taxonomy
$r = wp_insert_term(
$args['name'],
'language',
array(
'slug' => $args['slug'],
'description' => $this->build_language_metas( $args ),
)
);
if ( is_wp_error( $r ) ) {
// Avoid an ugly fatal error if something went wrong ( reported once in the forum )
return new WP_Error( 'pll_add_language', __( 'Impossible to add the language.', 'polylang' ) );
}
wp_update_term( (int) $r['term_id'], 'language', array( 'term_group' => (int) $args['term_group'] ) ); // can't set the term group directly in wp_insert_term
// The other language taxonomies.
$this->update_secondary_language_terms( $args['slug'], $args['name'] );
if ( ! isset( $this->options['default_lang'] ) ) {
// If this is the first language created, set it as default language
$this->options['default_lang'] = $args['slug'];
update_option( 'polylang', $this->options );
}
// Refresh languages.
$this->clean_languages_cache();
$this->get_languages_list();
flush_rewrite_rules(); // Refresh rewrite rules.
/**
* Fires when a language is added.
*
* @since 1.9
*
* @param array $args Arguments used to create the language. @see PLL_Admin_Model::add_language().
*/
do_action( 'pll_add_language', $args );
return true;
}
/**
* Delete a language.
*
* @since 1.2
*
* @param int $lang_id Language term_id.
* @return bool
*/
public function delete_language( $lang_id ) {
$lang = $this->get_language( (int) $lang_id );
if ( empty( $lang ) ) {
return false;
}
// Oops ! we are deleting the default language...
// Need to do this before loosing the information for default category translations
if ( $lang->is_default ) {
$slugs = $this->get_languages_list( array( 'fields' => 'slug' ) );
$slugs = array_diff( $slugs, array( $lang->slug ) );
if ( ! empty( $slugs ) ) {
$this->update_default_lang( reset( $slugs ) ); // Arbitrary choice...
} else {
unset( $this->options['default_lang'] );
}
}
// Delete the translations
$this->update_translations( $lang->slug );
// Delete language option in widgets
foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
$obj = $widget['callback'][0];
$number = $widget['params'][0]['number'];
if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
$settings = $obj->get_settings();
if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $lang->slug ) {
unset( $settings[ $number ]['pll_lang'] );
$obj->save_settings( $settings );
}
}
}
}
// Delete menus locations
if ( ! empty( $this->options['nav_menus'] ) ) {
foreach ( $this->options['nav_menus'] as $theme => $locations ) {
foreach ( array_keys( $locations ) as $location ) {
unset( $this->options['nav_menus'][ $theme ][ $location ][ $lang->slug ] );
}
}
}
// Delete users options
delete_metadata( 'user', 0, 'pll_filter_content', '', true );
delete_metadata( 'user', 0, "description_{$lang->slug}", '', true );
// Delete domain
unset( $this->options['domains'][ $lang->slug ] );
/*
* Delete the language itself.
*
* Reverses the language taxonomies order is required to make sure 'language' is deleted in last.
*
* The initial order with the 'language' taxonomy at the beginning of 'PLL_Language::term_props' property
* is done by {@see PLL_Model::filter_language_terms_orderby()}
*/
foreach ( array_reverse( $lang->get_tax_props( 'term_id' ) ) as $taxonomy_name => $term_id ) {
wp_delete_term( $term_id, $taxonomy_name );
}
// Refresh languages.
$this->clean_languages_cache();
$this->get_languages_list();
update_option( 'polylang', $this->options );
flush_rewrite_rules(); // refresh rewrite rules
return true;
}
/**
* Updates language properties.
*
* @since 1.2
*
* @param array $args {
* @type int $lang_id Id of the language to modify.
* @type string $name Language name ( used only for display ).
* @type string $slug Language code ( ideally 2-letters ISO 639-1 language code ).
* @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be loaded...
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type int $term_group Language order when displayed.
* @type string $flag Optional, country code, @see flags.php.
* }
* @return WP_Error|true true if success / WP_Error if failed.
*/
public function update_language( $args ) {
$lang = $this->get_language( (int) $args['lang_id'] );
if ( empty( $lang ) ) {
return new WP_Error( 'pll_invalid_language_id', __( 'The language does not seem to exist.', 'polylang' ) );
}
$errors = $this->validate_lang( $args, $lang );
if ( $errors->get_error_code() ) { // Using has_errors() would be more meaningful but is available only since WP 5.0
return $errors;
}
// Update links to this language in posts and terms in case the slug has been modified
$slug = $args['slug'];
$old_slug = $lang->slug;
if ( $old_slug != $slug ) {
// Update the language slug in translations
$this->update_translations( $old_slug, $slug );
// Update language option in widgets
foreach ( $GLOBALS['wp_registered_widgets'] as $widget ) {
if ( ! empty( $widget['callback'][0] ) && ! empty( $widget['params'][0]['number'] ) ) {
$obj = $widget['callback'][0];
$number = $widget['params'][0]['number'];
if ( is_object( $obj ) && method_exists( $obj, 'get_settings' ) && method_exists( $obj, 'save_settings' ) ) {
$settings = $obj->get_settings();
if ( isset( $settings[ $number ]['pll_lang'] ) && $settings[ $number ]['pll_lang'] == $old_slug ) {
$settings[ $number ]['pll_lang'] = $slug;
$obj->save_settings( $settings );
}
}
}
}
// Update menus locations
if ( ! empty( $this->options['nav_menus'] ) ) {
foreach ( $this->options['nav_menus'] as $theme => $locations ) {
foreach ( array_keys( $locations ) as $location ) {
if ( ! empty( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] ) ) {
$this->options['nav_menus'][ $theme ][ $location ][ $slug ] = $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ];
unset( $this->options['nav_menus'][ $theme ][ $location ][ $old_slug ] );
}
}
}
}
// Update domains
if ( ! empty( $this->options['domains'][ $old_slug ] ) ) {
$this->options['domains'][ $slug ] = $this->options['domains'][ $old_slug ];
unset( $this->options['domains'][ $old_slug ] );
}
// Update the default language option if necessary
if ( $lang->is_default ) {
$this->options['default_lang'] = $slug;
}
}
update_option( 'polylang', $this->options );
// And finally update the language itself.
$this->update_secondary_language_terms( $args['slug'], $args['name'], $lang );
$description = $this->build_language_metas( $args );
wp_update_term( $lang->get_tax_prop( 'language', 'term_id' ), 'language', array( 'slug' => $slug, 'name' => $args['name'], 'description' => $description, 'term_group' => (int) $args['term_group'] ) );
// Refresh languages.
$this->clean_languages_cache();
$this->get_languages_list();
// Refresh rewrite rules.
flush_rewrite_rules();
/**
* Fires after a language is updated.
*
* @since 1.9
* @since 3.2 Added $lang parameter.
*
* @param array $args {
* Arguments used to modify the language. @see PLL_Admin_Model::update_language().
*
* @type string $name Language name (used only for display).
* @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
* @type string $locale WordPress locale.
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type int $term_group Language order when displayed.
* @type string $no_default_cat Optional, if set, no default category has been created for this language.
* @type string $flag Optional, country code, @see flags.php.
* }
* @param PLL_Language $lang Previous value of the language being edited.
*/
do_action( 'pll_update_language', $args, $lang );
return true;
}
/**
* Builds the language metas into an array and serializes it, to be stored in the term description.
*
* @since 3.4
*
* @param array $args {
* @type string $name Language name (used only for display).
* @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
* @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will not be
* loaded...
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type int $term_group Language order when displayed.
* @type int $lang_id Optional, ID of the language to modify. An empty value means the language is being
* created.
* @type string $flag Optional, country code, {@see settings/flags.php}.
* }
* @return string The serialized description array updated.
*/
protected function build_language_metas( array $args ) {
if ( ! empty( $args['lang_id'] ) ) {
$language_term = get_term( (int) $args['lang_id'] );
if ( $language_term instanceof WP_Term ) {
$old_data = maybe_unserialize( $language_term->description );
}
}
if ( empty( $old_data ) || ! is_array( $old_data ) ) {
$old_data = array();
}
$new_data = array(
'locale' => $args['locale'],
'rtl' => ! empty( $args['rtl'] ) ? 1 : 0,
'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'],
);
/**
* Allow to add data to store for a language.
* `$locale`, `$rtl`, and `$flag_code` cannot be overwritten.
*
* @since 3.4
*
* @param mixed[] $add_data Data to add.
* @param mixed[] $args {
* Arguments used to create the language.
*
* @type string $name Language name (used only for display).
* @type string $slug Language code (ideally 2-letters ISO 639-1 language code).
* @type string $locale WordPress locale. If something wrong is used for the locale, the .mo files will
* not be loaded...
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type int $term_group Language order when displayed.
* @type int $lang_id Optional, ID of the language to modify. An empty value means the language is
* being created.
* @type string $flag Optional, country code, {@see settings/flags.php}.
* }
* @param mixed[] $new_data New data.
* @param mixed[] $old_data {
* Original data. Contains at least the following:
*
* @type string $locale WordPress locale.
* @type int $rtl 1 if rtl language, 0 otherwise.
* @type string $flag_code Country code.
* }
*/
$add_data = apply_filters( 'pll_language_metas', array(), $args, $new_data, $old_data );
// Don't allow to overwrite `$locale`, `$rtl`, and `$flag_code`.
$new_data = array_merge( $old_data, $add_data, $new_data );
/** @var non-empty-string $serialized maybe_serialize() cannot return anything else than a string when fed by an array. */
$serialized = maybe_serialize( $new_data );
return $serialized;
}
/**
* Validates data entered when creating or updating a language.
*
* @see PLL_Admin_Model::add_language().
*
* @since 0.4
*
* @param array $args Parameters of {@see PLL_Admin_Model::add_language() or @see PLL_Admin_Model::update_language()}.
* @param PLL_Language|null $lang Optional the language currently updated, the language is created if not set.
* @return WP_Error
*/
protected function validate_lang( $args, $lang = null ) {
$errors = new WP_Error();
// Validate locale with the same pattern as WP 4.3. See #28303
if ( ! preg_match( '#^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?$#', $args['locale'], $matches ) ) {
$errors->add( 'pll_invalid_locale', __( 'Enter a valid WordPress locale', 'polylang' ) );
}
// Validate slug characters
if ( ! preg_match( '#^[a-z_-]+$#', $args['slug'] ) ) {
$errors->add( 'pll_invalid_slug', __( 'The language code contains invalid characters', 'polylang' ) );
}
// Validate slug is unique
foreach ( $this->get_languages_list() as $language ) {
if ( $language->slug === $args['slug'] && ( null === $lang || $lang->term_id !== $language->term_id ) ) {
$errors->add( 'pll_non_unique_slug', __( 'The language code must be unique', 'polylang' ) );
}
}
// Validate name
// No need to sanitize it as wp_insert_term will do it for us
if ( empty( $args['name'] ) ) {
$errors->add( 'pll_invalid_name', __( 'The language must have a name', 'polylang' ) );
}
// Validate flag
if ( ! empty( $args['flag'] ) && ! is_readable( POLYLANG_DIR . '/flags/' . $args['flag'] . '.png' ) ) {
$flag = PLL_Language::get_flag_informations( $args['flag'] );
if ( ! empty( $flag['url'] ) ) {
$response = function_exists( 'vip_safe_wp_remote_get' ) ? vip_safe_wp_remote_get( esc_url_raw( $flag['url'] ) ) : wp_remote_get( esc_url_raw( $flag['url'] ) );
}
if ( empty( $response ) || is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
$errors->add( 'pll_invalid_flag', __( 'The flag does not exist', 'polylang' ) );
}
}
return $errors;
}
/**
* Updates the translations when a language slug has been modified in settings
* or deletes them when a language is removed.
*
* @since 0.5
*
* @param string $old_slug The old language slug.
* @param string $new_slug Optional, the new language slug, if not set it means that the language has been deleted.
* @return void
*/
public function update_translations( $old_slug, $new_slug = '' ) {
global $wpdb;
$term_ids = array();
$dr = array();
$dt = array();
$ut = array();
$taxonomies = $this->translatable_objects->get_taxonomy_names( array( 'translations' ) );
$terms = get_terms( array( 'taxonomy' => $taxonomies ) );
if ( is_array( $terms ) ) {
foreach ( $terms as $term ) {
$term_ids[ $term->taxonomy ][] = $term->term_id;
$tr = maybe_unserialize( $term->description );
/**
* Filters the unserialized translation group description before it is
* updated when a language is deleted or a language slug is changed.
*
* @since 3.2
*
* @param (int|string[])[] $tr {
* List of translations with lang codes as array keys and IDs as array values.
* Also in this array:
*
* @type string[] $sync List of synchronized translations with lang codes as array keys and array values.
* }
* @param string $old_slug The old language slug.
* @param string $new_slug The new language slug.
* @param WP_Term $term The term containing the post or term translation group.
*/
$tr = apply_filters( 'update_translation_group', $tr, $old_slug, $new_slug, $term );
if ( ! empty( $tr[ $old_slug ] ) ) {
if ( $new_slug ) {
$tr[ $new_slug ] = $tr[ $old_slug ]; // Suppress this for delete
} else {
$dr['id'][] = (int) $tr[ $old_slug ];
$dr['tt'][] = (int) $term->term_taxonomy_id;
}
unset( $tr[ $old_slug ] );
if ( empty( $tr ) || 1 == count( $tr ) ) {
$dt['t'][] = (int) $term->term_id;
$dt['tt'][] = (int) $term->term_taxonomy_id;
} else {
$ut['case'][] = $wpdb->prepare( 'WHEN %d THEN %s', $term->term_id, maybe_serialize( $tr ) );
$ut['in'][] = (int) $term->term_id;
}
}
}
}
// Delete relationships
if ( ! empty( $dr ) ) {
// PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
"DELETE FROM $wpdb->term_relationships
WHERE object_id IN ( " . implode( ',', $dr['id'] ) . ' )
AND term_taxonomy_id IN ( ' . implode( ',', $dr['tt'] ) . ' )'
);
// PHPCS:enable
}
// Delete terms
if ( ! empty( $dt ) ) {
$wpdb->query( "DELETE FROM $wpdb->terms WHERE term_id IN ( " . implode( ',', $dt['t'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( "DELETE FROM $wpdb->term_taxonomy WHERE term_taxonomy_id IN ( " . implode( ',', $dt['tt'] ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
}
// Update terms
if ( ! empty( $ut ) ) {
// PHPCS:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query(
"UPDATE $wpdb->term_taxonomy
SET description = ( CASE term_id " . implode( ' ', $ut['case'] ) . ' END )
WHERE term_id IN ( ' . implode( ',', $ut['in'] ) . ' )'
);
// PHPCS:enable
}
if ( ! empty( $term_ids ) ) {
foreach ( $term_ids as $taxonomy => $ids ) {
clean_term_cache( $ids, $taxonomy );
}
}
}
/**
* Updates the default language
* taking care to update the default category & the nav menu locations.
*
* @since 1.8
*
* @param string $slug New language slug.
* @return void
*/
public function update_default_lang( $slug ) {
// The nav menus stored in theme locations should be in the default language
$theme = get_stylesheet();
if ( ! empty( $this->options['nav_menus'][ $theme ] ) ) {
$menus = array();
foreach ( $this->options['nav_menus'][ $theme ] as $key => $loc ) {
$menus[ $key ] = empty( $loc[ $slug ] ) ? 0 : $loc[ $slug ];
}
set_theme_mod( 'nav_menu_locations', $menus );
}
/**
* Fires when a default language is updated.
*
* @since 3.1
*
* @param string $slug Slug.
*/
do_action( 'pll_update_default_lang', $slug );
// Update options
$this->options['default_lang'] = $slug;
update_option( 'polylang', $this->options );
$this->clean_languages_cache();
flush_rewrite_rules();
}
}

View File

@@ -0,0 +1,280 @@
<?php
/**
* @package Polylang
*/
/**
* Manages custom menus translations as well as the language switcher menu item on admin side
*
* @since 1.2
*/
class PLL_Admin_Nav_Menu extends PLL_Nav_Menu {
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
// Populates nav menus locations
// Since WP 4.4, must be done before customize_register is fired
add_filter( 'theme_mod_nav_menu_locations', array( $this, 'theme_mod_nav_menu_locations' ), 20 );
// Integration in the WP menu interface
add_action( 'admin_init', array( $this, 'admin_init' ) ); // after Polylang upgrade
}
/**
* Setups filters and terms
* adds the language switcher metabox and create new nav menu locations
*
* @since 1.1
*
* @return void
*/
public function admin_init() {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
add_action( 'wp_update_nav_menu_item', array( $this, 'wp_update_nav_menu_item' ), 10, 2 );
// Translation of menus based on chosen locations
add_filter( 'pre_update_option_theme_mods_' . $this->theme, array( $this, 'pre_update_option_theme_mods' ) );
add_action( 'delete_nav_menu', array( $this, 'delete_nav_menu' ) );
// FIXME is it possible to choose the order ( after theme locations in WP3.5 and older ) ?
// FIXME not displayed if Polylang is activated before the first time the user goes to nav menus http://core.trac.wordpress.org/ticket/16828
add_meta_box( 'pll_lang_switch_box', __( 'Language switcher', 'polylang' ), array( $this, 'lang_switch' ), 'nav-menus', 'side', 'high' );
$this->create_nav_menu_locations();
}
/**
* Language switcher metabox
* The checkbox and all hidden fields are important
* Thanks to John Morris for his very interesting post http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/
*
* @since 1.1
*
* @return void
*/
public function lang_switch() {
global $_nav_menu_placeholder, $nav_menu_selected_id;
$_nav_menu_placeholder = 0 > $_nav_menu_placeholder ? $_nav_menu_placeholder - 1 : -1;
?>
<div id="posttype-lang-switch" class="posttypediv">
<div id="tabs-panel-lang-switch" class="tabs-panel tabs-panel-active">
<ul id="lang-switch-checklist" class="categorychecklist form-no-clear">
<li>
<label class="menu-item-title">
<input type="checkbox" class="menu-item-checkbox" name="menu-item[<?php echo (int) $_nav_menu_placeholder; ?>][menu-item-object-id]" value="-1"> <?php esc_html_e( 'Languages', 'polylang' ); ?>
</label>
<input type="hidden" class="menu-item-type" name="menu-item[<?php echo (int) $_nav_menu_placeholder; ?>][menu-item-type]" value="custom">
<input type="hidden" class="menu-item-title" name="menu-item[<?php echo (int) $_nav_menu_placeholder; ?>][menu-item-title]" value="<?php esc_attr_e( 'Languages', 'polylang' ); ?>">
<input type="hidden" class="menu-item-url" name="menu-item[<?php echo (int) $_nav_menu_placeholder; ?>][menu-item-url]" value="#pll_switcher">
</li>
</ul>
</div>
<p class="button-controls">
<span class="add-to-menu">
<input type="submit" <?php disabled( $nav_menu_selected_id, 0 ); ?> class="button-secondary submit-add-to-menu right" value="<?php esc_attr_e( 'Add to Menu', 'polylang' ); ?>" name="add-post-type-menu-item" id="submit-posttype-lang-switch">
<span class="spinner"></span>
</span>
</p>
</div>
<?php
}
/**
* Prepares javascript to modify the language switcher menu item
*
* @since 1.1
*
* @return void
*/
public function admin_enqueue_scripts() {
$screen = get_current_screen();
if ( empty( $screen ) || 'nav-menus' !== $screen->base ) {
return;
}
$suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
wp_enqueue_script( 'pll_nav_menu', plugins_url( '/js/build/nav-menu' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION );
$data = array(
'strings' => PLL_Switcher::get_switcher_options( 'menu', 'string' ), // The strings for the options
'title' => __( 'Languages', 'polylang' ), // The title
'val' => array(),
);
// Get all language switcher menu items
$items = get_posts(
array(
'numberposts' => -1,
'nopaging' => true,
'post_type' => 'nav_menu_item',
'fields' => 'ids',
'meta_key' => '_pll_menu_item',
)
);
// The options values for the language switcher
foreach ( $items as $item ) {
$data['val'][ $item ] = get_post_meta( $item, '_pll_menu_item', true );
}
// Send all these data to javascript
wp_localize_script( 'pll_nav_menu', 'pll_data', $data );
}
/**
* Save our menu item options.
*
* @since 1.1
*
* @param int $menu_id ID of the updated menu.
* @param int $menu_item_db_id ID of the updated menu item.
* @return void
*/
public function wp_update_nav_menu_item( $menu_id = 0, $menu_item_db_id = 0 ) {
if ( empty( $_POST['menu-item-url'][ $menu_item_db_id ] ) || '#pll_switcher' !== $_POST['menu-item-url'][ $menu_item_db_id ] ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
// Security check as 'wp_update_nav_menu_item' can be called from outside WP admin
if ( current_user_can( 'edit_theme_options' ) ) {
check_admin_referer( 'update-nav_menu', 'update-nav-menu-nonce' );
$options = array( 'hide_if_no_translation' => 0, 'hide_current' => 0, 'force_home' => 0, 'show_flags' => 0, 'show_names' => 1, 'dropdown' => 0 ); // Default values
// Our jQuery form has not been displayed
if ( empty( $_POST['menu-item-pll-detect'][ $menu_item_db_id ] ) ) {
if ( ! get_post_meta( $menu_item_db_id, '_pll_menu_item', true ) ) { // Our options were never saved
update_post_meta( $menu_item_db_id, '_pll_menu_item', $options );
}
}
else {
foreach ( array_keys( $options ) as $opt ) {
$options[ $opt ] = empty( $_POST[ 'menu-item-' . $opt ][ $menu_item_db_id ] ) ? 0 : 1;
}
update_post_meta( $menu_item_db_id, '_pll_menu_item', $options ); // Allow us to easily identify our nav menu item
}
}
}
/**
* Assign menu languages and translations based on ( temporary ) locations
*
* @since 1.8
*
* @param array $locations nav menu locations
* @return array
*/
public function update_nav_menu_locations( $locations ) {
// Extract language and menu from locations
foreach ( $locations as $loc => $menu ) {
$infos = $this->explode_location( $loc );
$this->options['nav_menus'][ $this->theme ][ $infos['location'] ][ $infos['lang'] ] = $menu;
if ( $this->options['default_lang'] != $infos['lang'] ) {
unset( $locations[ $loc ] ); // Remove temporary locations before database update
}
}
update_option( 'polylang', $this->options );
return $locations;
}
/**
* Assign menu languages and translations based on ( temporary ) locations.
*
* @since 1.1
*
* @param mixed $mods Theme mods.
* @return mixed
*/
public function pre_update_option_theme_mods( $mods ) {
if ( current_user_can( 'edit_theme_options' ) && is_array( $mods ) && isset( $mods['nav_menu_locations'] ) ) {
// Manage Locations tab in Appearance -> Menus
if ( isset( $_GET['action'] ) && 'locations' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification
check_admin_referer( 'save-menu-locations' );
$this->options['nav_menus'][ $this->theme ] = array();
}
// Edit Menus tab in Appearance -> Menus
// Add the test of $_POST['update-nav-menu-nonce'] to avoid conflict with Vantage theme
elseif ( isset( $_POST['action'], $_POST['update-nav-menu-nonce'] ) && 'update' === $_POST['action'] ) {
check_admin_referer( 'update-nav_menu', 'update-nav-menu-nonce' );
$this->options['nav_menus'][ $this->theme ] = array();
}
// Customizer
// Don't reset locations in this case.
// see http://wordpress.org/support/topic/menus-doesnt-show-and-not-saved-in-theme-settings-multilingual-site
elseif ( isset( $_POST['action'] ) && 'customize_save' == $_POST['action'] ) {
check_ajax_referer( 'save-customize_' . $GLOBALS['wp_customize']->get_stylesheet(), 'nonce' );
}
else {
return $mods; // No modification for nav menu locations
}
$mods['nav_menu_locations'] = $this->update_nav_menu_locations( $mods['nav_menu_locations'] );
}
return $mods;
}
/**
* Fills temporary menu locations based on menus translations
*
* @since 1.2
*
* @param bool|array $menus Associative array of registered navigation menu IDs keyed by their location name.
* @return bool|array
*/
public function theme_mod_nav_menu_locations( $menus ) {
// Prefill locations with 0 value in case a location does not exist in $menus
$locations = get_registered_nav_menus();
if ( is_array( $locations ) ) {
$locations = array_fill_keys( array_keys( $locations ), 0 );
$menus = is_array( $menus ) ? array_merge( $locations, $menus ) : $locations;
}
if ( is_array( $menus ) ) {
foreach ( array_keys( $menus ) as $loc ) {
foreach ( $this->model->get_languages_list() as $lang ) {
if ( ! empty( $this->options['nav_menus'][ $this->theme ][ $loc ][ $lang->slug ] ) && term_exists( $this->options['nav_menus'][ $this->theme ][ $loc ][ $lang->slug ], 'nav_menu' ) ) {
$menus[ $this->combine_location( $loc, $lang ) ] = $this->options['nav_menus'][ $this->theme ][ $loc ][ $lang->slug ];
}
}
}
}
return $menus;
}
/**
* Removes the nav menu term_id from the locations stored in Polylang options when a nav menu is deleted
*
* @since 1.7.3
*
* @param int $term_id nav menu id
* @return void
*/
public function delete_nav_menu( $term_id ) {
if ( isset( $this->options['nav_menus'] ) ) {
foreach ( $this->options['nav_menus'] as $theme => $locations ) {
foreach ( $locations as $loc => $languages ) {
foreach ( $languages as $lang => $menu_id ) {
if ( $menu_id === $term_id ) {
unset( $this->options['nav_menus'][ $theme ][ $loc ][ $lang ] );
}
}
}
}
update_option( 'polylang', $this->options );
}
}
}

View File

@@ -0,0 +1,270 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manage admin notices
* displayed only to admin, based on 'manage_options' capability
* and only on dashboard, plugins and Polylang admin pages
*
* @since 2.3.9
* @since 2.7 Dismissed notices are stored in an option instead of a user meta
*/
class PLL_Admin_Notices {
/**
* Stores the plugin options.
*
* @var array
*/
protected $options;
/**
* Stores custom notices.
*
* @var string[]
*/
private static $notices = array();
/**
* Constructor
* Setup actions
*
* @since 2.3.9
*
* @param object $polylang The Polylang object.
*/
public function __construct( $polylang ) {
$this->options = &$polylang->options;
add_action( 'admin_init', array( $this, 'hide_notice' ) );
add_action( 'admin_notices', array( $this, 'display_notices' ) );
}
/**
* Add a custom notice
*
* @since 2.3.9
*
* @param string $name Notice name
* @param string $html Content of the notice
* @return void
*/
public static function add_notice( $name, $html ) {
self::$notices[ $name ] = $html;
}
/**
* Get custom notices.
*
* @since 2.3.9
*
* @return string[]
*/
public static function get_notices() {
return self::$notices;
}
/**
* Has a notice been dismissed?
*
* @since 2.3.9
*
* @param string $notice Notice name
* @return bool
*/
public static function is_dismissed( $notice ) {
$dismissed = get_option( 'pll_dismissed_notices', array() );
// Handle legacy user meta
$dismissed_meta = get_user_meta( get_current_user_id(), 'pll_dismissed_notices', true );
if ( is_array( $dismissed_meta ) ) {
if ( array_diff( $dismissed_meta, $dismissed ) ) {
$dismissed = array_merge( $dismissed, $dismissed_meta );
update_option( 'pll_dismissed_notices', $dismissed );
}
if ( ! is_multisite() ) {
// Don't delete on multisite to avoid the notices to appear in other sites.
delete_user_meta( get_current_user_id(), 'pll_dismissed_notices' );
}
}
return in_array( $notice, $dismissed );
}
/**
* Should we display notices on this screen?
*
* @since 2.3.9
*
* @param string $notice The notice name.
* @return bool
*/
protected function can_display_notice( $notice ) {
$screen = get_current_screen();
if ( empty( $screen ) ) {
return false;
}
$screen_id = sanitize_title( __( 'Languages', 'polylang' ) );
/**
* Filter admin notices which can be displayed
*
* @since 2.7.0
*
* @param bool $display Whether the notice should be displayed or not.
* @param string $notice The notice name.
*/
return apply_filters(
'pll_can_display_notice',
in_array(
$screen->id,
array(
'dashboard',
'plugins',
'toplevel_page_mlang',
$screen_id . '_page_mlang_strings',
$screen_id . '_page_mlang_settings',
)
),
$notice
);
}
/**
* Stores a dismissed notice in the database.
*
* @since 2.3.9
*
* @param string $notice Notice name.
* @return void
*/
public static function dismiss( $notice ) {
$dismissed = get_option( 'pll_dismissed_notices', array() );
if ( ! in_array( $notice, $dismissed ) ) {
$dismissed[] = $notice;
update_option( 'pll_dismissed_notices', array_unique( $dismissed ) );
}
}
/**
* Handle a click on the dismiss button
*
* @since 2.3.9
*
* @return void
*/
public function hide_notice() {
if ( isset( $_GET['pll-hide-notice'], $_GET['_pll_notice_nonce'] ) ) {
$notice = sanitize_key( $_GET['pll-hide-notice'] );
check_admin_referer( $notice, '_pll_notice_nonce' );
self::dismiss( $notice );
wp_safe_redirect( remove_query_arg( array( 'pll-hide-notice', '_pll_notice_nonce' ), wp_get_referer() ) );
exit;
}
}
/**
* Displays notices
*
* @since 2.3.9
*
* @return void
*/
public function display_notices() {
if ( current_user_can( 'manage_options' ) ) {
// Core notices
if ( defined( 'WOOCOMMERCE_VERSION' ) && ! defined( 'PLLWC_VERSION' ) && $this->can_display_notice( 'pllwc' ) && ! $this->is_dismissed( 'pllwc' ) ) {
$this->pllwc_notice();
}
if ( ! defined( 'POLYLANG_PRO' ) && $this->can_display_notice( 'review' ) && ! $this->is_dismissed( 'review' ) && ! empty( $this->options['first_activation'] ) && time() > $this->options['first_activation'] + 15 * DAY_IN_SECONDS ) {
$this->review_notice();
}
// Custom notices
foreach ( $this->get_notices() as $notice => $html ) {
if ( $this->can_display_notice( $notice ) && ! $this->is_dismissed( $notice ) ) {
?>
<div class="pll-notice notice notice-info">
<?php
$this->dismiss_button( $notice );
echo wp_kses_post( $html );
?>
</div>
<?php
}
}
}
}
/**
* Displays a dismiss button
*
* @since 2.3.9
*
* @param string $name Notice name
* @return void
*/
public function dismiss_button( $name ) {
printf(
'<a class="notice-dismiss" href="%s"><span class="screen-reader-text">%s</span></a>',
esc_url( wp_nonce_url( add_query_arg( 'pll-hide-notice', $name ), $name, '_pll_notice_nonce' ) ),
/* translators: accessibility text */
esc_html__( 'Dismiss this notice.', 'polylang' )
);
}
/**
* Displays a notice if WooCommerce is activated without Polylang for WooCommerce
*
* @since 2.3.9
*
* @return void
*/
private function pllwc_notice() {
?>
<div class="pll-notice notice notice-warning">
<?php $this->dismiss_button( 'pllwc' ); ?>
<p>
<?php
printf(
/* translators: %1$s is link start tag, %2$s is link end tag. */
esc_html__( 'We have noticed that you are using Polylang with WooCommerce. To ensure compatibility, we recommend you use %1$sPolylang for WooCommerce%2$s.', 'polylang' ),
'<a href="https://polylang.pro/downloads/polylang-for-woocommerce/">',
'</a>'
);
?>
</p>
</div>
<?php
}
/**
* Displays a notice asking for a review
*
* @since 2.3.9
*
* @return void
*/
private function review_notice() {
?>
<div class="pll-notice notice notice-info">
<?php $this->dismiss_button( 'review' ); ?>
<p>
<?php
printf(
/* translators: %1$s is link start tag, %2$s is link end tag. */
esc_html__( 'We have noticed that you have been using Polylang for some time. We hope you love it, and we would really appreciate it if you would %1$sgive us a 5 stars rating%2$s.', 'polylang' ),
'<a href="https://wordpress.org/support/plugin/polylang/reviews/?rate=5#new-post">',
'</a>'
);
?>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* @package Polylang
*/
/**
* Manages the static front page and the page for posts on admin side
*
* @since 1.8
*/
class PLL_Admin_Static_Pages extends PLL_Static_Pages {
/**
* @var PLL_Admin_Links|null
*/
protected $links;
/**
* Constructor: setups filters and actions.
*
* @since 1.8
*
* @param object $polylang An array of attachment metadata.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->links = &$polylang->links;
// Add post state for translations of the front page and posts page
add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 );
// Refreshes the language cache when a static front page or page for for posts has been translated.
add_action( 'pll_save_post', array( $this, 'pll_save_post' ), 10, 3 );
// Prevents WP resetting the option
add_filter( 'pre_update_option_show_on_front', array( $this, 'update_show_on_front' ), 10, 2 );
add_action( 'admin_notices', array( $this, 'notice_must_translate' ) );
}
/**
* Adds post state for translations of the front page and posts page.
*
* @since 1.8
*
* @param string[] $post_states An array of post display states.
* @param WP_Post $post The current post object.
* @return string[]
*/
public function display_post_states( $post_states, $post ) {
if ( in_array( $post->ID, $this->model->get_languages_list( array( 'fields' => 'page_on_front' ) ) ) ) {
$post_states['page_on_front'] = __( 'Front Page', 'polylang' );
}
if ( in_array( $post->ID, $this->model->get_languages_list( array( 'fields' => 'page_for_posts' ) ) ) ) {
$post_states['page_for_posts'] = __( 'Posts Page', 'polylang' );
}
return $post_states;
}
/**
* Refreshes the language cache when a static front page or page for for posts has been translated.
*
* @since 1.8
*
* @param int $post_id Not used.
* @param WP_Post $post Not used.
* @param int[] $translations Translations of the post being saved.
* @return void
*/
public function pll_save_post( $post_id, $post, $translations ) {
if ( in_array( $this->page_on_front, $translations ) || in_array( $this->page_for_posts, $translations ) ) {
$this->model->clean_languages_cache();
}
}
/**
* Prevents WP resetting the option if the admin language filter is active for a language with no pages.
*
* @since 1.9.3
*
* @param string $value The new, unserialized option value.
* @param string $old_value The old option value.
* @return string
*/
public function update_show_on_front( $value, $old_value ) {
if ( ! empty( $GLOBALS['pagenow'] ) && 'options-reading.php' === $GLOBALS['pagenow'] && 'posts' === $value && ! get_pages() && get_pages( array( 'lang' => '' ) ) ) {
$value = $old_value;
}
return $value;
}
/**
* Add a notice to translate the static front page if it is not translated in all languages
* This is especially useful after a new language is created.
* The notice is not dismissible and displayed on the Languages pages and the list of pages.
*
* @since 2.6
*
* @return void
*/
public function notice_must_translate() {
$screen = get_current_screen();
if ( ! empty( $screen ) && ( 'toplevel_page_mlang' === $screen->id || 'edit-page' === $screen->id ) ) {
$message = $this->get_must_translate_message();
if ( ! empty( $message ) ) {
printf(
'<div class="error"><p>%s</p></div>',
$message // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
}
}
/**
* Returns the message asking to translate the static front page in all languages.
*
* @since 2.8
*
* @return string
*/
public function get_must_translate_message() {
$message = '';
if ( $this->page_on_front ) {
$untranslated = array();
foreach ( $this->model->get_languages_list() as $language ) {
if ( ! $this->model->post->get( $this->page_on_front, $language ) ) {
$untranslated[] = sprintf(
'<a href="%s">%s</a>',
esc_url( $this->links->get_new_post_translation_link( $this->page_on_front, $language ) ),
esc_html( $language->name )
);
}
}
if ( ! empty( $untranslated ) ) {
$message = sprintf(
/* translators: %s is a comma separated list of native language names */
esc_html__( 'You must translate your static front page in %s.', 'polylang' ),
implode( ', ', $untranslated ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
}
}
return $message;
}
}

View File

@@ -0,0 +1,136 @@
<?php
/**
* @package Polylang
*/
/**
* A fully static class to manage strings translations on admin side
*
* @since 1.6
*/
class PLL_Admin_Strings {
/**
* Stores the strings to translate.
*
* @var array {
* @type string $name A unique name for the string.
* @type string $string The actual string to translate.
* @type string $context The group in which the string is registered.
* @type bool $multiline Whether the string table should display a multiline textarea or a single line input.
* }
*/
protected static $strings = array();
/**
* The strings to register by default.
*
* @var string[]|null
*/
protected static $default_strings;
/**
* Add filters
*
* @since 1.6
*
* @return void
*/
public static function init() {
// default strings translations sanitization
add_filter( 'pll_sanitize_string_translation', array( __CLASS__, 'sanitize_string_translation' ), 10, 2 );
}
/**
* Register strings for translation making sure it is not duplicate or empty
*
* @since 0.6
*
* @param string $name A unique name for the string
* @param string $string The string to register
* @param string $context Optional, the group in which the string is registered, defaults to 'polylang'
* @param bool $multiline Optional, whether the string table should display a multiline textarea or a single line input, defaults to single line
* @return void
*/
public static function register_string( $name, $string, $context = 'Polylang', $multiline = false ) {
if ( $string && is_scalar( $string ) ) {
self::$strings[ md5( $string ) ] = compact( 'name', 'string', 'context', 'multiline' );
}
}
/**
* Get registered strings
*
* @since 0.6.1
*
* @return array list of all registered strings
*/
public static function &get_strings() {
self::$default_strings = array(
'widget_title' => __( 'Widget title', 'polylang' ),
'widget_text' => __( 'Widget text', 'polylang' ),
);
// Widgets titles
global $wp_registered_widgets;
$sidebars = wp_get_sidebars_widgets();
foreach ( $sidebars as $sidebar => $widgets ) {
if ( 'wp_inactive_widgets' == $sidebar || empty( $widgets ) ) {
continue;
}
foreach ( $widgets as $widget ) {
// Nothing can be done if the widget is created using pre WP2.8 API :(
// There is no object, so we can't access it to get the widget options
if ( ! isset( $wp_registered_widgets[ $widget ]['callback'][0] ) || ! is_object( $wp_registered_widgets[ $widget ]['callback'][0] ) || ! method_exists( $wp_registered_widgets[ $widget ]['callback'][0], 'get_settings' ) ) {
continue;
}
$widget_settings = $wp_registered_widgets[ $widget ]['callback'][0]->get_settings();
$number = $wp_registered_widgets[ $widget ]['params'][0]['number'];
// Don't enable widget translation if the widget is visible in only one language or if there is no title
if ( empty( $widget_settings[ $number ]['pll_lang'] ) ) {
if ( isset( $widget_settings[ $number ]['title'] ) && $title = $widget_settings[ $number ]['title'] ) {
self::register_string( self::$default_strings['widget_title'], $title, 'Widget' );
}
if ( isset( $widget_settings[ $number ]['text'] ) && $text = $widget_settings[ $number ]['text'] ) {
self::register_string( self::$default_strings['widget_text'], $text, 'Widget', true );
}
}
}
}
/**
* Filter the list of strings registered for translation
* Mainly for use by our PLL_WPML_Compat class
*
* @since 1.0.2
*
* @param array $strings list of strings
*/
self::$strings = apply_filters( 'pll_get_strings', self::$strings );
return self::$strings;
}
/**
* Performs the sanitization ( before saving in DB ) of default strings translations
*
* @since 1.6
*
* @param string $translation translation to sanitize
* @param string $name unique name for the string
* @return string
*/
public static function sanitize_string_translation( $translation, $name ) {
if ( $name == self::$default_strings['widget_title'] ) {
$translation = sanitize_text_field( $translation );
}
if ( $name == self::$default_strings['widget_text'] && ! current_user_can( 'unfiltered_html' ) ) {
$translation = wp_kses_post( $translation );
}
return $translation;
}
}

View File

@@ -0,0 +1,180 @@
<?php
/**
* @package Polylang
*/
/**
* Main Polylang class for admin (except Polylang pages), accessible from @see PLL().
*
* @since 1.2
*/
class PLL_Admin extends PLL_Admin_Base {
/**
* @var PLL_Admin_Filters|null
*/
public $filters;
/**
* @var PLL_Admin_Filters_Columns|null
*/
public $filters_columns;
/**
* @var PLL_Admin_Filters_Post|null
*/
public $filters_post;
/**
* @var PLL_Admin_Filters_Term|null
*/
public $filters_term;
/**
* @var PLL_Admin_Filters_Media|null
*/
public $filters_media;
/**
* @since 2.9
*
* @var PLL_Filters_Sanitization|null
*/
public $filters_sanitization;
/**
* @var PLL_Admin_Block_Editor|null
*/
public $block_editor;
/**
* @var PLL_Admin_Classic_Editor|null
*/
public $classic_editor;
/**
* @var PLL_Admin_Nav_Menu|null
*/
public $nav_menu;
/**
* @var PLL_Admin_Filters_Widgets_Options|null
*/
public $filters_widgets_options;
/**
* Setups filters and action needed on all admin pages and on plugins page.
*
* @since 1.2
*
* @param PLL_Links_Model $links_model Reference to the links model.
*/
public function __construct( &$links_model ) {
parent::__construct( $links_model );
// Adds a 'settings' link in the plugins table
add_filter( 'plugin_action_links_' . POLYLANG_BASENAME, array( $this, 'plugin_action_links' ) );
add_action( 'in_plugin_update_message-' . POLYLANG_BASENAME, array( $this, 'plugin_update_message' ), 10, 2 );
}
/**
* 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();
// Setup filters for admin pages
// Priority 5 to make sure filters are there before customize_register is fired
if ( $this->model->has_languages() ) {
add_action( 'wp_loaded', array( $this, 'add_filters' ), 5 );
}
}
/**
* Adds a 'settings' link for our plugin in the plugins list table.
*
* @since 0.1
*
* @param string[] $links List of links associated to the plugin.
* @return string[] Modified list of links.
*/
public function plugin_action_links( $links ) {
array_unshift( $links, '<a href="admin.php?page=mlang">' . __( 'Settings', 'polylang' ) . '</a>' );
return $links;
}
/**
* Adds the upgrade notice in plugins table
*
* @since 1.1.6
*
* @param array $plugin_data Not used
* @param object $r Plugin update data
* @return void
*/
public function plugin_update_message( $plugin_data, $r ) {
if ( ! empty( $r->upgrade_notice ) ) {
printf( '<p style="margin: 3px 0 0 0; border-top: 1px solid #ddd; padding-top: 3px">%s</p>', esc_html( $r->upgrade_notice ) );
}
}
/**
* Setup filters for admin pages
*
* @since 1.2
* @since 2.7 instantiate a PLL_Bulk_Translate instance.
* @return void
*/
public function add_filters() {
$this->filters_sanitization = new PLL_Filters_Sanitization( $this->get_locale_for_sanitization() );
$this->filters_widgets_options = new PLL_Admin_Filters_Widgets_Options( $this );
// All these are separated just for convenience and maintainability
$classes = array( 'Filters', 'Filters_Columns', 'Filters_Post', 'Filters_Term', 'Nav_Menu', 'Classic_Editor', 'Block_Editor' );
// Don't load media filters if option is disabled or if user has no right
if ( $this->options['media_support'] && ( $obj = get_post_type_object( 'attachment' ) ) && ( current_user_can( $obj->cap->edit_posts ) || current_user_can( $obj->cap->create_posts ) ) ) {
$classes[] = 'Filters_Media';
}
foreach ( $classes as $class ) {
$obj = strtolower( $class );
/**
* Filter the class to instantiate when loading admin filters
*
* @since 1.5
*
* @param string $class class name
*/
$class = apply_filters( 'pll_' . $obj, 'PLL_Admin_' . $class );
$this->$obj = new $class( $this );
}
}
/**
* Retrieve the locale according to the current language instead of the language
* of the admin interface.
*
* @since 2.0
*
* @return string
*/
public function get_locale_for_sanitization() {
$locale = get_locale();
if ( isset( $_POST['post_lang_choice'] ) && $lang = $this->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$locale = $lang->locale;
} elseif ( isset( $_POST['term_lang_choice'] ) && $lang = $this->model->get_language( sanitize_key( $_POST['term_lang_choice'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$locale = $lang->locale;
} elseif ( isset( $_POST['inline_lang_choice'] ) && $lang = $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$locale = $lang->locale;
} elseif ( ! empty( $this->curlang ) ) {
$locale = $this->curlang->locale;
}
return $locale;
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Displays the translations fields for media
* Needs WP 3.5+
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
?>
<p><strong><?php esc_html_e( 'Translations', 'polylang' ); ?></strong></p>
<table>
<?php
foreach ( $this->model->get_languages_list() as $language ) {
if ( $language->term_id == $lang->term_id ) {
continue;
}
?>
<tr>
<td class = "pll-media-language-column"><span class = "pll-translation-flag"><?php echo $language->flag; // phpcs:ignore WordPress.Security.EscapeOutput ?></span><?php echo esc_html( $language->name ); ?></td>
<td class = "pll-media-edit-column">
<?php
if ( ( $translation_id = $this->model->post->get_translation( $post_ID, $language ) ) && $translation_id !== $post_ID ) {
// The translation exists
printf(
'<input type="hidden" name="media_tr_lang[%s]" value="%d" />',
esc_attr( $language->slug ),
esc_attr( $translation_id )
);
echo $this->links->edit_post_translation_link( $translation_id ); // phpcs:ignore WordPress.Security.EscapeOutput
} else {
// No translation
echo $this->links->new_post_translation_link( $post_ID, $language ); // phpcs:ignore WordPress.Security.EscapeOutput
}
?>
</td>
</tr>
<?php
} // End foreach
?>
</table>

View File

@@ -0,0 +1,69 @@
<?php
/**
* Displays the translations fields for posts
*
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
?>
<p><strong><?php esc_html_e( 'Translations', 'polylang' ); ?></strong></p>
<table>
<?php
foreach ( $this->model->get_languages_list() as $language ) {
if ( $language->term_id === $lang->term_id ) {
continue;
}
$translation_id = $this->model->post->get_translation( $post_ID, $language );
if ( ! $translation_id || $translation_id === $post_ID ) { // $translation_id == $post_ID happens if the post has been (auto)saved before changing the language.
$translation_id = 0;
}
if ( ! empty( $from_post_id ) ) {
$translation_id = $this->model->post->get( $from_post_id, $language );
}
$add_link = $this->links->new_post_translation_link( $post_ID, $language );
$link = $add_link;
$translation = null;
if ( $translation_id ) {
$translation = get_post( $translation_id );
$link = $this->links->edit_post_translation_link( $translation_id );
}
?>
<tr>
<th class = "pll-language-column"><?php echo $language->flag ? $language->flag : esc_html( $language->slug ); // phpcs:ignore WordPress.Security.EscapeOutput ?></th>
<td class = "hidden"><?php echo $add_link; // phpcs:ignore WordPress.Security.EscapeOutput ?></td>
<td class = "pll-edit-column pll-column-icon"><?php echo $link; // phpcs:ignore WordPress.Security.EscapeOutput ?></td>
<?php
/**
* Fires before the translation column is outputted in the language metabox.
* The dynamic portion of the hook name, `$lang`, refers to the language code.
*
* @since 2.1
*/
do_action( 'pll_before_post_translation_' . $language->slug );
?>
<td class = "pll-translation-column">
<?php
printf(
'<label class="screen-reader-text" for="tr_lang_%1$s">%2$s</label>
<input type="hidden" name="post_tr_lang[%1$s]" id="htr_lang_%1$s" value="%3$s" />
<span lang="%5$s" dir="%6$s"><input type="text" class="tr_lang" id="tr_lang_%1$s" value="%4$s" /></span>',
esc_attr( $language->slug ),
/* translators: accessibility text */
esc_html__( 'Translation', 'polylang' ),
( empty( $translation ) ? 0 : esc_attr( $translation->ID ) ),
( empty( $translation ) ? '' : esc_attr( $translation->post_title ) ),
esc_attr( $language->get_locale( 'display' ) ),
( $language->is_rtl ? 'rtl' : 'ltr' )
);
?>
</td>
</tr>
<?php
}
?>
</table>

View File

@@ -0,0 +1,98 @@
<?php
/**
* Displays the translations fields for terms
*
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
if ( isset( $term_id ) ) {
// Edit term form ?>
<th scope="row"><?php esc_html_e( 'Translations', 'polylang' ); ?></th>
<td>
<?php
}
else {
// Add term form
?>
<p><?php esc_html_e( 'Translations', 'polylang' ); ?></p>
<?php
}
?>
<table class="widefat term-translations" id="<?php echo isset( $term_id ) ? 'edit' : 'add'; ?>-term-translations">
<?php
foreach ( $this->model->get_languages_list() as $language ) {
if ( $language->term_id == $lang->term_id ) {
continue;
}
// Look for any existing translation in this language
// Take care not to propose a self link
$translation = 0;
if ( isset( $term_id ) && ( $translation_id = $this->model->term->get_translation( $term_id, $language ) ) && $translation_id != $term_id ) {
$translation = get_term( $translation_id, $taxonomy );
}
if ( ! empty( $from_term_id ) && ( $translation_id = $this->model->term->get( $from_term_id, $language ) ) && ! $this->model->term->get_translation( $translation_id, $lang ) ) {
$translation = get_term( $translation_id, $taxonomy );
}
if ( isset( $term_id ) ) { // Do not display the add new link in add term form ( $term_id not set !!! )
$link = $add_link = $this->links->new_term_translation_link( $term_id, $taxonomy, $post_type, $language );
}
if ( $translation ) {
$link = $this->links->edit_term_translation_link( $translation->term_id, $taxonomy, $post_type );
}
?>
<tr>
<th class = "pll-language-column">
<span class = "pll-translation-flag"><?php echo $language->flag ? $language->flag : esc_html( $language->slug ); // phpcs:ignore WordPress.Security.EscapeOutput ?></span>
<?php
printf(
'<span class="pll-language-name%1$s">%2$s</span>',
isset( $term_id ) ? '' : ' screen-reader-text',
esc_html( $language->name )
);
?>
</th>
<?php
if ( isset( $term_id ) ) {
?>
<td class = "hidden"><?php echo $add_link; // phpcs:ignore WordPress.Security.EscapeOutput ?></td>
<td class = "pll-edit-column"><?php echo $link; // phpcs:ignore WordPress.Security.EscapeOutput ?></td>
<?php
}
?>
<td class = "pll-translation-column">
<?php
printf(
'<label class="screen-reader-text" for="tr_lang_%1$s">%2$s</label>
<input type="hidden" class="htr_lang" name="term_tr_lang[%1$s]" id="htr_lang_%1$s" value="%3$s" />
<span lang="%6$s" dir="%7$s"><input type="text" class="tr_lang" id="tr_lang_%1$s" value="%4$s"%5$s /></span>',
esc_attr( $language->slug ),
/* translators: accessibility text */
esc_html__( 'Translation', 'polylang' ),
( empty( $translation ) ? 0 : esc_attr( $translation->term_id ) ),
( empty( $translation ) ? '' : esc_attr( $translation->name ) ),
disabled( empty( $disabled ), false, false ),
esc_attr( $language->get_locale( 'display' ) ),
( $language->is_rtl ? 'rtl' : 'ltr' )
);
?>
</td>
</tr>
<?php
} // End foreach
?>
</table>
<?php
if ( isset( $term_id ) ) {
// Edit term form
?>
</td>
<?php
}