File manager - Edit - /home/rangceb/diohome.com/wp-includes6790ed/pomo/polylang.tar
Back
polylang.php 0000644 00000005406 15222563522 0007113 0 ustar 00 <?php /** * Polylang * * @package Polylang * @author WP SYNTEX * @license GPL-3.0-or-later * * @wordpress-plugin * Plugin Name: Polylang * Plugin URI: https://polylang.pro * Description: Adds multilingual capability to WordPress * Version: 3.1 * Requires at least: 5.4 * Requires PHP: 5.6 * Author: WP SYNTEX * Author URI: https://polylang.pro * Text Domain: polylang * Domain Path: /languages * License: GPL v3 or later * License URI: https://www.gnu.org/licenses/gpl-3.0.txt * * Copyright 2011-2019 Frédéric Demarle * Copyright 2019-2021 WP SYNTEX * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ if ( ! defined( 'ABSPATH' ) ) { exit; // Don't access directly. }; if ( defined( 'POLYLANG_VERSION' ) ) { // The user is attempting to activate a second plugin instance, typically Polylang and Polylang Pro. require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-includes/pluggable.php'; if ( is_plugin_active( plugin_basename( __FILE__ ) ) ) { deactivate_plugins( plugin_basename( __FILE__ ) ); // Deactivate this plugin. // WP does not allow us to send a custom meaningful message, so just tell the plugin has been deactivated. wp_safe_redirect( add_query_arg( 'deactivate', 'true', remove_query_arg( 'activate' ) ) ); exit; } } else { // Go on loading the plugin define( 'POLYLANG_VERSION', '3.1' ); define( 'PLL_MIN_WP_VERSION', '5.4' ); define( 'PLL_MIN_PHP_VERSION', '5.6' ); define( 'POLYLANG_FILE', __FILE__ ); define( 'POLYLANG_DIR', __DIR__ ); // Whether we are using Polylang or Polylang Pro, get the filename of the plugin in use. if ( ! defined( 'POLYLANG_ROOT_FILE' ) ) { define( 'POLYLANG_ROOT_FILE', __FILE__ ); } if ( ! defined( 'POLYLANG_BASENAME' ) ) { define( 'POLYLANG_BASENAME', plugin_basename( __FILE__ ) ); // Plugin name as known by WP. require __DIR__ . '/vendor/autoload.php'; } define( 'POLYLANG', ucwords( str_replace( '-', ' ', dirname( POLYLANG_BASENAME ) ) ) ); if ( empty( $_GET['deactivate-polylang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification new Polylang(); } } uninstall.php 0000644 00000007370 15222563522 0007301 0 ustar 00 <?php /** * @package Polylang */ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { // If uninstall not called from WordPress exit exit; } /** * Manages Polylang uninstallation * The goal is to remove ALL Polylang related data in db * * @since 0.5 */ class PLL_Uninstall { /** * Constructor: manages uninstall for multisite * * @since 0.5 */ public function __construct() { global $wpdb; // Don't do anything except if the constant PLL_REMOVE_ALL_DATA is explicitely defined and true. if ( ! defined( 'PLL_REMOVE_ALL_DATA' ) || ! PLL_REMOVE_ALL_DATA ) { return; } // Check if it is a multisite uninstall - if so, run the uninstall function for each blog id if ( is_multisite() ) { foreach ( $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" ) as $blog_id ) { switch_to_blog( $blog_id ); $this->uninstall(); } restore_current_blog(); } else { $this->uninstall(); } } /** * Removes ALL plugin data * only when the relevant option is active * * @since 0.5 */ public function uninstall() { global $wpdb; do_action( 'pll_uninstall' ); // Need to register the taxonomies $pll_taxonomies = array( 'language', 'term_language', 'post_translations', 'term_translations' ); foreach ( $pll_taxonomies as $taxonomy ) { register_taxonomy( $taxonomy, null, array( 'label' => false, 'public' => false, 'query_var' => false, 'rewrite' => false ) ); } $languages = get_terms( 'language', array( 'hide_empty' => false ) ); // Delete users options foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { delete_user_meta( $user_id, 'pll_filter_content' ); delete_user_meta( $user_id, 'pll_dismissed_notices' ); // Legacy meta. foreach ( $languages as $lang ) { delete_user_meta( $user_id, 'description_' . $lang->slug ); } } // Delete menu language switchers $ids = get_posts( array( 'post_type' => 'nav_menu_item', 'numberposts' => -1, 'nopaging' => true, 'fields' => 'ids', 'meta_key' => '_pll_menu_item', ) ); foreach ( $ids as $id ) { wp_delete_post( $id, true ); } // Delete the strings translations. register_post_type( 'polylang_mo', array( 'rewrite' => false, 'query_var' => false ) ); $ids = get_posts( array( 'post_type' => 'polylang_mo', 'post_status' => 'any', 'numberposts' => -1, 'nopaging' => true, 'fields' => 'ids', ) ); foreach ( $ids as $id ) { wp_delete_post( $id, true ); } // Delete all what is related to languages and translations $term_ids = array(); $tt_ids = array(); foreach ( get_terms( $pll_taxonomies, array( 'hide_empty' => false ) ) as $term ) { $term_ids[] = (int) $term->term_id; $tt_ids[] = (int) $term->term_taxonomy_id; } if ( ! empty( $term_ids ) ) { $term_ids = array_unique( $term_ids ); $wpdb->query( "DELETE FROM {$wpdb->terms} WHERE term_id IN ( " . implode( ',', $term_ids ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "DELETE FROM {$wpdb->term_taxonomy} WHERE term_id IN ( " . implode( ',', $term_ids ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared } if ( ! empty( $tt_ids ) ) { $tt_ids = array_unique( $tt_ids ); $wpdb->query( "DELETE FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', $tt_ids ) . ' )' ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared } // Delete options delete_option( 'polylang' ); delete_option( 'widget_polylang' ); // Automatically created by WP delete_option( 'polylang_wpml_strings' ); // Strings registered with icl_register_string delete_option( 'polylang_licenses' ); delete_option( 'pll_dismissed_notices' ); // Delete transients delete_transient( 'pll_languages_list' ); } } new PLL_Uninstall(); admin/admin.php 0000644 00000011411 15222563522 0007437 0 ustar 00 <?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 */ public $filters; /** * @var PLL_Admin_Filters_Columns */ public $filters_columns; /** * @var PLL_Admin_Filters_Post */ public $filters_post; /** * @var PLL_Admin_Filters_Term */ public $filters_term; /** * @var PLL_Admin_Filters_Media */ public $filters_media; /** * @since 2.9 * * @var PLL_Filters_Sanitization */ public $filters_sanitization; /** * @var PLL_Admin_Block_Editor */ public $block_editor; /** * @var PLL_Admin_Classic_Editor */ public $classic_editor; /** * @var PLL_Admin_Nav_Menu */ public $nav_menu; /** * @var PLL_Admin_Filters_Widgets_Options */ 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->get_languages_list() ) { 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 ( isset( $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; } } admin/admin-notices.php 0000644 00000014551 15222563522 0011111 0 ustar 00 <?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 */ 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 database * * @since 2.3.9 * * @param string $notice * @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 } } admin/admin-filters-columns.php 0000644 00000036147 15222563522 0012600 0 ustar 00 <?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 */ public $links; /** * Language selected in the admin language filter. * * @var PLL_Language */ public $filter_lang; /** * Constructor: setups filters and actions * * @since 1.2 * * @param object $polylang */ 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 * @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' => __( '— No Change —', '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 'Categories' or 'Post Tags' table * * @since 0.1 * * @param string $out * @param string $column Column name * @param int $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 ( $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; if ( $tag = get_term( $term_id, $taxonomy ) ) { 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 teh 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 ) ); } } admin/view-translations-term.php 0000644 00000006127 15222563522 0013015 0 ustar 00 <?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 } admin/admin-strings.php 0000644 00000010360 15222563522 0011130 0 ustar 00 <?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[] */ 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; } } admin/admin-default-term.php 0000644 00000017155 15222563522 0012041 0 ustar 00 <?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 */ protected $pref_lang; /** * Reference to Polylang options array * * @var array */ protected $options; /** * 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 */ public function __construct( &$polylang ) { $this->model = &$polylang->model; $this->pref_lang = &$polylang->pref_lang; $this->options = &$polylang->options; $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_language( $this->options['default_lang'] ); $this->model->term->set_language( (int) $value, $default_cat_lang ); } $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 ] ); } } } } } admin/admin-static-pages.php 0000644 00000013424 15222563522 0012027 0 ustar 00 <?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 */ protected $links; /** * Constructor: setups filters and actions * * @since 1.8 * * @param object $polylang */ public function __construct( &$polylang ) { parent::__construct( $polylang ); $this->links = &$polylang->links; // Removes the editor and the template select dropdown for pages for posts add_filter( 'use_block_editor_for_post', array( $this, 'use_block_editor_for_post' ), 10, 2 ); // Since WP 5.0 add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ), 10, 2 ); // Add post state for translations of the front page and posts page add_filter( 'display_post_states', array( $this, 'display_post_states' ), 10, 2 ); // Refresh language cache when a static front page 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' ) ); } /** * Don't use the block editor for the translations of the pages for posts * * @since 2.5 * * @param bool $use_block_editor Whether the post can be edited or not. * @param WP_Post $post The post being checked. * @return bool */ public function use_block_editor_for_post( $use_block_editor, $post ) { if ( 'page' === $post->post_type ) { add_filter( 'option_page_for_posts', array( $this, 'translate_page_for_posts' ) ); if ( ( get_option( 'page_for_posts' ) == $post->ID ) && empty( $post->post_content ) ) { return false; } } return $use_block_editor; } /** * Removes the editor for the translations of the pages for posts. * Removes the page template select dropdown in page attributes metabox too. * * @since 2.2.2 * * @param string $post_type Current post type. * @param WP_Post $post Current post. * @return void */ public function add_meta_boxes( $post_type, $post ) { if ( 'page' === $post_type ) { add_filter( 'option_page_for_posts', array( $this, 'translate_page_for_posts' ) ); if ( ( get_option( 'page_for_posts' ) == $post->ID ) && empty( $post->post_content ) ) { add_action( 'edit_form_after_title', '_wp_posts_page_notice' ); remove_post_type_support( $post_type, 'editor' ); } } } /** * 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 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 ) ) { $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 * @param string $old_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; } } admin/admin-filters-widgets-options.php 0000644 00000002252 15222563522 0014245 0 ustar 00 <?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 ); } } } admin/view-translations-post.php 0000644 00000004433 15222563522 0013031 0 ustar 00 <?php /** * Displays the translations fields for posts * * @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; } $value = $this->model->post->get_translation( $post_ID, $language ); if ( ! $value || $value == $post_ID ) { // $value == $post_ID happens if the post has been (auto)saved before changing the language $value = ''; } if ( ! empty( $from_post_id ) ) { $value = $this->model->post->get( $from_post_id, $language ); } $link = $add_link = $this->links->new_post_translation_link( $post_ID, $language ); if ( $value ) { $selected = get_post( $value ); $link = $this->links->edit_post_translation_link( $value ); } ?> <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( $value ) ? 0 : esc_attr( $selected->ID ) ), ( empty( $value ) ? '' : esc_attr( $selected->post_title ) ), esc_attr( $language->get_locale( 'display' ) ), ( $language->is_rtl ? 'rtl' : 'ltr' ) ); ?> </td> </tr> <?php } ?> </table> admin/admin-classic-editor.php 0000644 00000025371 15222563522 0012354 0 ustar 00 <?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 */ public $links; /** * Current language (used to filter the content). * * @var PLL_Language */ public $curlang; /** * Preferred language to assign to new contents. * * @var PLL_Language */ public $pref_lang; /** * Constructor: setups filters and actions * * @since 2.4 * * @param object $polylang */ 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->update_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' => '— ' . $taxonomy->labels->parent_item . ' —', '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 = apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post ); // Since WP 3.3 $x->Add( array( 'what' => 'pages', 'data' => wp_dropdown_pages( $dropdown_args ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput } } // 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 ) { $dropdown_args['lang'] = isset( $_POST['lang'] ) ? $this->model->get_language( sanitize_key( $_POST['lang'] ) ) : $this->model->post->get_language( $post->ID ); // phpcs:ignore WordPress.Security.NonceVerification if ( ! $dropdown_args['lang'] ) { $dropdown_args['lang'] = $this->pref_lang; } 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 } } } admin/admin-links.php 0000644 00000015537 15222563522 0010572 0 ustar 00 <?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 '&'. * 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 ); } } admin/block-editor-filter-preload-paths.php 0000644 00000004114 15222563522 0014753 0 ustar 00 <?php /** * @package Polylang */ /** * This class handles the deprecated filter 'block_editor_preload_paths' * to replace it by the new filter 'block_editor_rest_api_preload_paths' * and is used for backward compatibility with WP < 5.8. * * @since 3.1 */ class PLL_Block_Editor_Filter_Preload_Paths { /** * @var callable */ private $callback; /** * PLL_Block_Editor_Filter_Preload_Paths constructor. * * @since 3.1 * * @param callable $callback Function or method to be executed when the filter is triggered. * @param int $priority Priority of execution for the given callback. Default 10. * @param int $arguments_number Number of arguments to pass to the callback. Default 1. */ public function __construct( $callback, $priority = 10, $arguments_number = 1 ) { $this->callback = $callback; if ( class_exists( 'WP_Block_Editor_Context' ) ) { // Since WP 5.8. add_filter( 'block_editor_rest_api_preload_paths', array( $this, 'block_editor_rest_api_preload_paths' ), $priority, $arguments_number ); } else { add_filter( 'block_editor_preload_paths', $callback, $priority, $arguments_number ); } } /** * Filters the array of REST API paths that will be used to preloaded common data to use with the block editor. * * Converts the WP_Block_Editor_Context object to a WP_Post if provided in the context. * * @since 3.1 * * @param string[] $preload_paths The preload paths loaded by the Block Editor. * @param WP_Block_Editor_Context $block_editor_context The post resource data. * @return array|mixed|string[] (string|string[])[] */ public function block_editor_rest_api_preload_paths( $preload_paths, $block_editor_context = null ) { if ( null === $block_editor_context ) { return call_user_func( $this->callback, $preload_paths ); } elseif ( ! empty( $block_editor_context->post ) ) { return call_user_func_array( $this->callback, array( $preload_paths, $block_editor_context->post ) ); } return call_user_func_array( $this->callback, array( $preload_paths, $block_editor_context ) ); } } admin/admin-filters-post-base.php 0000644 00000002313 15222563522 0013001 0 ustar 00 <?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 */ public $links; /** * Language selected in the admin language filter. * * @var PLL_Language */ public $filter_lang; /** * Constructor: setups filters and actions * * @since 1.2 * * @param object $polylang */ 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; } } admin/admin-filters.php 0000644 00000007754 15222563522 0011124 0 ustar 00 <?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 */ 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 language user preference set in user profile * * @since 0.4 * * @param int $user_id * @return void */ public function personal_options_update( $user_id ) { // Biography translations foreach ( $this->model->get_languages_list() as $lang ) { $meta = $lang->slug == $this->options['default_lang'] ? '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->slug == $this->options['default_lang'] ? 'description' : 'description_' . $lang->slug; /** This filter is documented in wp-includes/user.php */ $description = apply_filters( 'user_description', get_user_meta( $profileuser->ID, $meta, true ) ); // Applies WP default filter wp_kses_data printf( '<input type="hidden" class="biography" name="%s___%s" value="%s" />', esc_attr( $lang->slug ), esc_attr( $lang->name ), esc_attr( $description ) ); } } /** * 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 a text direction dependent class to the body * * @since 2.2 * * @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' ); } 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; } } admin/admin-nav-menu.php 0000644 00000023600 15222563522 0011166 0 ustar 00 <?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 */ 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 not used * @param int $menu_item_db_id * @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' ) && 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 * @return bool|array modified list of menu locations */ 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 ); } } } admin/admin-filters-term.php 0000644 00000051503 15222563522 0012060 0 ustar 00 <?php /** * @package Polylang */ /** * Manages filters and actions related to terms on admin side * * @since 1.2 */ class PLL_Admin_Filters_Term { /** * Stores the plugin options. * * @var array */ public $options; /** * @var PLL_Model */ public $model; /** * @var PLL_Admin_Links */ public $links; /** * Language selected in the admin language filter. * * @var PLL_Language */ public $filter_lang; /** * Preferred language to assign to the new terms. * * @var PLL_Language */ public $pref_lang; /** * Stores the term name before creating a slug if needed. * * @var string */ protected $pre_term_name; /** * Stores the current post_id when bulk editing posts. * * @var int */ protected $post_id; /** * A reference to the PLL_Admin_Default_Term instance. * * @since 2.8 * * @var PLL_Admin_Default_Term */ protected $default_term; /** * Constructor: setups filters and actions * * @param object $polylang */ public function __construct( &$polylang ) { $this->links = &$polylang->links; $this->model = &$polylang->model; $this->options = &$polylang->options; $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( 'pre_term_name', array( $this, 'pre_term_name' ) ); add_filter( 'pre_term_slug', array( $this, 'pre_term_slug' ), 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 pre_term_slug * * @since 1.7 * * @param int $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 language * * @since 1.5 * * @param int $term_id * @param string $taxonomy * @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 } $this->model->term->set_language( $term_id, $this->model->get_language( sanitize_key( $_POST['term_lang_choice'] ) ) ); } // *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 == $_GET['inline_lang_choice'] ) { // The language of the current term is set a according to the language of the current post $this->model->term->set_language( $term_id, $this->model->post->get_language( $this->post_id ) ); $term = get_term( $term_id, $taxonomy ); // 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 $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 ); } } else { if ( 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->update_language( $term_id, $lang ); } // Edit post elseif ( isset( $_POST['post_lang_choice'] ) ) { // FIXME should be useless now check_admin_referer( 'pll_language', '_pll_nonce' ); $this->model->term->set_language( $term_id, $this->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) ) ); } } /** * Save translations from our form. * * @since 1.5 * * @param int $term_id The term id of teh 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 * @param int $tt_id term taxonomy id * @param string $taxonomy * @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 ); } } } /** * Stores the term name for use in pre_term_slug * * @since 0.9.5 * * @param string $name term name * @return string unmodified term name */ public function pre_term_name( $name ) { return $this->pre_term_name = $name; } /** * Creates the term slug in case the term already exists in another language * * @since 0.9.5 * * @param string $slug * @param string $taxonomy * @return string */ public function pre_term_slug( $slug, $taxonomy ) { $name = sanitize_title( $this->pre_term_name ); // If the term already exists in another language if ( ! $slug && $this->model->is_translated_taxonomy( $taxonomy ) && term_exists( $name, $taxonomy ) ) { if ( isset( $_POST['term_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $lang = $this->model->get_language( sanitize_key( $_POST['term_lang_choice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } elseif ( isset( $_POST['inline_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification $lang = $this->model->get_language( sanitize_key( $_POST['inline_lang_choice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } // *Post* bulk edit, in case a new term is created elseif ( isset( $_GET['bulk_edit'], $_GET['inline_lang_choice'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification // Bulk edit does not modify the language if ( -1 == $_GET['inline_lang_choice'] ) { // phpcs:ignore WordPress.Security.NonceVerification $lang = $this->model->post->get_language( $this->post_id ); } else { $lang = $this->model->get_language( sanitize_key( $_GET['inline_lang_choice'] ) ); // phpcs:ignore WordPress.Security.NonceVerification } } if ( ! empty( $lang ) && ! $this->model->term_exists_by_slug( $name, $lang, $taxonomy ) ) { $slug = $name . '-' . $lang->slug; } } return $slug; } /** * 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' ) ); } if ( $tag_cloud = wp_tag_cloud( $args ) ) { $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( $taxonomy, 'hide_empty=0&lang=0&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 ) { $return[] = array( 'id' => $term->term_id, 'value' => rtrim( // Trim the seperator added at the end by WP. get_term_parents_list( $term->term_id, $term->taxonomy, array( 'separator' => ' > ', 'link' => false, ) ), ' >' ), '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 Shared term_id * @param int $new_term_id * @param int $term_taxonomy_id * @param string $taxonomy * @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 ); $translations[ $key ] = _split_shared_term( $tr_id, $tr_term->term_taxonomy_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; } } admin/view-translations-media.php 0000644 00000002457 15222563522 0013127 0 ustar 00 <?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> admin/admin-model.php 0000644 00000045135 15222563522 0010547 0 ustar 00 <?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 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->get_error_code() ) { // Using has_errors() would be more meaningful but is available only since WP 5.0 return $errors; } // First the language taxonomy $description = maybe_serialize( array( 'locale' => $args['locale'], 'rtl' => (int) $args['rtl'], 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'] ) ); $r = wp_insert_term( $args['name'], 'language', array( 'slug' => $args['slug'], 'description' => $description ) ); 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 term_language taxonomy // Don't want shared terms so use a different slug wp_insert_term( $args['name'], 'term_language', array( 'slug' => 'pll_' . $args['slug'] ) ); $this->clean_languages_cache(); // Update the languages list now ! 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 ); } // Init a mo_id for this language $mo = new PLL_MO(); $mo->export_to_db( $this->get_language( $args['slug'] ) ); /** * 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 ); $this->clean_languages_cache(); // Again to set add mo_id in the cached languages list flush_rewrite_rules(); // Refresh rewrite rules 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 ( $this->options['default_lang'] == $lang->slug ) { $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 foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { delete_user_meta( $user_id, 'pll_filter_content', $lang->slug ); delete_user_meta( $user_id, 'description_' . $lang->slug ); } // Delete the string translations $post = wpcom_vip_get_page_by_title( 'polylang_mo_' . $lang->term_id, OBJECT, 'polylang_mo' ); if ( $post instanceof WP_Post ) { wp_delete_post( $post->ID ); } // Delete domain unset( $this->options['domains'][ $lang->slug ] ); // Delete the language itself wp_delete_term( $lang->term_id, 'language' ); wp_delete_term( $lang->tl_term_id, 'term_language' ); // Update languages list $this->clean_languages_cache(); 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'] ); $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 ( $this->options['default_lang'] == $old_slug ) { $this->options['default_lang'] = $slug; } } update_option( 'polylang', $this->options ); // And finally update the language itself $description = maybe_serialize( array( 'locale' => $args['locale'], 'rtl' => (int) $args['rtl'], 'flag_code' => empty( $args['flag'] ) ? '' : $args['flag'] ) ); wp_update_term( (int) $lang->term_id, 'language', array( 'slug' => $slug, 'name' => $args['name'], 'description' => $description, 'term_group' => (int) $args['term_group'] ) ); if ( empty( $lang->tl_term_id ) ) { // Attempt to repair the term_language if it has been deleted by a database cleaning tool. wp_insert_term( $args['name'], 'term_language', array( 'slug' => 'pll_' . $slug ) ); } else { wp_update_term( (int) $lang->tl_term_id, 'term_language', array( 'slug' => 'pll_' . $slug, 'name' => $args['name'] ) ); } /** * Fires when a language is updated. * * @since 1.9 * * @param array $args Arguments used to modify the language. @see PLL_Admin_Model::update_language(). */ do_action( 'pll_update_language', $args ); $this->clean_languages_cache(); flush_rewrite_rules(); // Refresh rewrite rules return true; } /** * 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 $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'] ) && ! file_exists( 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; } /** * Assigns a language to posts or terms in mass. * * @since 1.2 * * @param string $type Either 'post' or 'term'. * @param int[] $ids Array of post ids or term ids. * @param PLL_Language|string $lang Language to assign to the posts or terms. * @return void */ public function set_language_in_mass( $type, $ids, $lang ) { global $wpdb; $lang = $this->get_language( $lang ); if ( empty( $lang ) ) { return; } $tt_id = 'term' === $type ? $lang->tl_term_taxonomy_id : $lang->term_taxonomy_id; $values = array(); $ids = array_map( 'intval', $ids ); foreach ( $ids as $id ) { $values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id ); } if ( ! empty( $values ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) ); $lang->update_count(); // Updating term count is mandatory ( thanks to AndyDeGroo ) } if ( 'term' === $type ) { clean_term_cache( $ids, 'term_language' ); $translations = array(); foreach ( $ids as $id ) { $translations[] = array( $lang->slug => $id ); } if ( ! empty( $translations ) ) { $this->set_translation_in_mass( 'term', $translations ); } } else { clean_term_cache( $ids, 'language' ); } } /** * Creates translations groups in mass. * * @since 1.6.3 * * @param string $type Either 'post' or 'term' * @param array $translations Array of translations arrays. * @return void */ public function set_translation_in_mass( $type, $translations ) { global $wpdb; $taxonomy = $type . '_translations'; $terms = array(); $slugs = array(); $description = array(); $count = array(); foreach ( $translations as $t ) { $term = uniqid( 'pll_' ); // the term name $terms[] = $wpdb->prepare( '( %s, %s )', $term, $term ); $slugs[] = $wpdb->prepare( '%s', $term ); $description[ $term ] = maybe_serialize( $t ); $count[ $term ] = count( $t ); } // Insert terms if ( ! empty( $terms ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->terms} ( slug, name ) VALUES " . implode( ',', array_unique( $terms ) ) ); } // Get all terms with their term_id // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $terms = $wpdb->get_results( "SELECT term_id, slug FROM {$wpdb->terms} WHERE slug IN ( " . implode( ',', $slugs ) . ' )' ); $term_ids = array(); $tts = array(); // Prepare terms taxonomy relationship foreach ( $terms as $term ) { $term_ids[] = $term->term_id; $tts[] = $wpdb->prepare( '( %d, %s, %s, %d )', $term->term_id, $taxonomy, $description[ $term->slug ], $count[ $term->slug ] ); } // Insert term_taxonomy if ( ! empty( $tts ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_taxonomy} ( term_id, taxonomy, description, count ) VALUES " . implode( ',', array_unique( $tts ) ) ); } // Get all terms with term_taxonomy_id $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); $trs = array(); // Prepare objects relationships. if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $t = maybe_unserialize( $term->description ); if ( in_array( $t, $translations ) ) { foreach ( $t as $object_id ) { if ( ! empty( $object_id ) ) { $trs[] = $wpdb->prepare( '( %d, %d )', $object_id, $term->term_taxonomy_id ); } } } } } // Insert term_relationships if ( ! empty( $trs ) ) { // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared $wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', $trs ) ); $trs = array_unique( $trs ); } clean_term_cache( $term_ids, $taxonomy ); } /** * 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; $terms = get_terms( array( 'post_translations', 'term_translations' ) ); $term_ids = array(); $dr = array(); $dt = array(); $ut = array(); if ( is_array( $terms ) ) { foreach ( $terms as $term ) { $term_ids[ $term->taxonomy ][] = $term->term_id; $tr = maybe_unserialize( $term->description ); 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(); } } admin/admin-block-editor.php 0000644 00000002746 15222563522 0012026 0 ustar 00 <?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; /** * Preferred language to assign to a new post. * * @var PLL_Language */ protected $pref_lang; /** * Constructor: setups filters and actions * * @since 2.5 * * @param object $polylang */ public function __construct( &$polylang ) { $this->model = &$polylang->model; $this->pref_lang = &$polylang->pref_lang; new PLL_Block_Editor_Filter_Preload_Paths( array( $this, 'preload_paths' ), 10, 2 ); } /** * Filters the preload REST requests by the current language of the post. * Necessary otherwise subsequent REST requests, all filtered by the language, * would not hit the preloaded requests. * * @since 2.5 * * @param (string|string[])[] $preload_paths Array of paths to preload. * @param WP_Post $post The post resource data. * @return (string|string[])[] */ public function preload_paths( $preload_paths, $post ) { if ( $post instanceof WP_Post && $this->model->is_translated_post_type( $post->post_type ) ) { $lang = $this->model->post->get_language( $post->ID ); if ( ! $lang ) { $lang = $this->pref_lang; } foreach ( $preload_paths as $k => $path ) { if ( is_string( $path ) && '/' !== $path ) { $preload_paths[ $k ] = $path . "&lang={$lang->slug}"; } } } return $preload_paths; } } admin/admin-filters-media.php 0000644 00000007124 15222563522 0012170 0 ustar 00 <?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 */ public $posts; /** * Constructor: setups filters and actions * * @since 1.2 * * @param object $polylang */ 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 * @param array $attachment * @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'] ) ) { $this->model->post->update_language( $post['ID'], $this->model->get_language( $attachment['language'] ) ); } return $post; } } admin/admin-base.php 0000644 00000043007 15222563522 0010355 0 ustar 00 <?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 */ public $curlang; /** * Language selected in the admin language filter. * * @var PLL_Language */ public $filter_lang; /** * Preferred language to assign to new contents. * * @var PLL_Language */ public $pref_lang; /** * @var PLL_Filters_Links */ public $filters_links; /** * @var PLL_Admin_Links */ public $links; /** * @var PLL_Admin_Notices */ public $notices; /** * @var PLL_Admin_Static_Pages */ public $static_pages; /** * @var PLL_Admin_Default_Term */ 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' ) ); // 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' ) ); } /** * 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 ); if ( ! $this->model->get_languages_list() ) { return; } $this->links = new PLL_Admin_Links( $this ); // FIXME needed here ? $this->static_pages = new PLL_Admin_Static_Pages( $this ); // FIXME needed here ? $this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ? $this->default_term = new PLL_Admin_Default_Term( $this ); $this->default_term->add_hooks(); // 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->get_languages_list() ) { $tabs['strings'] = __( '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 => 1 if loaded even if languages have not been defined yet, 0 otherwise * 3 => 1 if loaded in footer */ $scripts = array( 'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), 0, 0 ), 'widgets' => array( array( 'widgets' ), array( 'jquery' ), 0, 0 ), ); 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' ), 0, 1 ); // 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' ), 0, 1 ); } // Block editor with legacy metabox in WP 5.0+. if ( method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin() ) { $scripts['block-editor'] = array( array( 'post' ), array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 ); } } 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' ), 0, 1 ); } foreach ( $scripts as $script => $v ) { if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->get_languages_list() ) ) { 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->localize_scripts(); } /** * Enqueue scripts to the WP Customizer. * * @since 2.4.0 * * @return void */ public function customize_controls_enqueue_scripts() { if ( $this->model->get_languages_list() ) { $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->localize_scripts(); } } /** * Localize scripts. * * @since 2.4.0 * * @return void */ public function localize_scripts() { 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 ) ); } $str = http_build_query( $params ); $arr = wp_json_encode( $params ); ?> <script type="text/javascript"> 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 datas. */ 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 } // Inform that the admin language has been set. if ( $this->curlang ) { /** 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_language( $this->options['default_lang'] ) : $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(); // Plugin i18n, only needed for backend. load_plugin_textdomain( 'polylang' ); } /** * 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 * @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>', 'all' === $selected->slug ? '' : 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' ) ) ), ) ); } } } admin/admin-filters-post.php 0000644 00000014072 15222563522 0012076 0 ustar 00 <?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 */ public $curlang; /** * Constructor: setups filters and actions * * @since 1.2 * * @param object $polylang */ 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, 'object' ) ) { // 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( $hierarchical_taxonomies, array( '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 ) ) { $this->model->post->set_language( $post_id, $this->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) ) ); if ( isset( $_POST['post_tr_lang'] ) ) { $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->update_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->update_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; } } integrations/integrations.php 0000644 00000002074 15222563522 0012500 0 ustar 00 <?php /** * @package Polylang */ /** * Container for 3rd party plugins ( and themes ) integrations. * This class is available as soon as the plugin is loaded. * * @since 1.0 * @since 2.8 Renamed from PLL_Plugins_Compat to PLL_Integrations. */ class PLL_Integrations { /** * Singleton instance. * * @var PLL_Integrations */ protected static $instance; /** * Constructor. * * @since 1.0 */ protected function __construct() { // Loads external integrations. foreach ( glob( __DIR__ . '/*/load.php', GLOB_NOSORT ) as $load_script ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable require_once $load_script; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable } } /** * Access to the single instance of the class. * * @since 1.7 * * @return object */ public static function instance() { if ( empty( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } } class_alias( 'PLL_Integrations', 'PLL_Plugins_Compat' ); // For Backward compatibility. readme.txt 0000644 00000024235 15222563522 0006554 0 ustar 00 === Polylang === Contributors: Chouby, manooweb, raaaahman, marianne38, sebastienserre Donate link: https://polylang.pro Tags: multilingual, bilingual, translate, translation, language, multilanguage, international, localization Requires at least: 5.4 Tested up to: 5.8 Requires PHP: 5.6 Stable tag: 3.1 License: GPLv3 or later License URI: https://www.gnu.org/licenses/gpl-3.0.html Making WordPress multilingual == Description == = Features = Polylang allows you to create a bilingual or multilingual WordPress site. You write posts, pages and create categories and post tags as usual, and then define the language for each of them. The translation of a post, whether it is in the default language or not, is optional. * You can use as many languages as you want. RTL language scripts are supported. WordPress languages packs are automatically downloaded and updated. * You can translate posts, pages, media, categories, post tags, menus, widgets... * Custom post types, custom taxonomies, sticky posts and post formats, RSS feeds and all default WordPress widgets are supported. * The language is either set by the content or by the language code in url, or you can use one different subdomain or domain per language * Categories, post tags as well as some other metas are automatically copied when adding a new post or page translation * A customizable language switcher is provided as a widget or in the nav menu > The author does not provide support on the wordpress.org forum. Support and extra features are available to [Polylang Pro](https://polylang.pro) users. If you wish to migrate from WPML, you can use the plugin [WPML to Polylang](https://wordpress.org/plugins/wpml-to-polylang/) If you wish to use a professional or automatic translation service, you can install [Lingotek Translation](https://wordpress.org/plugins/lingotek-translation/), as an addon of Polylang. Lingotek offers a complete translation management system which provides services such as translation memory or semi-automated translation processes (e.g. machine translation > human translation > legal review). = Credits = Thanks a lot to all translators who [help translating Polylang](https://translate.wordpress.org/projects/wp-plugins/polylang). Thanks a lot to [Alex Lopez](http://www.alexlopez.rocks/) for the design of the logo. Most of the flags included with Polylang are coming from [famfamfam](http://famfamfam.com/) and are public domain. Wherever third party code has been used, credit has been given in the code’s comments. = Do you like Polylang? = Don't hesitate to [give your feedback](http://wordpress.org/support/view/plugin-reviews/polylang#postform). == Installation == 1. Make sure you are using WordPress 5.1 or later and that your server is running PHP 5.6 or later (same requirement as WordPress itself) 1. If you tried other multilingual plugins, deactivate them before activating Polylang, otherwise, you may get unexpected results! 1. Install and activate the plugin as usual from the 'Plugins' menu in WordPress. 1. Go to the languages settings page and create the languages you need 1. Add the 'language switcher' widget to let your visitors switch the language. 1. Take care that your theme must come with the corresponding .mo files (Polylang automatically downloads them when they are available for themes and plugins in this repository). If your theme is not internationalized yet, please refer to the [Theme Handbook](https://developer.wordpress.org/themes/functionality/internationalization/) or ask the theme author to internationalize it. == Frequently Asked Questions == = Where to find help ? = * First time users should read [Polylang - Getting started](https://polylang.pro/doc-category/getting-started/), which explains the basics with a lot of screenshots. * Read the [documentation](https://polylang.pro/doc/). It includes a [FAQ](https://polylang.pro/doc-category/faq/) and the [documentation for developers](https://polylang.pro/doc-category/developers/). * Search the [community support forum](https://wordpress.org/search/). You will probably find your answer here. * Read the sticky posts in the [community support forum](http://wordpress.org/support/plugin/polylang). * If you still have a problem, open a new thread in the [community support forum](http://wordpress.org/support/plugin/polylang). * [Polylang Pro](https://polylang.pro) users have access to our helpdesk. = Is Polylang compatible with WooCommerce? = * You need a separate addon to make Polylang and WooCommerce work together. [A Premium addon](https://polylang.pro/downloads/polylang-for-woocommerce/) is available. = Do you need translation services? = * If you want to use professional or automatic translation services, install and activate the [Lingotek Translation](https://wordpress.org/plugins/lingotek-translation/) plugin. == Screenshots == 1. The Polylang languages admin panel 2. The Strings translations admin panel 3. Multilingual media library 4. The Edit Post screen with the Languages metabox == Changelog == = 3.1 (2021-07-27) = * Add compatibility with WordPress 5.8 * Raise Minimum WordPress version to 5.4 * Pro: Allow to filter blocks by language in the widget block editor * Pro: Allow to export and import XLIFF files for string translations * Pro: Add the language switcher in the navigation block (experimental) * Pro: Replace dashicons by svg icons in the block editor * Pro: The Events Calendar: Add compatibility with Views V2 (only for sites using only one domain) * Pro: Fix + icon displayed in the block editor sidebar when the user cannot create a translation * Add a warning section to the site health for posts and terms without languages #825 * Require the simplexml extension in the site health if a wpml-config.xml is found #827 * Remove the information about the WPML compabitility mode in settings #843 * The browser preferred language detection is now deactivated by default * The media are now untranslated by default * Highlight the language filter in the admin toolbar when it's active #821 * Allow to query comments in multiple languages (just as posts and terms) #840 * Don't disable the translation input field in the classic metabox #841 Props Onatcer * Optimize all images including flags #848 Props lowwebtech * Don't redirect if WordPress doesn't validate the redirect url to avoid redirects to /wp-admin/ #879 * Fix media appearing to have a language after the language is changed in the media library grid view #807 * Fix media not all deleted when bulk deleting from the grid view of the media library #830 * Fix when more than one language switcher are added to the same menu #853 * Fix PHP notice when adding a CPT archive link to a menu #868 Props davidwebca = 3.0.6 (2021-06-22) = * Fix a conflict with the WooCommerce cart translation and cache plugins #876 = 3.0.5 (2021-06-08) = * Pro: fix original post not assigned to a new translation when the languages sidebar is closed * Pro: Attempt to fix zip file corrupted on some installations when exporting string translations * Support session cookie with the pll_cookie_expiration filter #835 * Fix javascript error when a plugin defines its own editor for translated post types #837 * Fix languages displayed in screen options when editing a term #850 * Cache: fix post type archive cache not cleared when saving a post #828 = 3.0.4 (2021-04-27) = * Improve performance in the pages (or hierarchical post types) list table * Fix an ajax conflict with WooCommerce License manager = 3.0.3 (2021-03-23) = * Fix a warning when a language is corrupted in database (term_language missing) * Fix confirmation modal with WooCommerce and WordPress < 5.6 * Fix an ajax conflict with WooCommerce Tree Table Rate Shipping and HubSpot All-In-One Marketing = 3.0.2 (2021-03-16) = * Move hreflang attributes higher in the head section #771 * Fix custom flags not working (introduced in 3.0) * Fix translation of the confirmation modal when changing the language of a post * Fix js and css not loaded when Polylang is used as a mu-plugin (introduced in 3.0) * Fix support for html5 stylesheet link tags #775 * Fix possible warning in frontend-filters-links.php * Yoast SEO Premium: Take over the multilingual compatibility removed in Yoast SEO Premium 15.8 #796 * Yoast SEO: Fix CPT breadcrumb title when the option is left empty #794 * Yoast SEO: Fix sitemap.xml not redirected on secondary domains #789 = 3.0.1 (2021-03-10) = * Fix media gallery messed when editing a post in the classic editor * Fix missing script dependency on old WP versions * Fix CSS conflict with WooCommerce Bookings for WP < 5.6 * Fix conflict resulting in '__' already defined in block-editor.js. #779 * Fix search form removed for some themes. Props Marián Kadaňka. #780 * Fix fatal error with very old versions of Yoast SEO. Props Nicola Peluchetti. #781 = 3.0 (2021-03-08) = * Add compatibility with WordPress 5.7 * Remove upgrades from Polylang older than 1.8 * Remove deprecated class PLL_Pointer * Pro: Hide the license keys * Pro: Fix redirect to the home page of a deactivated language * Pro: Fix synchronization of post status not working * Pro: Fix language switcher block not working in a post retrieved in REST API * Pro: Fix PO export of strings with line breaks * Pro: Fix file block title customization lost * Add a dialog box to ask a confirmation about a language change in classic and block editors * Improve browser language detection #591 * Improve robustness and documentation of code * Fix media library after the language has been chnaged in the editor metabox * Fix duplicated title attribute on flag link in posts list * Fix legacy block editor language metabox compatibility with WordPress 5.6 * Fix uploaded theme and plugin files in media library * Fix site title not translated in email change confirmation email * Fix remaining deprecated jQuery notices #741 * Fix compatibility with GN publisher * Fix compatibility with Woodmart theme search form * Fix compatibility issue with 3rd party ajax requests since jQuery 3.3 #744 * Fix CSS conflict with WooCommerce Bookings * Fix browser error when displaying an embed and using a cache plugin #757 * Fix post type archive title and metadesc not translated in Yoast SEO * Fix PHP notice in REST API See [changelog.txt](https://plugins.svn.wordpress.org/polylang/trunk/changelog.txt) for older changelog flags/mt.png 0000644 00000000333 15222563522 0006771 0 ustar 00 �PNG IHDR A<� BPLTE������ � ������������rr�]]�KK�����;;�11�))������ � �����&