first commit

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

View File

@@ -0,0 +1,574 @@
<?php
/**
* The Polylang public API.
*
* @package Polylang
*/
/**
* Template tag: displays the language switcher.
* The function does nothing if used outside the frontend.
*
* @api
* @since 0.5
*
* @param array $args {
* Optional array of arguments.
*
* @type int $dropdown The list is displayed as dropdown if set to 1, defaults to 0.
* @type int $echo Echoes the list if set to 1, defaults to 1.
* @type int $hide_if_empty Hides languages with no posts ( or pages ) if set to 1, defaults to 1.
* @type int $show_flags Displays flags if set to 1, defaults to 0.
* @type int $show_names Shows language names if set to 1, defaults to 1.
* @type string $display_names_as Whether to display the language name or its slug, valid options are 'slug' and 'name', defaults to name.
* @type int $force_home Will always link to the homepage in the translated language if set to 1, defaults to 0.
* @type int $hide_if_no_translation Hides the link if there is no translation if set to 1, defaults to 0.
* @type int $hide_current Hides the current language if set to 1, defaults to 0.
* @type int $post_id Returns links to the translations of the post defined by post_id if set, defaults to not set.
* @type int $raw Return a raw array instead of html markup if set to 1, defaults to 0.
* @type string $item_spacing Whether to preserve or discard whitespace between list items, valid options are 'preserve' and 'discard', defaults to 'preserve'.
* }
* @return string|array Either the html markup of the switcher or the raw elements to build a custom language switcher.
*/
function pll_the_languages( $args = array() ) {
if ( empty( PLL()->links ) ) {
return empty( $args['raw'] ) ? '' : array();
}
$switcher = new PLL_Switcher();
return $switcher->the_languages( PLL()->links, $args );
}
/**
* Returns the current language on frontend.
* Returns the language set in admin language filter on backend (false if set to all languages).
*
* @api
* @since 0.8.1
* @since 3.4 Accepts composite values.
*
* @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`.
* Pass `\OBJECT` constant to get the language object. A composite value can be used for language
* term property values, in the form of `{language_taxonomy_name}:{property_name}` (see
* {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`.
* @return string|int|bool|string[]|PLL_Language The requested field or object for the current language, `false` if the field isn't set or if current language doesn't exist yet.
*
* @phpstan-return (
* $field is \OBJECT ? PLL_Language : (
* $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string>
* )
* )|false
*/
function pll_current_language( $field = 'slug' ) {
if ( empty( PLL()->curlang ) ) {
return false;
}
if ( \OBJECT === $field ) {
return PLL()->curlang;
}
return PLL()->curlang->get_prop( $field );
}
/**
* Returns the default language.
*
* @api
* @since 1.0
* @since 3.4 Accepts composite values.
*
* @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`.
* Pass `\OBJECT` constant to get the language object. A composite value can be used for language
* term property values, in the form of `{language_taxonomy_name}:{property_name}` (see
* {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`.
* @return string|int|bool|string[]|PLL_Language The requested field or object for the default language, `false` if the field isn't set or if default language doesn't exist yet.
*
* @phpstan-return (
* $field is \OBJECT ? PLL_Language : (
* $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string>
* )
* )|false
*/
function pll_default_language( $field = 'slug' ) {
$lang = PLL()->model->get_default_language();
if ( empty( $lang ) ) {
return false;
}
if ( \OBJECT === $field ) {
return $lang;
}
return $lang->get_prop( $field );
}
/**
* Among the post and its translations, returns the ID of the post which is in the language represented by $lang.
*
* @api
* @since 0.5
* @since 3.4 Returns 0 instead of false.
* @since 3.4 $lang accepts PLL_Language or string.
*
* @param int $post_id Post ID.
* @param PLL_Language|string $lang Optional language (object or slug), defaults to the current language.
* @return int|false The translation post ID if exists, otherwise the passed ID. False if the passed object has no language or if the language doesn't exist.
*
* @phpstan-return int<0, max>|false
*/
function pll_get_post( $post_id, $lang = '' ) {
$lang = $lang ? $lang : pll_current_language();
if ( empty( $lang ) ) {
return false;
}
return PLL()->model->post->get( $post_id, $lang );
}
/**
* Among the term and its translations, returns the ID of the term which is in the language represented by $lang.
*
* @api
* @since 0.5
* @since 3.4 Returns 0 instead of false.
* @since 3.4 $lang accepts PLL_Language or string.
*
* @param int $term_id Term ID.
* @param PLL_Language|string $lang Optional language (object or slug), defaults to the current language.
* @return int|false The translation term ID if exists, otherwise the passed ID. False if the passed object has no language or if the language doesn't exist.
*
* @phpstan-return int<0, max>|false
*/
function pll_get_term( $term_id, $lang = null ) {
$lang = $lang ? $lang : pll_current_language();
if ( empty( $lang ) ) {
return false;
}
return PLL()->model->term->get( $term_id, $lang );
}
/**
* Returns the home url in a language.
*
* @api
* @since 0.8
*
* @param string $lang Optional language code, defaults to the current language.
* @return string
*/
function pll_home_url( $lang = '' ) {
if ( empty( $lang ) ) {
$lang = pll_current_language();
}
if ( empty( $lang ) || empty( PLL()->links ) ) {
return home_url( '/' );
}
return PLL()->links->get_home_url( $lang );
}
/**
* Registers a string for translation in the "strings translation" panel.
*
* @api
* @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, true if the string table should display a multiline textarea,
* false if should display a single line input, defaults to false.
* @return void
*/
function pll_register_string( $name, $string, $context = 'Polylang', $multiline = false ) {
if ( PLL() instanceof PLL_Admin_Base ) {
PLL_Admin_Strings::register_string( $name, $string, $context, $multiline );
}
}
/**
* Translates a string ( previously registered with pll_register_string ).
*
* @api
* @since 0.6
*
* @param string $string The string to translate.
* @return string The string translated in the current language.
*/
function pll__( $string ) {
if ( ! is_scalar( $string ) || '' === $string ) {
return $string;
}
return __( $string, 'pll_string' ); // PHPCS:ignore WordPress.WP.I18n
}
/**
* Translates a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML output.
*
* @api
* @since 2.1
*
* @param string $string The string to translate.
* @return string The string translated in the current language.
*/
function pll_esc_html__( $string ) {
return esc_html( pll__( $string ) );
}
/**
* Translates a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML attributes.
*
* @api
* @since 2.1
*
* @param string $string The string to translate.
* @return string The string translated in the current language.
*/
function pll_esc_attr__( $string ) {
return esc_attr( pll__( $string ) );
}
/**
* Echoes a translated string ( previously registered with pll_register_string )
* It is an equivalent of _e() and is not escaped.
*
* @api
* @since 0.6
*
* @param string $string The string to translate.
* @return void
*/
function pll_e( $string ) {
echo pll__( $string ); // phpcs:ignore
}
/**
* Echoes a translated string ( previously registered with pll_register_string ) and escapes it for safe use in HTML output.
*
* @api
* @since 2.1
*
* @param string $string The string to translate.
* @return void
*/
function pll_esc_html_e( $string ) {
echo pll_esc_html__( $string ); // phpcs:ignore WordPress.Security.EscapeOutput
}
/**
* Echoes a translated a string ( previously registered with pll_register_string ) and escapes it for safe use in HTML attributes.
*
* @api
* @since 2.1
*
* @param string $string The string to translate.
* @return void
*/
function pll_esc_attr_e( $string ) {
echo pll_esc_attr__( $string ); // phpcs:ignore WordPress.Security.EscapeOutput
}
/**
* Translates a string ( previously registered with pll_register_string ).
*
* @api
* @since 1.5.4
*
* @param string $string The string to translate.
* @param string $lang Language code.
* @return string The string translated in the requested language.
*/
function pll_translate_string( $string, $lang ) {
if ( PLL() instanceof PLL_Frontend && pll_current_language() === $lang ) {
return pll__( $string );
}
if ( ! is_scalar( $string ) || '' === $string ) {
return $string;
}
$lang = PLL()->model->get_language( $lang );
if ( empty( $lang ) ) {
return $string;
}
static $cache; // Cache object to avoid loading the same translations object several times.
if ( empty( $cache ) ) {
$cache = new PLL_Cache();
}
$mo = $cache->get( $lang->slug );
if ( ! $mo instanceof PLL_MO ) {
$mo = new PLL_MO();
$mo->import_from_db( $lang );
$cache->set( $lang->slug, $mo );
}
return $mo->translate( $string );
}
/**
* Returns true if Polylang manages languages and translations for this post type.
*
* @api
* @since 1.0.1
*
* @param string $post_type Post type name.
* @return bool
*/
function pll_is_translated_post_type( $post_type ) {
return PLL()->model->is_translated_post_type( $post_type );
}
/**
* Returns true if Polylang manages languages and translations for this taxonomy.
*
* @api
* @since 1.0.1
*
* @param string $tax Taxonomy name.
* @return bool
*/
function pll_is_translated_taxonomy( $tax ) {
return PLL()->model->is_translated_taxonomy( $tax );
}
/**
* Returns the list of available languages.
*
* @api
* @since 1.5
*
* @param array $args {
* Optional array of arguments.
*
* @type bool $hide_empty Hides languages with no posts if set to true ( defaults to false ).
* @type string $fields Return only that field if set ( @see PLL_Language for a list of fields ), defaults to 'slug'.
* }
* @return string[]
*/
function pll_languages_list( $args = array() ) {
$args = wp_parse_args( $args, array( 'fields' => 'slug' ) );
return PLL()->model->get_languages_list( $args );
}
/**
* Sets the post language.
*
* @api
* @since 1.5
* @since 3.4 $lang accepts PLL_Language or string.
* @since 3.4 Returns a boolean.
*
* @param int $id Post ID.
* @param PLL_Language|string $lang Language (object or slug).
* @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
* the post).
*/
function pll_set_post_language( $id, $lang ) {
return PLL()->model->post->set_language( $id, $lang );
}
/**
* Sets the term language.
*
* @api
* @since 1.5
* @since 3.4 $lang accepts PLL_Language or string.
* @since 3.4 Returns a boolean.
*
* @param int $id Term ID.
* @param PLL_Language|string $lang Language (object or slug).
* @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
* the term).
*/
function pll_set_term_language( $id, $lang ) {
return PLL()->model->term->set_language( $id, $lang );
}
/**
* Save posts translations.
*
* @api
* @since 1.5
* @since 3.4 Returns an associative array of translations.
*
* @param int[] $arr An associative array of translations with language code as key and post ID as value.
* @return int[] An associative array with language codes as key and post IDs as values.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
function pll_save_post_translations( $arr ) {
$id = reset( $arr );
if ( $id ) {
return PLL()->model->post->save_translations( $id, $arr );
}
return array();
}
/**
* Save terms translations
*
* @api
* @since 1.5
* @since 3.4 Returns an associative array of translations.
*
* @param int[] $arr An associative array of translations with language code as key and term ID as value.
* @return int[] An associative array with language codes as key and term IDs as values.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
function pll_save_term_translations( $arr ) {
$id = reset( $arr );
if ( $id ) {
return PLL()->model->term->save_translations( $id, $arr );
}
return array();
}
/**
* Returns the post language.
*
* @api
* @since 1.5.4
* @since 3.4 Accepts composite values for `$field`.
*
* @param int $post_id Post ID.
* @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`.
* Pass `\OBJECT` constant to get the language object. A composite value can be used for language
* term property values, in the form of `{language_taxonomy_name}:{property_name}` (see
* {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`.
* @return string|int|bool|string[]|PLL_Language The requested field or object for the post language, `false` if no language is associated to that post.
*
* @phpstan-return (
* $field is \OBJECT ? PLL_Language : (
* $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string>
* )
* )|false
*/
function pll_get_post_language( $post_id, $field = 'slug' ) {
$lang = PLL()->model->post->get_language( $post_id );
if ( empty( $lang ) || \OBJECT === $field ) {
return $lang;
}
return $lang->get_prop( $field );
}
/**
* Returns the term language.
*
* @api
* @since 1.5.4
* @since 3.4 Accepts composite values for `$field`.
*
* @param int $term_id Term ID.
* @param string $field Optional, the language field to return (@see PLL_Language), defaults to `'slug'`.
* Pass `\OBJECT` constant to get the language object. A composite value can be used for language
* term property values, in the form of `{language_taxonomy_name}:{property_name}` (see
* {@see PLL_Language::get_tax_prop()} for the possible values). Ex: `term_language:term_taxonomy_id`.
* @return string|int|bool|string[]|PLL_Language The requested field or object for the post language, `false` if no language is associated to that term.
*
* @phpstan-return (
* $field is \OBJECT ? PLL_Language : (
* $field is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string>
* )
* )|false
*/
function pll_get_term_language( $term_id, $field = 'slug' ) {
$lang = PLL()->model->term->get_language( $term_id );
if ( empty( $lang ) || \OBJECT === $field ) {
return $lang;
}
return $lang->get_prop( $field );
}
/**
* Returns an array of translations of a post.
*
* @api
* @since 1.8
*
* @param int $post_id Post ID.
* @return int[] An associative array of translations with language code as key and translation post ID as value.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
function pll_get_post_translations( $post_id ) {
return PLL()->model->post->get_translations( $post_id );
}
/**
* Returns an array of translations of a term.
*
* @api
* @since 1.8
*
* @param int $term_id Term ID.
* @return int[] An associative array of translations with language code as key and translation term ID as value.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
function pll_get_term_translations( $term_id ) {
return PLL()->model->term->get_translations( $term_id );
}
/**
* Counts posts in a language.
*
* @api
* @since 1.5
*
* @param string $lang Language code.
* @param array $args {
* Optional array of arguments.
*
* @type string $post_type Post type.
* @type int $m YearMonth ( ex: 201307 ).
* @type int $year 4 digit year.
* @type int $monthnum Month number (from 1 to 12).
* @type int $day Day of the month (from 1 to 31).
* @type int $author Author id.
* @type string $author_name Author nicename.
* @type string $post_format Post format.
* @type string $post_status Post status.
* }
* @return int Posts count.
*/
function pll_count_posts( $lang, $args = array() ) {
$lang = PLL()->model->get_language( $lang );
if ( empty( $lang ) ) {
return 0;
}
return PLL()->model->count_posts( $lang, $args );
}
/**
* Allows to access the Polylang instance.
* However, it is always preferable to use API functions
* as internal methods may be changed without prior notice.
*
* @since 1.8
*
* @return PLL_Frontend|PLL_Admin|PLL_Settings|PLL_REST_Request
*/
function PLL() { // PHPCS:ignore WordPress.NamingConventions.ValidFunctionName
return $GLOBALS['polylang'];
}

View File

@@ -0,0 +1,219 @@
<?php
/**
* @package Polylang
*/
/**
* Base class for both admin and frontend
*
* @since 1.2
*/
#[AllowDynamicProperties]
abstract class PLL_Base {
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
public $links_model;
/**
* Registers hooks on insert / update post related actions and filters.
*
* @var PLL_CRUD_Posts|null
*/
public $posts;
/**
* Registers hooks on insert / update term related action and filters.
*
* @var PLL_CRUD_Terms|null
*/
public $terms;
/**
* Constructor.
*
* @since 1.2
*
* @param PLL_Links_Model $links_model Links Model.
*/
public function __construct( &$links_model ) {
$this->links_model = &$links_model;
$this->model = &$links_model->model;
$this->options = &$this->model->options;
$GLOBALS['l10n_unloaded']['pll_string'] = true; // Short-circuit _load_textdomain_just_in_time() for 'pll_string' domain in WP 4.6+
add_action( 'widgets_init', array( $this, 'widgets_init' ) );
// User defined strings translations
add_action( 'pll_language_defined', array( $this, 'load_strings_translations' ), 5 );
add_action( 'change_locale', array( $this, 'load_strings_translations' ) ); // Since WP 4.7
add_action( 'personal_options_update', array( $this, 'load_strings_translations' ), 1, 0 ); // Before WP, for confirmation request when changing the user email.
add_action( 'lostpassword_post', array( $this, 'load_strings_translations' ), 10, 0 ); // Password reset email.
// Switch_to_blog
add_action( 'switch_blog', array( $this, 'switch_blog' ), 10, 2 );
}
/**
* Instantiates classes reacting to CRUD operations on posts and terms,
* only when at least one language is defined.
*
* @since 2.6
*
* @return void
*/
public function init() {
if ( $this->model->has_languages() ) {
$this->posts = new PLL_CRUD_Posts( $this );
$this->terms = new PLL_CRUD_Terms( $this );
// WordPress options.
new PLL_Translate_Option( 'blogname', array(), array( 'context' => 'WordPress' ) );
new PLL_Translate_Option( 'blogdescription', array(), array( 'context' => 'WordPress' ) );
new PLL_Translate_Option( 'date_format', array(), array( 'context' => 'WordPress' ) );
new PLL_Translate_Option( 'time_format', array(), array( 'context' => 'WordPress' ) );
}
}
/**
* Registers our widgets
*
* @since 0.1
*
* @return void
*/
public function widgets_init() {
register_widget( 'PLL_Widget_Languages' );
// Overwrites the calendar widget to filter posts by language
if ( ! defined( 'PLL_WIDGET_CALENDAR' ) || PLL_WIDGET_CALENDAR ) {
unregister_widget( 'WP_Widget_Calendar' );
register_widget( 'PLL_Widget_Calendar' );
}
}
/**
* Loads user defined strings translations
*
* @since 1.2
* @since 2.1.3 $locale parameter added.
*
* @param string $locale Language locale or slug. Defaults to current locale.
* @return void
*/
public function load_strings_translations( $locale = '' ) {
if ( empty( $locale ) ) {
$locale = ( is_admin() && ! Polylang::is_ajax_on_front() ) ? get_user_locale() : get_locale();
}
$language = $this->model->get_language( $locale );
if ( ! empty( $language ) ) {
$mo = new PLL_MO();
$mo->import_from_db( $language );
$GLOBALS['l10n']['pll_string'] = &$mo;
} else {
unset( $GLOBALS['l10n']['pll_string'] );
}
}
/**
* Resets some variables when the blog is switched.
* Applied only if Polylang is active on the new blog.
*
* @since 1.5.1
*
* @param int $new_blog_id New blog ID.
* @param int $prev_blog_id Previous blog ID.
* @return void
*/
public function switch_blog( $new_blog_id, $prev_blog_id ) {
if ( (int) $new_blog_id === (int) $prev_blog_id ) {
// Do nothing if same blog.
return;
}
$this->links_model->remove_filters();
if ( $this->is_active_on_current_site() ) {
$this->options = get_option( 'polylang' ); // Needed for menus.
$this->links_model = $this->model->get_links_model();
}
}
/**
* Checks if Polylang is active on the current blog (useful when the blog is switched).
*
* @since 3.5.2
*
* @return bool
*/
protected function is_active_on_current_site(): bool {
return pll_is_plugin_active( POLYLANG_BASENAME ) && get_option( 'polylang' );
}
/**
* Check if the customize menu should be removed or not.
*
* @since 3.2
*
* @return bool True if it should be removed, false otherwise.
*/
public function should_customize_menu_be_removed() {
// Exit if a block theme isn't activated.
if ( ! function_exists( 'wp_is_block_theme' ) || ! wp_is_block_theme() ) {
return false;
}
return ! $this->is_customize_register_hooked();
}
/**
* Tells whether or not Polylang or third party callbacks are hooked to `customize_register`.
*
* @since 3.4.3
*
* @global $wp_filter
*
* @return bool True if Polylang's callbacks are hooked, false otherwise.
*/
protected function is_customize_register_hooked() {
global $wp_filter;
if ( empty( $wp_filter['customize_register'] ) || ! $wp_filter['customize_register'] instanceof WP_Hook ) {
return false;
}
/*
* 'customize_register' is hooked by:
* @see PLL_Nav_Menu::create_nav_menu_locations()
* @see PLL_Frontend_Static_Pages::filter_customizer()
*/
$floor = 0;
if ( ! empty( $this->nav_menu ) && (bool) $wp_filter['customize_register']->has_filter( 'customize_register', array( $this->nav_menu, 'create_nav_menu_locations' ) ) ) {
++$floor;
}
if ( ! empty( $this->static_pages ) && (bool) $wp_filter['customize_register']->has_filter( 'customize_register', array( $this->static_pages, 'filter_customizer' ) ) ) {
++$floor;
}
$count = array_sum( array_map( 'count', $wp_filter['customize_register']->callbacks ) );
return $count > $floor;
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* @package Polylang
*/
/**
* An extremely simple non persistent cache system.
*
* @since 1.7
*
* @template TCacheData
*/
class PLL_Cache {
/**
* Current site id.
*
* @var int
*/
protected $blog_id;
/**
* The cache container.
*
* @var array
*
* @phpstan-var array<int, array<non-empty-string, TCacheData>>
*/
protected $cache = array();
/**
* Constructor.
*
* @since 1.7
*/
public function __construct() {
$this->blog_id = get_current_blog_id();
add_action( 'switch_blog', array( $this, 'switch_blog' ) );
}
/**
* Called when switching blog.
*
* @since 1.7
*
* @param int $new_blog_id New blog ID.
* @return void
*/
public function switch_blog( $new_blog_id ) {
$this->blog_id = $new_blog_id;
}
/**
* Adds a value in cache.
*
* @since 1.7
* @since 3.6 Returns the cached value.
*
* @param string $key Cache key.
* @param mixed $data The value to add to the cache.
* @return mixed
*
* @phpstan-param non-empty-string $key
* @phpstan-param TCacheData $data
* @phpstan-return TCacheData
*/
public function set( $key, $data ) {
$this->cache[ $this->blog_id ][ $key ] = $data;
return $data;
}
/**
* Returns value from cache.
*
* @since 1.7
*
* @param string $key Cache key.
* @return mixed
*
* @phpstan-param non-empty-string $key
* @phpstan-return TCacheData|false
*/
public function get( $key ) {
return isset( $this->cache[ $this->blog_id ][ $key ] ) ? $this->cache[ $this->blog_id ][ $key ] : false;
}
/**
* Cleans the cache (for this blog only).
*
* @since 1.7
*
* @param string $key Optional. Cache key. An empty string to clean the whole cache for the current blog.
* Default is an empty string.
* @return void
*/
public function clean( $key = '' ) {
if ( '' === $key ) {
unset( $this->cache[ $this->blog_id ] );
} else {
unset( $this->cache[ $this->blog_id ][ $key ] );
}
}
/**
* Generates and returns a "unique" cache key, depending on `$data` and prefixed by `$prefix`.
*
* @since 3.6
*
* @param string $prefix String to prefix the cache key.
* @param string|array|object $data Data.
* @return string
*
* @phpstan-param non-empty-string $prefix
* @phpstan-return non-empty-string
*/
public function get_unique_key( string $prefix, $data ): string {
/** @var scalar */
$serialized = maybe_serialize( $data );
return $prefix . md5( (string) $serialized );
}
}

View File

@@ -0,0 +1,298 @@
<?php
/**
* @package Polylang
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Don't access directly
}
// Default directory to store user data such as custom flags
if ( ! defined( 'PLL_LOCAL_DIR' ) ) {
define( 'PLL_LOCAL_DIR', WP_CONTENT_DIR . '/polylang' );
}
// Includes local config file if exists
if ( is_readable( PLL_LOCAL_DIR . '/pll-config.php' ) ) {
include_once PLL_LOCAL_DIR . '/pll-config.php';
}
/**
* Controls the plugin, as well as activation, and deactivation
*
* @since 0.1
*
* @template TPLLClass of PLL_Base
*/
class Polylang {
/**
* Constructor
*
* @since 0.1
*/
public function __construct() {
require_once __DIR__ . '/functions.php'; // VIP functions
// register an action when plugin is activating.
register_activation_hook( POLYLANG_BASENAME, array( 'PLL_Wizard', 'start_wizard' ) );
$install = new PLL_Install( POLYLANG_BASENAME );
// Stopping here if we are going to deactivate the plugin ( avoids breaking rewrite rules )
if ( $install->is_deactivation() || ! $install->can_activate() ) {
return;
}
// Plugin initialization
// Take no action before all plugins are loaded
add_action( 'plugins_loaded', array( $this, 'init' ), 1 );
// Override load text domain waiting for the language to be defined
// Here for plugins which load text domain as soon as loaded :(
if ( ! defined( 'PLL_OLT' ) || PLL_OLT ) {
PLL_OLT_Manager::instance();
}
/*
* Loads the compatibility with some plugins and themes.
* Loaded as soon as possible as we may need to act before other plugins are loaded.
*/
if ( ! defined( 'PLL_PLUGINS_COMPAT' ) || PLL_PLUGINS_COMPAT ) {
PLL_Integrations::instance();
}
}
/**
* Tells whether the current request is an ajax request on frontend or not
*
* @since 2.2
*
* @return bool
*/
public static function is_ajax_on_front() {
// Special test for plupload which does not use jquery ajax and thus does not pass our ajax prefilter
// Special test for customize_save done in frontend but for which we want to load the admin
$in = isset( $_REQUEST['action'] ) && in_array( sanitize_key( $_REQUEST['action'] ), array( 'upload-attachment', 'customize_save' ) ); // phpcs:ignore WordPress.Security.NonceVerification
$is_ajax_on_front = wp_doing_ajax() && empty( $_REQUEST['pll_ajax_backend'] ) && ! $in; // phpcs:ignore WordPress.Security.NonceVerification
/**
* Filters whether the current request is an ajax request on front.
*
* @since 2.3
*
* @param bool $is_ajax_on_front Whether the current request is an ajax request on front.
*/
return apply_filters( 'pll_is_ajax_on_front', $is_ajax_on_front );
}
/**
* Is the current request a REST API request?
* Inspired by WP::parse_request()
* Needed because at this point, the constant REST_REQUEST is not defined yet
*
* @since 2.4.1
*
* @return bool
*/
public static function is_rest_request() {
// Handle pretty permalinks.
$home_path = trim( (string) wp_parse_url( home_url(), PHP_URL_PATH ), '/' );
$home_path_regex = sprintf( '|^%s|i', preg_quote( $home_path, '|' ) );
$req_uri = trim( (string) wp_parse_url( pll_get_requested_url(), PHP_URL_PATH ), '/' );
$req_uri = (string) preg_replace( $home_path_regex, '', $req_uri );
$req_uri = trim( $req_uri, '/' );
$req_uri = str_replace( 'index.php', '', $req_uri );
$req_uri = trim( $req_uri, '/' );
// And also test rest_route query string parameter is not empty for plain permalinks.
$query_string = array();
wp_parse_str( (string) wp_parse_url( pll_get_requested_url(), PHP_URL_QUERY ), $query_string );
$rest_route = isset( $query_string['rest_route'] ) ? trim( $query_string['rest_route'], '/' ) : false;
return 0 === strpos( $req_uri, rest_get_url_prefix() . '/' ) || ! empty( $rest_route );
}
/**
* Tells if we are in the wizard process.
*
* @since 2.7
*
* @return bool
*/
public static function is_wizard() {
return isset( $_GET['page'] ) && ! empty( $_GET['page'] ) && 'mlang_wizard' === sanitize_key( $_GET['page'] ); // phpcs:ignore WordPress.Security.NonceVerification
}
/**
* Defines constants
* May be overridden by a plugin if set before plugins_loaded, 1
*
* @since 1.6
*
* @return void
*/
public static function define_constants() {
// Cookie name. no cookie will be used if set to false
if ( ! defined( 'PLL_COOKIE' ) ) {
define( 'PLL_COOKIE', 'pll_language' );
}
// Backward compatibility with Polylang < 2.3
if ( ! defined( 'PLL_AJAX_ON_FRONT' ) ) {
define( 'PLL_AJAX_ON_FRONT', self::is_ajax_on_front() );
}
// Admin
if ( ! defined( 'PLL_ADMIN' ) ) {
define( 'PLL_ADMIN', wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) || ( is_admin() && ! PLL_AJAX_ON_FRONT ) );
}
// Settings page whatever the tab except for the wizard which needs to be an admin process.
if ( ! defined( 'PLL_SETTINGS' ) ) {
define( 'PLL_SETTINGS', is_admin() && ( ( isset( $_GET['page'] ) && 0 === strpos( sanitize_key( $_GET['page'] ), 'mlang' ) && ! self::is_wizard() ) || ! empty( $_REQUEST['pll_ajax_settings'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
}
/**
* Polylang initialization
* setups models and separate admin and frontend
*
* @since 1.2
*
* @return void
*/
public function init() {
self::define_constants();
$options = get_option( 'polylang' );
// Plugin upgrade
if ( $options && version_compare( $options['version'], POLYLANG_VERSION, '<' ) ) {
$upgrade = new PLL_Upgrade( $options );
if ( ! $upgrade->upgrade() ) { // If the version is too old
return;
}
}
// In some edge cases, it's possible that no options were found in the database. Load default options as we need some.
if ( ! $options ) {
$options = PLL_Install::get_default_options();
}
/**
* Filter the model class to use
* /!\ this filter is fired *before* the $polylang object is available
*
* @since 1.5
*
* @param string $class either PLL_Model or PLL_Admin_Model
*/
$class = apply_filters( 'pll_model', PLL_SETTINGS || self::is_wizard() ? 'PLL_Admin_Model' : 'PLL_Model' );
/** @var PLL_Model $model */
$model = new $class( $options );
if ( ! $model->has_languages() ) {
/**
* Fires when no language has been defined yet
* Used to load overridden textdomains
*
* @since 1.2
*/
do_action( 'pll_no_language_defined' );
}
$class = '';
if ( PLL_SETTINGS ) {
$class = 'PLL_Settings';
} elseif ( PLL_ADMIN ) {
$class = 'PLL_Admin';
} elseif ( self::is_rest_request() ) {
$class = 'PLL_REST_Request';
} elseif ( $model->has_languages() ) {
$class = 'PLL_Frontend';
}
/**
* Filters the class to use to instantiate the $polylang object
*
* @since 2.6
*
* @param string $class A class name.
*/
$class = apply_filters( 'pll_context', $class );
if ( ! empty( $class ) ) {
/** @phpstan-var class-string<TPLLClass> $class */
$this->init_context( $class, $model );
}
}
/**
* Polylang initialization.
* Setups the Polylang Context, loads the modules and init Polylang.
*
* @since 3.6
*
* @param string $class The class name.
* @param PLL_Model $model Instance of PLL_Model.
* @return PLL_Base
*
* @phpstan-param class-string<TPLLClass> $class
* @phpstan-return TPLLClass
*/
public function init_context( string $class, PLL_Model $model ): PLL_Base {
global $polylang;
$links_model = $model->get_links_model();
$polylang = new $class( $links_model );
/**
* Fires after Polylang's model init.
* This is the best place to register a custom table (see `PLL_Model`'s constructor).
* /!\ This hook is fired *before* the $polylang object is available.
* /!\ The languages are also not available yet.
*
* @since 3.4
*
* @param PLL_Model $model Polylang model.
*/
do_action( 'pll_model_init', $model );
$model->maybe_create_language_terms();
/**
* Fires after the $polylang object is created and before the API is loaded
*
* @since 2.0
*
* @param object $polylang
*/
do_action_ref_array( 'pll_pre_init', array( &$polylang ) );
// Loads the API
require_once POLYLANG_DIR . '/include/api.php';
// Loads the modules.
$load_scripts = glob( POLYLANG_DIR . '/modules/*/load.php', GLOB_NOSORT );
if ( is_array( $load_scripts ) ) {
foreach ( $load_scripts as $load_script ) {
require_once $load_script; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable
}
}
$polylang->init();
/**
* Fires after the $polylang object and the API is loaded
*
* @since 1.7
*
* @param object $polylang
*/
do_action_ref_array( 'pll_init', array( &$polylang ) );
return $polylang;
}
}

View File

@@ -0,0 +1,107 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manage manage the language cookie
*
* @since 2.9
*/
class PLL_Cookie {
/**
* Parses the cookie parameters.
*
* @since 2.9
*
* @param array $args {@see PLL_Cookie::set()}
* @return array
*/
protected static function parse_args( $args ) {
/**
* Filters the Polylang cookie duration.
*
* If a cookie duration of 0 is specified, a session cookie will be set.
* If a negative cookie duration is specified, the cookie is removed.
* /!\ This filter may be fired *before* the theme is loaded.
*
* @since 1.8
*
* @param int $duration Cookie duration in seconds.
*/
$expiration = (int) apply_filters( 'pll_cookie_expiration', YEAR_IN_SECONDS );
$defaults = array(
'expires' => 0 !== $expiration ? time() + $expiration : 0,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN, // Cookie domain must be set to false for localhost (default value for `COOKIE_DOMAIN`) thanks to Stephen Harris.
'secure' => is_ssl(),
'httponly' => false,
'samesite' => 'Lax',
);
$args = wp_parse_args( $args, $defaults );
/**
* Filters the Polylang cookie arguments.
* /!\ This filter may be fired *before* the theme is loaded.
*
* @since 3.6
*
* @param array $args {
* Optional. Array of arguments for setting the cookie.
*
* @type int $expires Cookie duration.
* If a cookie duration of 0 is specified, a session cookie will be set.
* If a negative cookie duration is specified, the cookie is removed.
* @type string $path Cookie path.
* @type string $domain Cookie domain. Must be set to false for localhost (default value for `COOKIE_DOMAIN`).
* @type bool $secure Should the cookie be sent only over https?
* @type bool $httponly Should the cookie be accessed only over http protocol?.
* @type string $samesite Either 'Strict', 'Lax' or 'None'.
* }
*/
return (array) apply_filters( 'pll_cookie_args', $args );
}
/**
* Sets the cookie.
*
* @since 2.9
*
* @param string $lang Language cookie value.
* @param array $args {
* Optional. Array of arguments for setting the cookie.
*
* @type string $path Cookie path, defaults to COOKIEPATH.
* @type string $domain Cookie domain, defaults to COOKIE_DOMAIN
* @type bool $secure Should the cookie be sent only over https?
* @type bool $httponly Should the cookie accessed only over http protocol? Defaults to false.
* @type string $samesite Either 'Strict', 'Lax' or 'None', defaults to 'Lax'.
* }
* @return void
*/
public static function set( $lang, $args = array() ) {
$args = self::parse_args( $args );
if ( ! headers_sent() && PLL_COOKIE !== false && self::get() !== $lang ) {
if ( version_compare( PHP_VERSION, '7.3', '<' ) ) {
$args['path'] .= '; SameSite=' . $args['samesite']; // Hack to set SameSite value in PHP < 7.3. Doesn't work with newer versions.
setcookie( PLL_COOKIE, $lang, $args['expires'], $args['path'], $args['domain'], $args['secure'], $args['httponly'] );
} else {
setcookie( PLL_COOKIE, $lang, $args );
}
}
}
/**
* Returns the language cookie value.
*
* @since 2.9
*
* @return string
*/
public static function get() {
return isset( $_COOKIE[ PLL_COOKIE ] ) ? sanitize_key( $_COOKIE[ PLL_COOKIE ] ) : '';
}
}

View File

@@ -0,0 +1,484 @@
<?php
/**
* @package Polylang
*/
/**
* Adds actions and filters related to languages when creating, updating or deleting posts.
* Actions and filters triggered when reading posts are handled separately.
*
* @since 2.4
*/
class PLL_CRUD_Posts {
/**
* @var PLL_Model
*/
protected $model;
/**
* Preferred language to assign to a new post.
*
* @var PLL_Language|null
*/
protected $pref_lang;
/**
* Current language.
*
* @var PLL_Language|null
*/
protected $curlang;
/**
* Reference to the Polylang options array.
*
* @var array
*/
protected $options;
/**
* Constructor
*
* @since 2.4
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->options = &$polylang->options;
$this->model = &$polylang->model;
$this->pref_lang = &$polylang->pref_lang;
$this->curlang = &$polylang->curlang;
add_action( 'save_post', array( $this, 'save_post' ), 10, 2 );
add_action( 'set_object_terms', array( $this, 'set_object_terms' ), 10, 4 );
add_filter( 'wp_insert_post_parent', array( $this, 'wp_insert_post_parent' ), 10, 2 );
add_action( 'before_delete_post', array( $this, 'delete_post' ) );
add_action( 'post_updated', array( $this, 'force_tags_translation' ), 10, 3 );
// Specific for media
if ( $polylang->options['media_support'] ) {
add_action( 'add_attachment', array( $this, 'set_default_language' ) );
add_action( 'delete_attachment', array( $this, 'delete_post' ) );
add_filter( 'wp_delete_file', array( $this, 'wp_delete_file' ) );
}
}
/**
* Allows to set a language by default for posts if it has no language yet.
*
* @since 1.5
*
* @param int $post_id Post ID.
* @return void
*/
public function set_default_language( $post_id ) {
if ( ! $this->model->post->get_language( $post_id ) ) {
if ( ! empty( $_GET['new_lang'] ) && $lang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// Defined only on admin.
$this->model->post->set_language( $post_id, $lang );
} elseif ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// Testing $this->pref_lang makes this test pass only on admin.
$this->model->post->set_language( $post_id, $lang );
} elseif ( ( $parent_id = wp_get_post_parent_id( $post_id ) ) && $parent_lang = $this->model->post->get_language( $parent_id ) ) {
$this->model->post->set_language( $post_id, $parent_lang );
} elseif ( isset( $this->pref_lang ) ) {
// Always defined on admin, never defined on frontend.
$this->model->post->set_language( $post_id, $this->pref_lang );
} elseif ( ! empty( $this->curlang ) ) {
// Only on frontend due to the previous test always true on admin.
$this->model->post->set_language( $post_id, $this->curlang );
} else {
// In all other cases set to default language.
$this->model->post->set_language( $post_id, $this->options['default_lang'] );
}
}
}
/**
* Called when a post ( or page ) is saved, published or updated.
*
* @since 0.1
* @since 2.3 Does not save the language and translations anymore, unless the post has no language yet.
*
* @param int $post_id Post id of the post being saved.
* @param WP_Post $post The post being saved.
* @return void
*/
public function save_post( $post_id, $post ) {
// Does nothing except on post types which are filterable.
if ( $this->model->is_translated_post_type( $post->post_type ) ) {
if ( $id = wp_is_post_revision( $post_id ) ) {
$post_id = $id;
}
$lang = $this->model->post->get_language( $post_id );
if ( empty( $lang ) ) {
$this->set_default_language( $post_id );
}
/**
* Fires after the post language and translations are saved.
*
* @since 1.2
*
* @param int $post_id Post id.
* @param WP_Post $post Post object.
* @param int[] $translations The list of translations post ids.
*/
do_action( 'pll_save_post', $post_id, $post, $this->model->post->get_translations( $post_id ) );
}
}
/**
* Makes sure that saved terms are in the right language.
*
* @since 2.3
*
* @param int $object_id Object ID.
* @param int[]|string[] $terms An array of object term IDs or slugs.
* @param int[] $tt_ids An array of term taxonomy IDs.
* @param string $taxonomy Taxonomy slug.
* @return void
*/
public function set_object_terms( $object_id, $terms, $tt_ids, $taxonomy ) {
static $avoid_recursion;
if ( $avoid_recursion || empty( $terms ) || ! is_array( $terms ) || ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return;
}
$lang = $this->model->post->get_language( $object_id );
if ( empty( $lang ) ) {
return;
}
// Use the term_taxonomy_ids to get all the requested terms in 1 query.
$new_terms = get_terms(
array(
'taxonomy' => $taxonomy,
'term_taxonomy_id' => array_map( 'intval', $tt_ids ),
'lang' => '',
)
);
if ( empty( $new_terms ) || ! is_array( $new_terms ) ) {
// Terms not found.
return;
}
$new_term_ids_translated = $this->translate_terms( $new_terms, $taxonomy, $lang );
// Query the object's term.
$orig_terms = get_terms(
array(
'taxonomy' => $taxonomy,
'object_ids' => $object_id,
'lang' => '',
)
);
if ( is_array( $orig_terms ) ) {
$orig_term_ids = wp_list_pluck( $orig_terms, 'term_id' );
$orig_term_ids_translated = $this->translate_terms( $orig_terms, $taxonomy, $lang );
// Terms that are not in the translated list.
$remove_term_ids = array_diff( $orig_term_ids, $orig_term_ids_translated );
if ( ! empty( $remove_term_ids ) ) {
wp_remove_object_terms( $object_id, $remove_term_ids, $taxonomy );
}
} else {
$orig_term_ids = array();
$orig_term_ids_translated = array();
}
// Terms to add.
$add_term_ids = array_unique( array_merge( $orig_term_ids_translated, $new_term_ids_translated ) );
$add_term_ids = array_diff( $add_term_ids, $orig_term_ids );
if ( ! empty( $add_term_ids ) ) {
$avoid_recursion = true;
wp_set_object_terms( $object_id, $add_term_ids, $taxonomy, true ); // Append.
$avoid_recursion = false;
}
}
/**
* Make sure that the post parent is in the correct language.
*
* @since 1.8
*
* @param int $post_parent Post parent ID.
* @param int $post_id Post ID.
* @return int
*/
public function wp_insert_post_parent( $post_parent, $post_id ) {
$lang = $this->model->post->get_language( $post_id );
$parent_post_type = $post_parent > 0 ? get_post_type( $post_parent ) : null;
// Dont break the hierarchy in case the post has no language
if ( ! empty( $lang ) && ! empty( $parent_post_type ) && $this->model->is_translated_post_type( $parent_post_type ) ) {
$post_parent = $this->model->post->get_translation( $post_parent, $lang );
}
return $post_parent;
}
/**
* Called when a post, page or media is deleted
* Don't delete translations if this is a post revision thanks to AndyDeGroo who caught this bug
* http://wordpress.org/support/topic/plugin-polylang-quick-edit-still-breaks-translation-linking-of-pages-in-072
*
* @since 0.1
*
* @param int $post_id Post ID.
* @return void
*/
public function delete_post( $post_id ) {
if ( ! wp_is_post_revision( $post_id ) ) {
$this->model->post->delete_translation( $post_id );
}
}
/**
* Prevents WP deleting files when there are still media using them.
*
* @since 0.9
*
* @param string $file Path to the file to delete.
* @return string Empty or unmodified path.
*/
public function wp_delete_file( $file ) {
global $wpdb;
$uploadpath = wp_upload_dir();
// Get the main attached file.
$attached_file = substr_replace( $file, '', 0, strlen( trailingslashit( $uploadpath['basedir'] ) ) );
$attached_file = preg_replace( '#-\d+x\d+\.([a-z]+)$#', '.$1', $attached_file );
$ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT post_id FROM $wpdb->postmeta
WHERE meta_key = '_wp_attached_file' AND meta_value = %s",
$attached_file
)
);
if ( ! empty( $ids ) ) {
return ''; // Prevent deleting the file.
}
return $file;
}
/**
* Creates a media translation
*
* @since 1.8
*
* @param int $post_id Original attachment id.
* @param string|object $lang New translation language.
* @return int Attachment id of the translated media.
*/
public function create_media_translation( $post_id, $lang ) {
if ( empty( $post_id ) ) {
return 0;
}
$post = get_post( $post_id, ARRAY_A );
if ( empty( $post ) ) {
return 0;
}
$lang = $this->model->get_language( $lang ); // Make sure we get a valid language slug.
if ( empty( $lang ) ) {
return 0;
}
// Create a new attachment ( translate attachment parent if exists ).
add_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Avoid a conflict with automatic duplicate at upload.
unset( $post['ID'] ); // Will force the creation.
if ( ! empty( $post['post_parent'] ) ) {
$post['post_parent'] = (int) $this->model->post->get_translation( $post['post_parent'], $lang->slug );
}
$post['tax_input'] = array( 'language' => array( $lang->slug ) ); // Assigns the language.
$tr_id = wp_insert_attachment( wp_slash( $post ) );
remove_filter( 'pll_enable_duplicate_media', '__return_false', 99 ); // Restore automatic duplicate at upload.
// Copy metadata.
$data = wp_get_attachment_metadata( $post_id, true ); // Unfiltered.
if ( is_array( $data ) ) {
wp_update_attachment_metadata( $tr_id, wp_slash( $data ) ); // Directly uses update_post_meta, so expects slashed.
}
// Copy attached file.
if ( $file = get_attached_file( $post_id, true ) ) { // Unfiltered.
update_attached_file( $tr_id, wp_slash( $file ) ); // Directly uses update_post_meta, so expects slashed.
}
// Copy alternative text. Direct use of the meta as there is no filtered wrapper to manipulate it.
if ( $text = get_post_meta( $post_id, '_wp_attachment_image_alt', true ) ) {
add_post_meta( $tr_id, '_wp_attachment_image_alt', wp_slash( $text ) );
}
$this->model->post->set_language( $tr_id, $lang );
$translations = $this->model->post->get_translations( $post_id );
$translations[ $lang->slug ] = $tr_id;
$this->model->post->save_translations( $tr_id, $translations );
/**
* Fires after a media translation is created
*
* @since 1.6.4
*
* @param int $post_id Post id of the source media.
* @param int $tr_id Post id of the new media translation.
* @param string $slug Language code of the new translation.
*/
do_action( 'pll_translate_media', $post_id, $tr_id, $lang->slug );
return $tr_id;
}
/**
* Ensure that tags are in the correct language when a post is updated, due to `tags_input` parameter being removed in `wp_update_post()`.
*
* @since 3.4.5
*
* @param int $post_id Post ID, unused.
* @param WP_Post $post_after Post object following the update.
* @param WP_Post $post_before Post object before the update.
* @return void
*/
public function force_tags_translation( $post_id, $post_after, $post_before ) {
if ( ! is_object_in_taxonomy( $post_before->post_type, 'post_tag' ) ) {
return;
}
$terms = get_the_terms( $post_before, 'post_tag' );
if ( empty( $terms ) || ! is_array( $terms ) ) {
return;
}
$term_ids = wp_list_pluck( $terms, 'term_id' );
// Let's ensure that `PLL_CRUD_Posts::set_object_terms()` will do its job.
wp_set_post_terms( $post_id, $term_ids, 'post_tag' );
}
/**
* Makes sure that all terms in the given list are in the given language.
* If not the case, the terms are translated or created (for a hierarchical taxonomy, terms are created recursively).
*
* @since 3.5
*
* @param WP_Term[] $terms List of terms to translate.
* @param string $taxonomy The terms' taxonomy.
* @param PLL_Language $language The language to translate the terms into.
* @return int[] List of `term_id`s.
*
* @phpstan-return array<positive-int>
*/
private function translate_terms( array $terms, string $taxonomy, PLL_Language $language ): array {
$term_ids_translated = array();
foreach ( $terms as $term ) {
$term_ids_translated[] = $this->translate_term( $term, $taxonomy, $language );
}
return array_filter( $term_ids_translated );
}
/**
* Translates the given term into the given language.
* If the translation doesn't exist, it is created (for a hierarchical taxonomy, terms are created recursively).
*
* @since 3.5
*
* @param WP_Term $term The term to translate.
* @param string $taxonomy The term's taxonomy.
* @param PLL_Language $language The language to translate the term into.
* @return int A `term_id` on success, `0` on failure.
*
* @phpstan-return int<0, max>
*/
private function translate_term( WP_Term $term, string $taxonomy, PLL_Language $language ): int {
// Check if the term is in the correct language or if a translation exists.
$tr_term_id = $this->model->term->get( $term->term_id, $language );
if ( ! empty( $tr_term_id ) ) {
// Already in the correct language.
return $tr_term_id;
}
// Or choose the correct language for tags (initially defined by name).
$tr_term_id = $this->model->term_exists( $term->name, $taxonomy, $term->parent, $language );
if ( ! empty( $tr_term_id ) ) {
return $tr_term_id;
}
// Or create the term in the correct language.
$tr_parent_term_id = 0;
if ( $term->parent > 0 && is_taxonomy_hierarchical( $taxonomy ) ) {
$parent = get_term( $term->parent, $taxonomy );
if ( $parent instanceof WP_Term ) {
// Translate the parent recursively.
$tr_parent_term_id = $this->translate_term( $parent, $taxonomy, $language );
}
}
$lang_callback = function ( $lang, $tax, $slug ) use ( $language, $term, $taxonomy ) {
if ( ! $lang instanceof PLL_Language && $tax === $taxonomy && $slug === $term->slug ) {
return $language;
}
return $lang;
};
$parent_callback = function ( $parent_id, $tax, $slug ) use ( $tr_parent_term_id, $term, $taxonomy ) {
if ( empty( $parent_id ) && $tax === $taxonomy && $slug === $term->slug ) {
return $tr_parent_term_id;
}
return $parent_id;
};
add_filter( 'pll_inserted_term_language', $lang_callback, 10, 3 );
add_filter( 'pll_inserted_term_parent', $parent_callback, 10, 3 );
$new_term_info = wp_insert_term(
$term->name,
$taxonomy,
array(
'parent' => $tr_parent_term_id,
'slug' => $term->slug, // Useless but prevents the use of `sanitize_title()` and for consistency with `$lang_callback`.
)
);
remove_filter( 'pll_inserted_term_language', $lang_callback );
remove_filter( 'pll_inserted_term_parent', $parent_callback );
if ( is_wp_error( $new_term_info ) ) {
// Term creation failed.
return 0;
}
$tr_term_id = max( 0, (int) $new_term_info['term_id'] );
if ( empty( $tr_term_id ) ) {
return 0;
}
$this->model->term->set_language( $tr_term_id, $language );
$trs = $this->model->term->get_translations( $term->term_id );
$trs[ $language->slug ] = $tr_term_id;
$this->model->term->save_translations( $term->term_id, $trs );
return $tr_term_id;
}
}

View File

@@ -0,0 +1,346 @@
<?php
/**
* @package Polylang
*/
/**
* Adds actions and filters related to languages when creating, reading, updating or deleting posts
* Acts both on frontend and backend
*
* @since 2.4
*/
class PLL_CRUD_Terms {
/**
* @var PLL_Model
*/
public $model;
/**
* Current language (used to filter the content).
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Language selected in the admin language filter.
*
* @var PLL_Language|null
*/
public $filter_lang;
/**
* Preferred language to assign to new contents.
*
* @var PLL_Language|null
*/
public $pref_lang;
/**
* Stores the 'lang' query var from WP_Query.
*
* @var string|null
*/
private $tax_query_lang;
/**
* Stores the term name before creating a slug if needed.
*
* @var string
*/
private $pre_term_name = '';
/**
* Reference to the Polylang options array.
*
* @var array
*/
protected $options;
/**
* Constructor
*
* @since 2.4
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->options = &$polylang->options;
$this->model = &$polylang->model;
$this->curlang = &$polylang->curlang;
$this->filter_lang = &$polylang->filter_lang;
$this->pref_lang = &$polylang->pref_lang;
// Saving terms
add_action( 'create_term', array( $this, 'save_term' ), 999, 3 );
add_action( 'edit_term', array( $this, 'save_term' ), 999, 3 ); // After PLL_Admin_Filters_Term
add_filter( 'pre_term_name', array( $this, 'set_pre_term_name' ) );
add_filter( 'pre_term_slug', array( $this, 'set_pre_term_slug' ), 10, 2 );
// Adds cache domain when querying terms
add_filter( 'get_terms_args', array( $this, 'get_terms_args' ), 10, 2 );
// Filters terms by language
add_filter( 'terms_clauses', array( $this, 'terms_clauses' ), 10, 3 );
add_action( 'pre_get_posts', array( $this, 'set_tax_query_lang' ), 999 );
add_action( 'posts_selection', array( $this, 'unset_tax_query_lang' ), 0 );
// Deleting terms
add_action( 'pre_delete_term', array( $this, 'delete_term' ), 10, 2 );
}
/**
* Allows to set a language by default for terms if it has no language yet.
*
* @since 1.5.4
*
* @param int $term_id Term ID.
* @param string $taxonomy Taxonomy name.
* @return void
*/
protected function set_default_language( $term_id, $taxonomy ) {
if ( ! $this->model->term->get_language( $term_id ) ) {
if ( ! isset( $this->pref_lang ) && ! empty( $_REQUEST['lang'] ) && $lang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// Testing $this->pref_lang makes this test pass only on frontend.
$this->model->term->set_language( $term_id, $lang );
} elseif ( ( $term = get_term( $term_id, $taxonomy ) ) && ! empty( $term->parent ) && $parent_lang = $this->model->term->get_language( $term->parent ) ) {
// Sets language from term parent if exists thanks to Scott Kingsley Clark
$this->model->term->set_language( $term_id, $parent_lang );
} elseif ( isset( $this->pref_lang ) ) {
// Always defined on admin, never defined on frontend
$this->model->term->set_language( $term_id, $this->pref_lang );
} elseif ( ! empty( $this->curlang ) ) {
// Only on frontend due to the previous test always true on admin
$this->model->term->set_language( $term_id, $this->curlang );
} else {
// In all other cases set to default language.
$this->model->term->set_language( $term_id, $this->options['default_lang'] );
}
}
}
/**
* Called when a category or post tag is created or edited.
* Does nothing except on taxonomies which are filterable.
*
* @since 0.1
*
* @param int $term_id Term id of the term being saved.
* @param int $tt_id Term taxonomy id.
* @param string $taxonomy Taxonomy name.
* @return void
*/
public function save_term( $term_id, $tt_id, $taxonomy ) {
if ( $this->model->is_translated_taxonomy( $taxonomy ) ) {
$lang = $this->model->term->get_language( $term_id );
if ( empty( $lang ) ) {
$this->set_default_language( $term_id, $taxonomy );
}
/**
* Fires after the term language and translations are saved.
*
* @since 1.2
*
* @param int $term_id Term id.
* @param string $taxonomy Taxonomy name.
* @param int[] $translations The list of translations term ids.
*/
do_action( 'pll_save_term', $term_id, $taxonomy, $this->model->term->get_translations( $term_id ) );
}
}
/**
* Get the language(s) to filter WP_Term_Query.
*
* @since 1.7.6
*
* @param string[] $taxonomies Queried taxonomies.
* @param array $args WP_Term_Query arguments.
* @return PLL_Language|string|false The language(s) to use in the filter, false otherwise.
*/
protected function get_queried_language( $taxonomies, $args ) {
global $pagenow;
// Does nothing except on taxonomies which are filterable
// Since WP 4.7, make sure not to filter wp_get_object_terms()
if ( ! $this->model->is_translated_taxonomy( $taxonomies ) || ! empty( $args['object_ids'] ) ) {
return false;
}
// If get_terms is queried with a 'lang' parameter
if ( isset( $args['lang'] ) ) {
return $args['lang'];
}
// On tags page, everything should be filtered according to the admin language filter except the parent dropdown
if ( 'edit-tags.php' === $pagenow && empty( $args['class'] ) ) {
return $this->filter_lang;
}
return $this->curlang;
}
/**
* Adds language dependent cache domain when querying terms.
* Useful as the 'lang' parameter is not included in cache key by WordPress.
*
* @since 1.3
*
* @param array $args WP_Term_Query arguments.
* @param string[] $taxonomies Queried taxonomies.
* @return array Modified arguments.
*/
public function get_terms_args( $args, $taxonomies ) {
// Don't break _get_term_hierarchy().
if ( 'all' === $args['get'] && 'id' === $args['orderby'] && 'id=>parent' === $args['fields'] ) {
$args['lang'] = '';
}
if ( isset( $this->tax_query_lang ) ) {
$args['lang'] = empty( $this->tax_query_lang ) && ! empty( $this->curlang ) && ! empty( $args['slug'] ) ? $this->curlang->slug : $this->tax_query_lang;
}
if ( $lang = $this->get_queried_language( $taxonomies, $args ) ) {
$lang = is_string( $lang ) && strpos( $lang, ',' ) ? explode( ',', $lang ) : $lang;
$key = '_' . ( is_array( $lang ) ? implode( ',', $lang ) : $this->model->get_language( $lang )->slug );
$args['cache_domain'] = empty( $args['cache_domain'] ) ? 'pll' . $key : $args['cache_domain'] . $key;
}
return $args;
}
/**
* Filters categories and post tags by language(s) when needed on admin side
*
* @since 0.2
*
* @param string[] $clauses List of sql clauses.
* @param string[] $taxonomies List of taxonomies.
* @param array $args WP_Term_Query arguments.
* @return string[] Modified sql clauses.
*/
public function terms_clauses( $clauses, $taxonomies, $args ) {
$lang = $this->get_queried_language( $taxonomies, $args );
return $this->model->terms_clauses( $clauses, $lang );
}
/**
* Sets the WP_Term_Query language when doing a WP_Query.
* Needed since WP 4.9.
*
* @since 2.3.2
*
* @param WP_Query $query WP_Query object.
* @return void
*/
public function set_tax_query_lang( $query ) {
$this->tax_query_lang = isset( $query->query_vars['lang'] ) ? $query->query_vars['lang'] : '';
}
/**
* Removes the WP_Term_Query language filter for WP_Query.
* Needed since WP 4.9.
*
* @since 2.3.2
*
* @return void
*/
public function unset_tax_query_lang() {
unset( $this->tax_query_lang );
}
/**
* Called when a category or post tag is deleted
* Deletes language and translations
*
* @since 0.1
*
* @param int $term_id Id of the term to delete.
* @param string $taxonomy Name of the taxonomy.
* @return void
*/
public function delete_term( $term_id, $taxonomy ) {
if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return;
}
// Delete translation and relationships only if the term is translatable.
$this->model->term->delete_translation( $term_id );
$this->model->term->delete_language( $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 set_pre_term_name( $name ) {
return $this->pre_term_name = $name;
}
/**
* Appends language slug to the term slug if needed.
*
* @since 3.3
*
* @param string $slug Term slug.
* @param string $taxonomy Term taxonomy.
* @return string Slug with a language suffix if found.
*/
public function set_pre_term_slug( $slug, $taxonomy ) {
if ( ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return $slug;
}
if ( ! $slug ) {
$slug = sanitize_title( $this->pre_term_name );
}
if ( ! term_exists( $slug, $taxonomy ) ) {
return $slug;
}
/**
* Filters the subsequently inserted term language.
*
* @since 3.3
*
* @param PLL_Language|null $lang Found language object, null otherwise.
* @param string $taxonomy Term taonomy.
* @param string $slug Term slug
*/
$lang = apply_filters( 'pll_inserted_term_language', null, $taxonomy, $slug );
if ( ! $lang instanceof PLL_Language ) {
return $slug;
}
$parent = 0;
if ( is_taxonomy_hierarchical( $taxonomy ) ) {
/**
* Filters the subsequently inserted term parent.
*
* @since 3.3
*
* @param int $parent Parent term ID, 0 if none.
* @param string $taxonomy Term taxonomy.
* @param string $slug Term slug
*/
$parent = apply_filters( 'pll_inserted_term_parent', 0, $taxonomy, $slug );
}
$term_id = (int) $this->model->term_exists_by_slug( $slug, $lang, $taxonomy, $parent );
// If no term exist in the given language with that slug, it can be created.
if ( ! $term_id ) {
$slug .= '-' . $lang->slug;
}
return $slug;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Small set of tools to work with the database.
*
* @since 3.2
*/
class PLL_Db_Tools {
/**
* Changes an array of values into a comma separated list, ready to be used in a `IN ()` clause.
* Only string and integers and supported for now.
*
* @since 3.2
*
* @param (int|string)[] $values An array of values.
* @return string A comma separated list of values.
*/
public static function prepare_values_list( $values ) {
$values = array_map( array( __CLASS__, 'prepare_value' ), (array) $values );
return implode( ',', $values );
}
/**
* Wraps a value in escaped double quotes or casts as an integer.
* Only string and integers and supported for now.
*
* @since 3.2
*
* @global wpdb $wpdb
*
* @param int|string $value A value.
* @return int|string
*/
public static function prepare_value( $value ) {
if ( ! is_numeric( $value ) ) {
return $GLOBALS['wpdb']->prepare( '%s', $value );
}
return (int) $value;
}
}

View File

@@ -0,0 +1,178 @@
<?php
/**
* @package Polylang
*/
/**
* Class to manage REST routes filterable by language.
*
* @since 3.5
*/
class PLL_Filter_REST_Routes {
/**
* REST routes filterable by language ordered by entity type.
*
* @var string[]
* @phpstan-var array<string, string>
*/
private $filtered_entities = array();
/**
* Other REST routes filterable by language.
*
* @var string[]
* @phpstan-var array<string, string>
*/
private $filtered_routes = array();
/**
* @var PLL_Model
*/
private $model;
/**
* Constructor.
*
* @since 3.5
*
* @param PLL_Model $model Shared instance of the current PLL_Model.
*/
public function __construct( PLL_Model $model ) {
$this->model = $model;
// Adds search REST endpoint.
$this->filtered_routes['search'] = 'wp/v2/search';
}
/**
* Adds query parameters to preload paths.
*
* @since 3.5
*
* @param (string|string[])[] $preload_paths Array of paths to preload.
* @param array $args Array of query strings to add paired by key/value.
* @return (string|string[])[]
*/
public function add_query_parameters( array $preload_paths, array $args ): array {
foreach ( $preload_paths as $k => $path ) {
if ( empty( $path ) ) {
continue;
}
$query_params = array();
// If the method request is OPTIONS, $path is an array and the first element is the path
if ( is_array( $path ) ) {
$temp_path = $path[0];
} else {
$temp_path = $path;
}
$path_parts = wp_parse_url( $temp_path );
if ( ! isset( $path_parts['path'] ) || ! $this->is_filtered( $path_parts['path'] ) ) {
continue;
}
if ( ! empty( $path_parts['query'] ) ) {
parse_str( $path_parts['query'], $query_params );
}
// Add params in query params
foreach ( $args as $key => $value ) {
$query_params[ $key ] = $value;
}
// Sort query params to put it in the same order as the preloading middleware does
ksort( $query_params );
// Replace the key by the correct path with query params reordered
$sorted_path = add_query_arg( urlencode_deep( $query_params ), $path_parts['path'] );
if ( is_array( $path ) ) {
$preload_paths[ $k ][0] = $sorted_path;
} else {
$preload_paths[ $k ] = $sorted_path;
}
}
return $preload_paths;
}
/**
* Adds inline script to declare filtered REST route on client side.
*
* @since 3.5
*
* @param string $script_handle Name of the script to add the inline script to.
* @return void
*/
public function add_inline_script( string $script_handle ) {
$script_var = 'let pllFilteredRoutes = ' . (string) wp_json_encode( $this->get() );
wp_add_inline_script( $script_handle, $script_var, 'before' );
}
/**
* Returns filtered REST routes by entity type (e.g. post type or taxonomy).
*
* @since 3.5
*
* @return string[] REST routes.
* @phpstan-return array<string, string>
*/
private function get(): array {
if ( ! empty( $this->filtered_entities ) ) {
return array_merge( $this->filtered_entities, $this->filtered_routes );
}
$translatable_post_types = $this->model->get_translated_post_types();
$translatable_taxonomies = $this->model->get_translated_taxonomies();
$post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
$taxonomies = get_taxonomies( array( 'show_in_rest' => true ), 'objects' );
$this->extract_filtered_rest_entities(
array_merge( $post_types, $taxonomies ),
array_merge( $translatable_post_types, $translatable_taxonomies )
);
return array_merge( $this->filtered_entities, $this->filtered_routes );
}
/**
* Tells if a given route is fileterable by language.
*
* @since 3.5
*
* @param string $rest_route Route to test.
* @return bool Whether the route is filterable or not.
*/
private function is_filtered( string $rest_route ): bool {
$rest_route = trim( $rest_route );
return ! preg_match( '/\d+$/', $rest_route ) && in_array( trim( $rest_route, '/' ), $this->get(), true );
}
/**
* Extracts filterable REST route from an array of entity objects
* from a list of translatable entities (e.g. post types or taxonomies).
*
* @since 3.5
*
* @param object[] $rest_entities Array of post type or taxonomy objects.
* @param string[] $translatable_entities Array of translatable entity names.
* @return void
* @phpstan-param array<WP_Post_Type|WP_Taxonomy> $rest_entities
*/
private function extract_filtered_rest_entities( array $rest_entities, array $translatable_entities ) {
$this->filtered_entities = array();
foreach ( $rest_entities as $rest_entity ) {
if ( in_array( $rest_entity->name, $translatable_entities, true ) ) {
$rest_base = empty( $rest_entity->rest_base ) ? $rest_entity->name : $rest_entity->rest_base;
$rest_namespace = empty( $rest_entity->rest_namespace ) ? 'wp/v2' : $rest_entity->rest_namespace;
$this->filtered_entities[ $rest_entity->name ] = "{$rest_namespace}/{$rest_base}";
}
}
}
}

View File

@@ -0,0 +1,202 @@
<?php
/**
* @package Polylang
*/
/**
* Manages links filters needed on both frontend and admin
*
* @since 1.8
*/
class PLL_Filters_Links {
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
public $links_model;
/**
* @var PLL_Links|null
*/
public $links;
/**
* Current language.
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor.
*
* @since 1.8
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links = &$polylang->links;
$this->links_model = &$polylang->links_model;
$this->model = &$polylang->model;
$this->options = &$polylang->options;
$this->curlang = &$polylang->curlang;
// Low priority on links filters to come after any other modifications.
if ( $this->options['force_lang'] ) {
add_filter( 'post_link', array( $this, 'post_type_link' ), 20, 2 );
add_filter( '_get_page_link', array( $this, '_get_page_link' ), 20, 2 );
}
add_filter( 'post_type_link', array( $this, 'post_type_link' ), 20, 2 );
add_filter( 'term_link', array( $this, 'term_link' ), 20, 3 );
if ( $this->options['force_lang'] > 0 ) {
add_filter( 'attachment_link', array( $this, 'attachment_link' ), 20, 2 );
}
// Keeps the preview post link on default domain when using multiple domains and SSO is not available.
if ( 3 === $this->options['force_lang'] && ! class_exists( 'PLL_Xdata_Domain' ) ) {
add_filter( 'preview_post_link', array( $this, 'preview_post_link' ), 20 );
}
// Rewrites post types archives links to filter them by language.
add_filter( 'post_type_archive_link', array( $this, 'post_type_archive_link' ), 20, 2 );
}
/**
* Modifies page links
*
* @since 1.7
*
* @param string $link post link
* @param int $post_id post ID
* @return string modified post link
*/
public function _get_page_link( $link, $post_id ) {
// /!\ WP does not use pretty permalinks for preview
return false !== strpos( $link, 'preview=true' ) && false !== strpos( $link, 'page_id=' ) ? $link : $this->links_model->switch_language_in_link( $link, $this->model->post->get_language( $post_id ) );
}
/**
* Modifies attachment links
*
* @since 1.6.2
*
* @param string $link attachment link
* @param int $post_id attachment link
* @return string modified attachment link
*/
public function attachment_link( $link, $post_id ) {
return wp_get_post_parent_id( $post_id ) ? $link : $this->links_model->switch_language_in_link( $link, $this->model->post->get_language( $post_id ) );
}
/**
* Modifies custom posts links.
*
* @since 1.6
*
* @param string $link Post link.
* @param WP_Post $post Post object.
* @return string Modified post link.
*/
public function post_type_link( $link, $post ) {
// /!\ WP does not use pretty permalinks for preview
if ( ( false === strpos( $link, 'preview=true' ) || false === strpos( $link, 'p=' ) ) && $this->model->is_translated_post_type( $post->post_type ) ) {
$lang = $this->model->post->get_language( $post->ID );
$link = $this->options['force_lang'] ? $this->links_model->switch_language_in_link( $link, $lang ) : $link;
/**
* Filters a post or custom post type link.
*
* @since 1.6
*
* @param string $link The post link.
* @param PLL_Language $lang The current language.
* @param WP_Post $post The post object.
*/
$link = apply_filters( 'pll_post_type_link', $link, $lang, $post );
}
return $link;
}
/**
* Modifies term links.
*
* @since 0.7
*
* @param string $link Term link.
* @param WP_Term $term Term object.
* @param string $tax Taxonomy name;
* @return string Modified term link.
*/
public function term_link( $link, $term, $tax ) {
if ( $this->model->is_translated_taxonomy( $tax ) ) {
$lang = $this->model->term->get_language( $term->term_id );
$link = $this->options['force_lang'] ? $this->links_model->switch_language_in_link( $link, $lang ) : $link;
/**
* Filter a term link
*
* @since 1.6
*
* @param string $link The term link.
* @param PLL_Language $lang The current language.
* @param WP_Term $term The term object.
*/
return apply_filters( 'pll_term_link', $link, $lang, $term );
}
// In case someone calls get_term_link for the 'language' taxonomy.
if ( 'language' === $tax ) {
$lang = $this->model->get_language( $term->term_id );
if ( $lang ) {
return $this->links_model->home_url( $lang );
}
}
return $link;
}
/**
* Keeps the preview post link on default domain when using multiple domains.
*
* @since 1.6.1
*
* @param string $url URL used for the post preview.
* @return string The modified url.
*/
public function preview_post_link( $url ) {
return $this->links_model->remove_language_from_link( $url );
}
/**
* Modifies the post type archive links to add the language parameter
* only if the post type is translated.
*
* The filter was originally only on frontend but is needed on admin too for
* compatibility with the archive link of the ACF link field since ACF 5.4.0.
*
* @since 1.7.6
*
* @param string $link The post type archive permalink.
* @param string $post_type Post type name.
* @return string
*/
public function post_type_archive_link( $link, $post_type ) {
return $this->model->is_translated_post_type( $post_type ) && 'post' !== $post_type ? $this->links_model->switch_language_in_link( $link, $this->curlang ) : $link;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* @package Polylang
*/
/**
* Setup specific filters useful for sanitization.
*
* Extract from PLL_Admin_Filters to be able to use in a REST API context.
*
* @since 2.9
*/
class PLL_Filters_Sanitization {
/**
* Language used for the sanitization depending on the context.
*
* @var string $locale
*/
public $locale;
/**
* Constructor: setups filters and actions.
*
* @since 2.9
*
* @param string $locale Locale code of the language.
*/
public function __construct( $locale ) {
$this->locale = $locale;
// We need specific filters for some languages like German and Danish
$specific_locales = array( 'da_DK', 'de_DE', 'de_DE_formal', 'de_CH', 'de_CH_informal', 'ca', 'sr_RS', 'bs_BA' );
if ( in_array( $locale, $specific_locales ) ) {
add_filter( 'sanitize_title', array( $this, 'sanitize_title' ), 10, 3 );
add_filter( 'sanitize_user', array( $this, 'sanitize_user' ), 10, 3 );
}
}
/**
* Retrieve the locale code of the language.
*
* @since 2.0
*
* @return string
*/
public function get_locale() {
return $this->locale;
}
/**
* Maybe fix the result of sanitize_title() in case the languages include German or Danish
* Without this filter, if language of the title being sanitized is different from the language
* used for the admin interface and if one this language is German or Danish, some specific
* characters such as ä, ö, ü, ß are incorrectly sanitized.
*
* All the process is done by the remove_accents() WordPress function based on the locale value
*
* @link https://github.com/WordPress/WordPress/blob/5.5.1/wp-includes/formatting.php#L1920-L1944
*
* @since 2.0
*
* @param string $title Sanitized title.
* @param string $raw_title The title prior to sanitization.
* @param string $context The context for which the title is being sanitized.
* @return string
*/
public function sanitize_title( $title, $raw_title, $context ) {
static $once = false;
if ( ! $once && 'save' == $context && ! empty( $title ) ) {
$once = true;
add_filter( 'locale', array( $this, 'get_locale' ), 20 ); // After the filter for the admin interface
$title = sanitize_title( $raw_title, '', $context );
remove_filter( 'locale', array( $this, 'get_locale' ), 20 );
$once = false;
}
return $title;
}
/**
* Maybe fix the result of sanitize_user() in case the languages include German or Danish
*
* All the process is done by the remove_accents() WordPress function based on the locale value
*
* @link https://github.com/WordPress/WordPress/blob/5.5-branch/wp-includes/formatting.php#L1920-L1944
*
* @since 2.0
*
* @param string $username Sanitized username.
* @param string $raw_username The username prior to sanitization.
* @param bool $strict Whether to limit the sanitization to specific characters. Default false.
* @return string
*/
public function sanitize_user( $username, $raw_username, $strict ) {
static $once = false;
if ( ! $once ) {
$once = true;
add_filter( 'locale', array( $this, 'get_locale' ), 20 ); // After the filter for the admin interface
$username = sanitize_user( $raw_username, $strict );
remove_filter( 'locale', array( $this, 'get_locale' ), 20 );
$once = false;
}
return $username;
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package Polylang
*/
/**
* Class PLL_Widgets_Filters
*
* @since 3.0
*
* Add new options to {@see https://developer.wordpress.org/reference/classes/wp_widget/ WP_Widget} and saves them.
*/
class PLL_Filters_Widgets_Options {
/**
* @var PLL_Model
*/
public $model;
/**
* PLL_Widgets_Filters constructor.
*
* @since 3.0 Moved actions from PLL_Admin_Filters.
*
* @param PLL_Base $polylang The Polylang object.
* @return void
*/
public function __construct( $polylang ) {
$this->model = $polylang->model;
add_action( 'in_widget_form', array( $this, 'in_widget_form' ), 10, 3 );
add_filter( 'widget_update_callback', array( $this, 'widget_update_callback' ), 10, 2 );
}
/**
* Add the language filter field to the widgets options form.
*
* @since 3.0 Moved PLL_Admin_Filters.
* @since 3.1 Rename lang_choice field name and id to pll_lang as the widget setting.
*
* @param WP_Widget $widget The widget instance (passed by reference).
* @param null $return Return null if new fields are added.
* @param array $instance An array of the widget's settings.
* @return void
*/
public function in_widget_form( $widget, $return, $instance ) {
$dropdown = new PLL_Walker_Dropdown();
$dropdown_html = $dropdown->walk(
array_merge(
array( (object) array( 'slug' => 0, 'name' => __( 'All languages', 'polylang' ) ) ),
$this->model->get_languages_list()
),
-1,
array(
'id' => $widget->get_field_id( 'pll_lang' ),
'name' => $widget->get_field_name( 'pll_lang' ),
'class' => 'tags-input pll-lang-choice',
'selected' => empty( $instance['pll_lang'] ) ? '' : $instance['pll_lang'],
)
);
printf(
'<p><label for="%1$s">%2$s %3$s</label></p>',
esc_attr( $widget->get_field_id( 'pll_lang' ) ),
esc_html__( 'The widget is displayed for:', 'polylang' ),
$dropdown_html // phpcs:ignore WordPress.Security.EscapeOutput
);
}
/**
* Called when widget options are saved.
* Saves the language associated to the widget.
*
* @since 0.3
* @since 3.0 Moved from PLL_Admin_Filters.
* @since 3.1 Remove unused $old_instance and $widget parameters.
*
* @param array $instance The current Widget's options.
* @param array $new_instance The new Widget's options.
* @return array Widget options.
*/
public function widget_update_callback( $instance, $new_instance ) {
if ( ! empty( $new_instance['pll_lang'] ) && $lang = $this->model->get_language( $new_instance['pll_lang'] ) ) {
$instance['pll_lang'] = $lang->slug;
} else {
unset( $instance['pll_lang'] );
}
return $instance;
}
}

View File

@@ -0,0 +1,472 @@
<?php
/**
* @package Polylang
*/
/**
* Setup filters common to admin and frontend
*
* @since 1.4
*/
class PLL_Filters {
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
public $links_model;
/**
* Current language.
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor: setups filters
*
* @since 1.4
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
global $wp_version;
$this->links_model = &$polylang->links_model;
$this->model = &$polylang->model;
$this->options = &$polylang->options;
$this->curlang = &$polylang->curlang;
// Deletes our cache for sticky posts when the list is updated.
add_action( 'update_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
add_action( 'add_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
add_action( 'delete_option_sticky_posts', array( $this, 'delete_sticky_posts_cache' ) );
// Filters the comments according to the current language
add_action( 'parse_comment_query', array( $this, 'parse_comment_query' ) );
add_filter( 'comments_clauses', array( $this, 'comments_clauses' ), 10, 2 );
// Filters the get_pages function according to the current language
if ( version_compare( $wp_version, '6.3-alpha', '<' ) ) {
// Backward compatibility with WP < 6.3.
add_filter( 'get_pages', array( $this, 'get_pages' ), 10, 2 );
}
add_filter( 'get_pages_query_args', array( $this, 'get_pages_query_args' ), 10, 2 );
// Rewrites next and previous post links to filter them by language
add_filter( 'get_previous_post_join', array( $this, 'posts_join' ), 10, 5 );
add_filter( 'get_next_post_join', array( $this, 'posts_join' ), 10, 5 );
add_filter( 'get_previous_post_where', array( $this, 'posts_where' ), 10, 5 );
add_filter( 'get_next_post_where', array( $this, 'posts_where' ), 10, 5 );
// Converts the locale to a valid W3C locale
add_filter( 'language_attributes', array( $this, 'language_attributes' ) );
// Translate the site title in emails sent to users
add_filter( 'password_change_email', array( $this, 'translate_user_email' ) );
add_filter( 'email_change_email', array( $this, 'translate_user_email' ) );
// Translates the privacy policy page
add_filter( 'option_wp_page_for_privacy_policy', array( $this, 'translate_page_for_privacy_policy' ), 20 ); // Since WP 4.9.6
add_filter( 'map_meta_cap', array( $this, 'fix_privacy_policy_page_editing' ), 10, 4 );
// Personal data exporter
add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ), 0 ); // Since WP 4.9.6
// Fix for `term_exists()`.
add_filter( 'term_exists_default_query_args', array( $this, 'term_exists_default_query_args' ), 0, 3 ); // Since WP 6.0.0.
}
/**
* Deletes the cache for multilingual sticky posts.
*
* @since 2.8.4
*
* @return void
*/
public function delete_sticky_posts_cache() {
wp_cache_delete( 'sticky_posts', 'options' );
}
/**
* Get the language to filter a comments query.
*
* @since 2.0
* @since 3.1 Always returns an array. Renamed from get_comments_queried_language().
*
* @param WP_Comment_Query $query WP_Comment_Query object.
* @return PLL_Language[] The languages to use in the filter.
*/
protected function get_comments_queried_languages( $query ) {
// Don't filter comments if comment ids or post ids are specified.
$plucked = wp_array_slice_assoc( $query->query_vars, array( 'comment__in', 'parent', 'post_id', 'post__in', 'post_parent' ) );
$fields = array_filter( $plucked );
if ( ! empty( $fields ) ) {
return array();
}
// Don't filter comments if a non translated post type is specified.
if ( ! empty( $query->query_vars['post_type'] ) && ! $this->model->is_translated_post_type( $query->query_vars['post_type'] ) ) {
return array();
}
// If comments are queried with a 'lang' parameter, keeps only language codes.
if ( isset( $query->query_vars['lang'] ) ) {
$languages = is_string( $query->query_vars['lang'] ) ? explode( ',', $query->query_vars['lang'] ) : $query->query_vars['lang'];
if ( is_array( $languages ) ) {
$languages = array_map( array( $this->model, 'get_language' ), $languages );
return array_filter( $languages );
}
}
if ( ! empty( $this->curlang ) ) {
return array( $this->curlang );
}
return array();
}
/**
* Adds a language dependent cache domain when querying comments.
* Useful as the 'lang' parameter is not included in cache key by WordPress.
* Needed since WP 4.6 as comments have been added to persistent cache. See #36906, #37419.
*
* @since 2.0
*
* @param WP_Comment_Query $query WP_Comment_Query object.
* @return void
*/
public function parse_comment_query( $query ) {
$lang = $this->get_comments_queried_languages( $query );
if ( ! empty( $lang ) ) {
$lang = wp_list_pluck( $lang, 'slug' );
$key = '_' . implode( ',', $lang );
$query->query_vars['cache_domain'] = empty( $query->query_vars['cache_domain'] ) ? 'pll' . $key : $query->query_vars['cache_domain'] . $key;
}
}
/**
* Filters the comments according to the current language.
* Used by the recent comments widget and admin language filter.
*
* @since 0.2
*
* @param string[] $clauses SQL clauses.
* @param WP_Comment_Query $query WP_Comment_Query object.
* @return string[] Modified $clauses.
*/
public function comments_clauses( $clauses, $query ) {
global $wpdb;
$lang = $this->get_comments_queried_languages( $query );
if ( ! empty( $lang ) ) {
$lang = wp_list_pluck( $lang, 'slug' );
// If this clause is not already added by WP.
if ( ! strpos( $clauses['join'], '.ID' ) ) {
$clauses['join'] .= " JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
}
$clauses['join'] .= $this->model->post->join_clause();
$clauses['where'] .= $this->model->post->where_clause( $lang );
}
return $clauses;
}
/**
* Filters get_pages() per language.
*
* @since 1.4
*
* @param WP_Post[] $pages An array of pages already queried.
* @param array $args Array of get_pages() arguments.
* @return WP_Post[] Modified list of pages.
*/
public function get_pages( $pages, $args ) {
if ( isset( $args['lang'] ) && empty( $args['lang'] ) ) {
return $pages;
}
$language = empty( $args['lang'] ) ? $this->curlang : $this->model->get_language( $args['lang'] );
if ( empty( $language ) || empty( $pages ) || ! $this->model->is_translated_post_type( $args['post_type'] ) ) {
return $pages;
}
static $once = false;
if ( ! empty( $args['number'] ) && ! $once ) {
// We are obliged to redo the get_pages() query if we want to get the right number.
$once = true; // Avoid infinite loop.
// Take care that 'exclude' argument accepts integer or strings too.
$args['exclude'] = array_merge( wp_parse_id_list( $args['exclude'] ), $this->get_related_page_ids( $language, 'NOT IN', $args ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
$numbered_pages = get_pages( $args );
$pages = ! $numbered_pages ? $pages : $numbered_pages;
}
$ids = wp_list_pluck( $pages, 'ID' );
if ( ! $once ) {
// Filters the queried list of pages by language.
$ids = array_intersect( $ids, $this->get_related_page_ids( $language, 'IN', $args ) );
foreach ( $pages as $key => $page ) {
if ( ! in_array( $page->ID, $ids ) ) {
unset( $pages[ $key ] );
}
}
$pages = array_values( $pages ); // In case 3rd parties suppose the existence of $pages[0].
}
// Not done by WP but extremely useful for performance when manipulating taxonomies.
update_object_term_cache( $ids, $args['post_type'] );
$once = false; // In case get_pages() is called another time.
return $pages;
}
/**
* Filters the WP_Query in get_pages() per language.
*
* @since 3.4.3
*
* @param array $query_args Array of arguments passed to WP_Query.
* @param array $parsed_args Array of get_pages() arguments.
* @return array Array of arguments passed to WP_Query with the language.
*/
public function get_pages_query_args( $query_args, $parsed_args ) {
if ( isset( $parsed_args['lang'] ) ) {
$query_args['lang'] = $parsed_args['lang'];
}
return $query_args;
}
/**
* Get page ids related to a get_pages() in or not in a given language.
*
* @since 3.2
*
* @param PLL_Language $language The language to use in the relationship
* @param string $relation 'IN' or 'NOT IN'.
* @param array $args Array of get_pages() arguments.
* @return int[]
*/
protected function get_related_page_ids( $language, $relation, $args ) {
$r = array(
'lang' => '', // Ensure this query is not filtered.
'numberposts' => -1,
'nopaging' => true,
'post_type' => $args['post_type'],
'post_status' => $args['post_status'],
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => 'language',
'field' => 'term_taxonomy_id', // Since WP 3.5.
'terms' => $language->get_tax_prop( 'language', 'term_taxonomy_id' ),
'operator' => $relation,
),
),
);
return get_posts( $r );
}
/**
* Modifies the sql request for get_adjacent_post to filter by the current language.
*
* @since 0.1
*
* @param string $sql The JOIN clause in the SQL.
* @param bool $in_same_term Whether post should be in a same taxonomy term.
* @param int[] $excluded_terms Array of excluded term IDs.
* @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
* @param WP_Post $post WP_Post object.
* @return string Modified JOIN clause.
*/
public function posts_join( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->join_clause( 'p' ) : $sql;
}
/**
* Modifies the sql request for wp_get_archives and get_adjacent_post to filter by the current language.
*
* @since 0.1
*
* @param string $sql The WHERE clause in the SQL.
* @param bool $in_same_term Whether post should be in a same taxonomy term.
* @param int[] $excluded_terms Array of excluded term IDs.
* @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
* @param WP_Post $post WP_Post object.
* @return string Modified WHERE clause.
*/
public function posts_where( $sql, $in_same_term, $excluded_terms, $taxonomy, $post ) {
return $this->model->is_translated_post_type( $post->post_type ) && ! empty( $this->curlang ) ? $sql . $this->model->post->where_clause( $this->curlang ) : $sql;
}
/**
* Converts WordPress locale to valid W3 locale in html language attributes
*
* @since 1.8
*
* @param string $output language attributes
* @return string
*/
public function language_attributes( $output ) {
if ( $language = $this->model->get_language( is_admin() ? get_user_locale() : get_locale() ) ) {
$output = str_replace( '"' . get_bloginfo( 'language' ) . '"', '"' . $language->get_locale( 'display' ) . '"', $output );
}
return $output;
}
/**
* Translates the site title in emails sent to the user (change email, reset password)
* It is necessary to filter the email because WP evaluates the site title before calling switch_to_locale()
*
* @since 2.1.3
*
* @param string[] $email Email contents.
* @return string[] Translated email contents.
*/
public function translate_user_email( $email ) {
$blog_name = wp_specialchars_decode( pll__( get_option( 'blogname' ) ), ENT_QUOTES );
$email['subject'] = sprintf( $email['subject'], $blog_name );
$email['message'] = str_replace( '###SITENAME###', $blog_name, $email['message'] );
return $email;
}
/**
* Translates the privacy policy page, on both frontend and admin
*
* @since 2.3.6
*
* @param int $id Privacy policy page id
* @return int
*/
public function translate_page_for_privacy_policy( $id ) {
return empty( $this->curlang ) ? $id : $this->model->post->get( $id, $this->curlang );
}
/**
* Prevents edit and delete links for the translations of the privacy policy page for non admin
*
* @since 2.3.7
*
* @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 category id.
* @return array
*/
public function fix_privacy_policy_page_editing( $caps, $cap, $user_id, $args ) {
if ( in_array( $cap, array( 'edit_page', 'edit_post', 'delete_page', 'delete_post' ) ) ) {
$privacy_page = get_option( 'wp_page_for_privacy_policy' );
if ( $privacy_page && array_intersect( $args, $this->model->post->get_translations( $privacy_page ) ) ) {
$caps = array_merge( $caps, map_meta_cap( 'manage_privacy_options', $user_id ) );
}
}
return $caps;
}
/**
* Register our personal data exporter
*
* @since 2.3.6
*
* @param array $exporters Personal data exporters
* @return array
*/
public function register_personal_data_exporter( $exporters ) {
$exporters[] = array(
'exporter_friendly_name' => __( 'Translated user descriptions', 'polylang' ),
'callback' => array( $this, 'user_data_exporter' ),
);
return $exporters;
}
/**
* Export translated user description as WP exports only the description in the default language
*
* @since 2.3.6
*
* @param string $email_address User email address
* @return array Personal data
*/
public function user_data_exporter( $email_address ) {
$email_address = trim( $email_address );
$data_to_export = array();
$user_data_to_export = array();
if ( $user = get_user_by( 'email', $email_address ) ) {
foreach ( $this->model->get_languages_list() as $lang ) {
if ( ! $lang->is_default && $value = get_user_meta( $user->ID, 'description_' . $lang->slug, true ) ) {
$user_data_to_export[] = array(
/* translators: %s is a language native name */
'name' => sprintf( __( 'User description - %s', 'polylang' ), $lang->name ),
'value' => $value,
);
}
}
if ( ! empty( $user_data_to_export ) ) {
$data_to_export[] = array(
'group_id' => 'user',
'group_label' => __( 'User', 'polylang' ),
'item_id' => "user-{$user->ID}",
'data' => $user_data_to_export,
);
}
}
return array(
'data' => $data_to_export,
'done' => true,
);
}
/**
* Filters default query arguments for checking if a term exists.
* In `term_exists()`, WP 6.0 uses `get_terms()`, which is filtered by language by Polylang.
* This filter prevents `term_exists()` to be filtered by language.
*
* @since 3.2
*
* @param array $defaults An array of arguments passed to get_terms().
* @param int|string $term The term to check. Accepts term ID, slug, or name.
* @param string $taxonomy The taxonomy name to use. An empty string indicates the search is against all taxonomies.
* @return array
*/
public function term_exists_default_query_args( $defaults, $term, $taxonomy ) {
if ( ! empty( $taxonomy ) && ! $this->model->is_translated_taxonomy( $taxonomy ) ) {
return $defaults;
}
if ( ! is_array( $defaults ) ) {
$defaults = array();
}
if ( ! isset( $defaults['lang'] ) ) {
$defaults['lang'] = '';
}
return $defaults;
}
}

View File

@@ -0,0 +1,237 @@
<?php
/**
* @package Polylang
*/
/**
* Define wordpress.com VIP equivalent of uncached functions
* WordPress backward compatibility functions
* and miscellaneous utility functions
*/
if ( ! function_exists( 'wpcom_vip_get_page_by_path' ) ) {
/**
* Retrieves a page given its path.
*
* @since 2.8.3
*
* @param string $page_path Page path.
* @param string $output Optional. The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which correspond to
* a WP_Post object, an associative array, or a numeric array, respectively. Default OBJECT.
* @param string|array $post_type Optional. Post type or array of post types. Default 'page'.
* @return WP_Post|array|null WP_Post (or array) on success, or null on failure.
*/
function wpcom_vip_get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
return get_page_by_path( $page_path, $output, $post_type ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_page_by_path_get_page_by_path
}
}
if ( ! function_exists( 'sanitize_locale_name' ) ) {
/**
* Strips out all characters not allowed in a locale code.
* Backward compatibility with WP < 6.2.1.
*
* @since 3.5
*
* @param string $locale_name The locale name to be sanitized.
* @return string The sanitized value.
*/
function sanitize_locale_name( $locale_name ) {
// Limit to A-Z, a-z, 0-9, '_', '-'.
$sanitized = (string) preg_replace( '/[^A-Za-z0-9_-]/', '', $locale_name );
/**
* Filters a sanitized locale name string.
* Backward compatibility with WP < 6.2.1.
*
* @since 3.5
*
* @param string $sanitized The sanitized locale name.
* @param string $locale_name The locale name before sanitization.
*/
return apply_filters( 'sanitize_locale_name', $sanitized, $locale_name );
}
}
/**
* Determines whether we should load the cache compatibility
*
* @since 2.3.8
*
* @return bool True if the cache compatibility must be loaded
*/
function pll_is_cache_active() {
/**
* Filters whether we should load the cache compatibility
*
* @since 2.3.8
*
* @bool $is_cache True if a known cache plugin is active
* incl. WP Fastest Cache which doesn't use WP_CACHE
*/
return apply_filters( 'pll_is_cache_active', ( defined( 'WP_CACHE' ) && WP_CACHE ) || defined( 'WPFC_MAIN_PATH' ) );
}
/**
* Get the the current requested url
*
* @since 2.6
*
* @return string Requested url
*/
function pll_get_requested_url() {
if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
return set_url_scheme( esc_url_raw( wp_unslash( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ) );
}
/** @var string */
$home_url = get_option( 'home' );
/*
* In WP CLI context, few developers define superglobals in wp-config.php
* as proposed in https://make.wordpress.org/cli/handbook/common-issues/#php-notice-undefined-index-on-_server-superglobal
* So let's return the unfiltered home url to avoid a bunch of notices.
*/
if ( defined( 'WP_CLI' ) && WP_CLI ) {
return $home_url;
}
/*
* When using system CRON instead of WP_CRON, the superglobals are likely undefined.
*/
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
return $home_url;
}
if ( WP_DEBUG ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
trigger_error( '$_SERVER[\'HTTP_HOST\'] or $_SERVER[\'REQUEST_URI\'] are required but not set.' );
}
return '';
}
/**
* Determines whether we should load the block editor plugin or the legacy languages metabox.
*
* @since 2.6.0
*
* @return bool True to use the block editor plugin.
*/
function pll_use_block_editor_plugin() {
/**
* Filters whether we should load the block editor plugin or the legacy languages metabox.
*
* @since 2.6.0
*
* @param bool $use_plugin True when loading the block editor plugin.
*/
return class_exists( 'PLL_Block_Editor_Plugin' ) && apply_filters( 'pll_use_block_editor_plugin', ! defined( 'PLL_USE_BLOCK_EDITOR_PLUGIN' ) || PLL_USE_BLOCK_EDITOR_PLUGIN );
}
/**
* Tells if a constant is defined.
*
* @since 3.5
*
* @param string $constant_name Name of the constant.
* @return bool True if the constant is defined, false otherwise.
*
* @phpstan-param non-falsy-string $constant_name
*/
function pll_has_constant( string $constant_name ): bool {
return defined( $constant_name ); // phpcs:ignore WordPressVIPMinimum.Constants.ConstantString.NotCheckingConstantName
}
/**
* Returns the value of a constant if it is defined.
*
* @since 3.5
*
* @param string $constant_name Name of the constant.
* @param mixed $default Optional. Value to return if the constant is not defined. Defaults to `null`.
* @return mixed The value of the constant.
*
* @phpstan-param non-falsy-string $constant_name
* @phpstan-param int|float|string|bool|array|null $default
*/
function pll_get_constant( string $constant_name, $default = null ) {
if ( ! pll_has_constant( $constant_name ) ) {
return $default;
}
return constant( $constant_name );
}
/**
* Defines a constant if it is not already defined.
*
* @since 3.5
*
* @param string $constant_name Name of the constant.
* @param mixed $value Value to set.
* @return bool True on success, false on failure or already defined.
*
* @phpstan-param non-falsy-string $constant_name
* @phpstan-param int|float|string|bool|array|null $value
*/
function pll_set_constant( string $constant_name, $value ): bool {
if ( pll_has_constant( $constant_name ) ) {
return false;
}
return define( $constant_name, $value ); // phpcs:ignore WordPressVIPMinimum.Constants.ConstantString.NotCheckingConstantName
}
/**
* Determines whether a plugin is active.
*
* We define our own function because `is_plugin_active()` is available only in the backend.
*
* @since 3.5
*
* @param string $plugin_name Plugin basename.
* @return bool True if activated, false otherwise.
*/
function pll_is_plugin_active( string $plugin_name ) {
$sitewide_plugins = get_site_option( 'active_sitewide_plugins' );
$sitewide_plugins = ! empty( $sitewide_plugins ) && is_array( $sitewide_plugins ) ? array_keys( $sitewide_plugins ) : array();
$current_site_plugins = (array) get_option( 'active_plugins', array() );
$plugins = array_merge( $sitewide_plugins, $current_site_plugins );
return in_array( $plugin_name, $plugins );
}
/**
* Prepares and registers notices.
*
* Wraps `add_settings_error()` to make its use more consistent.
*
* @since 3.6
*
* @param WP_Error $error Error object.
* @return void
*/
function pll_add_notice( WP_Error $error ) {
if ( ! $error->has_errors() ) {
return;
}
foreach ( $error->get_error_codes() as $error_code ) {
// Extract the "error" type.
$data = $error->get_error_data( $error_code );
$type = empty( $data ) || ! is_string( $data ) ? 'error' : $data;
$message = wp_kses(
implode( '<br>', $error->get_error_messages( $error_code ) ),
array(
'a' => array( 'href' ),
'br' => array(),
'code' => array(),
'em' => array(),
)
);
add_settings_error( 'polylang', $error_code, $message, $type );
}
}

View File

@@ -0,0 +1,256 @@
<?php
/**
* @package Polylang
*/
/**
* Holds everything related to deprecated properties of `PLL_Language`.
*
* @since 3.4
*/
abstract class PLL_Language_Deprecated {
/**
* List of deprecated term properties and related arguments to use with `get_tax_prop()`.
*
* @private
*
* @var string[][]
*/
const DEPRECATED_TERM_PROPERTIES = array(
'term_taxonomy_id' => array( 'language', 'term_taxonomy_id' ),
'count' => array( 'language', 'count' ),
'tl_term_id' => array( 'term_language', 'term_id' ),
'tl_term_taxonomy_id' => array( 'term_language', 'term_taxonomy_id' ),
'tl_count' => array( 'term_language', 'count' ),
);
/**
* List of deprecated URL properties and related getter to use.
*
* @private
*
* @var string[]
*/
const DEPRECATED_URL_PROPERTIES = array(
'home_url' => 'get_home_url',
'search_url' => 'get_search_url',
);
/**
* Returns a language term property value (term ID, term taxonomy ID, or count).
*
* @since 3.4
*
* @param string $taxonomy_name Name of the taxonomy.
* @param string $prop_name Name of the property: 'term_taxonomy_id', 'term_id', 'count'.
* @return int
*
* @phpstan-param non-empty-string $taxonomy_name
* @phpstan-param 'term_taxonomy_id'|'term_id'|'count' $prop_name
* @phpstan-return int<0, max>
*/
abstract public function get_tax_prop( $taxonomy_name, $prop_name );
/**
* Returns language's home URL. Takes care to render it dynamically if no cache is allowed.
*
* @since 3.4
*
* @return string Language home URL.
*
* @phpstan-return non-empty-string
*/
abstract public function get_home_url();
/**
* Returns language's search URL. Takes care to render it dynamically if no cache is allowed.
*
* @since 3.4
*
* @return string Language search URL.
*
* @phpstan-return non-empty-string
*/
abstract public function get_search_url();
/**
* Throws a depreciation notice if someone tries to get one of the following properties:
* `term_taxonomy_id`, `count`, `tl_term_id`, `tl_term_taxonomy_id` or `tl_count`.
*
* Backward compatibility with Polylang < 3.4.
*
* @since 3.4
*
* @param string $property Property to get.
* @return mixed Required property value.
*/
public function __get( $property ) {
// Deprecated property.
if ( $this->is_deprecated_term_property( $property ) ) {
$this->deprecated_property(
$property,
sprintf(
"get_tax_prop( '%s', '%s' )",
self::DEPRECATED_TERM_PROPERTIES[ $property ][0],
self::DEPRECATED_TERM_PROPERTIES[ $property ][1]
)
);
return $this->get_deprecated_term_property( $property );
}
if ( $this->is_deprecated_url_property( $property ) ) {
$this->deprecated_property( $property, "get_{$property}()" );
return $this->get_deprecated_url_property( $property );
}
// Undefined property.
if ( ! property_exists( $this, $property ) ) {
return null;
}
// The property is defined.
$ref = new ReflectionProperty( $this, $property );
// Public property.
if ( $ref->isPublic() ) {
return $this->{$property};
}
// Protected or private property.
$visibility = $ref->isPrivate() ? 'private' : 'protected';
$trace = debug_backtrace(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection, WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
$file = isset( $trace[0]['file'] ) ? $trace[0]['file'] : '';
$line = isset( $trace[0]['line'] ) ? $trace[0]['line'] : 0;
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
esc_html(
sprintf(
"Cannot access %s property %s::$%s in %s on line %d.\nError handler",
$visibility,
get_class( $this ),
$property,
$file,
$line
)
),
E_USER_ERROR
);
}
/**
* Checks for a deprecated property.
* Is triggered by calling `isset()` or `empty()` on inaccessible (protected or private) or non-existing properties.
*
* Backward compatibility with Polylang < 3.4.
*
* @since 3.4
*
* @param string $property A property name.
* @return bool
*/
public function __isset( $property ) {
return $this->is_deprecated_term_property( $property ) || $this->is_deprecated_url_property( $property );
}
/**
* Tells if the given term property is deprecated.
*
* @since 3.4
* @see PLL_Language::DEPRECATED_TERM_PROPERTIES for the list of deprecated properties.
*
* @param string $property A property name.
* @return bool
*
* @phpstan-assert-if-true key-of<PLL_Language::DEPRECATED_TERM_PROPERTIES> $property
*/
protected function is_deprecated_term_property( $property ) {
return array_key_exists( $property, self::DEPRECATED_TERM_PROPERTIES );
}
/**
* Returns a deprecated term property's value.
*
* @since 3.4
* @see PLL_Language::DEPRECATED_TERM_PROPERTIES for the list of deprecated properties.
*
* @param string $property A property name.
* @return int
*
* @phpstan-param key-of<PLL_Language::DEPRECATED_TERM_PROPERTIES> $property
* @phpstan-return int<0, max>
*/
protected function get_deprecated_term_property( $property ) {
return $this->get_tax_prop(
self::DEPRECATED_TERM_PROPERTIES[ $property ][0],
self::DEPRECATED_TERM_PROPERTIES[ $property ][1]
);
}
/**
* Tells if the given URL property is deprecated.
*
* @since 3.4
* @see PLL_Language::DEPRECATED_URL_PROPERTIES for the list of deprecated properties.
*
* @param string $property A property name.
* @return bool
*
* @phpstan-assert-if-true key-of<PLL_Language::DEPRECATED_URL_PROPERTIES> $property
*/
protected function is_deprecated_url_property( $property ) {
return array_key_exists( $property, self::DEPRECATED_URL_PROPERTIES );
}
/**
* Returns a deprecated URL property's value.
*
* @since 3.4
* @see PLL_Language::DEPRECATED_URL_PROPERTIES for the list of deprecated properties.
*
* @param string $property A property name.
* @return string
*
* @phpstan-param key-of<PLL_Language::DEPRECATED_URL_PROPERTIES> $property
* @phpstan-return non-empty-string
*/
protected function get_deprecated_url_property( $property ) {
return $this->{self::DEPRECATED_URL_PROPERTIES[ $property ]}();
}
/**
* Triggers a deprecated an error for a deprecated property.
*
* @since 3.4
*
* @param string $property Deprecated property name.
* @param string $replacement Method or property name to use instead.
* @return void
*/
private function deprecated_property( $property, $replacement ) {
/**
* Filters whether to trigger an error for deprecated properties.
*
* The filter name is intentionally not prefixed to use the same as WordPress
* in case it is added in the future.
*
* @since 3.4
*
* @param bool $trigger Whether to trigger the error for deprecated properties. Default true.
*/
if ( ! WP_DEBUG || ! apply_filters( 'deprecated_property_trigger_error', true ) ) {
return;
}
trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
sprintf(
"Class property %1\$s::\$%2\$s is deprecated, use %1\$s::%3\$s instead.\nError handler",
esc_html( get_class( $this ) ),
esc_html( $property ),
esc_html( $replacement )
),
E_USER_DEPRECATED
);
}
}

View File

@@ -0,0 +1,338 @@
<?php
/**
* @package Polylang
*/
/**
* PLL_Language factory.
*
* @since 3.4
*
* @phpstan-import-type LanguageData from PLL_Language
*/
class PLL_Language_Factory {
/**
* Predefined languages.
*
* @var array[]
*
* @phpstan-var array<string, array<string, string>>
*/
private static $languages;
/**
* Polylang's options.
*
* @var array
*/
private $options;
/**
* Constructor.
*
* @since 3.4
*
* @param array $options Array of Poylang's options passed by reference.
* @return void
*/
public function __construct( &$options ) {
$this->options = &$options;
}
/**
* Returns a language object matching the given data, looking up in cached transient.
*
* @since 3.4
*
* @param array $language_data Language object properties stored as an array. See `PLL_Language::__construct()`
* for information on accepted properties.
*
* @return PLL_Language A language object if given data pass sanitization.
*
* @phpstan-param LanguageData $language_data
*/
public function get( $language_data ) {
return new PLL_Language( $this->sanitize_data( $language_data ) );
}
/**
* Returns a language object based on terms.
*
* @since 3.4
*
* @param WP_Term[] $terms List of language terms, with the language taxonomy names as array keys.
* `language` is a mandatory key for the object to be created,
* `term_language` should be too in a fully operational environment.
* @return PLL_Language|null Language object on success, `null` on failure.
*
* @phpstan-param array{language?:WP_Term}&array<string, WP_Term> $terms
*/
public function get_from_terms( array $terms ) {
if ( ! isset( $terms['language'] ) ) {
return null;
}
$languages = $this->get_languages();
$data = array(
'name' => $terms['language']->name,
'slug' => $terms['language']->slug,
'term_group' => $terms['language']->term_group,
'term_props' => array(),
'is_default' => $this->options['default_lang'] === $terms['language']->slug,
);
foreach ( $terms as $term ) {
$data['term_props'][ $term->taxonomy ] = array(
'term_id' => $term->term_id,
'term_taxonomy_id' => $term->term_taxonomy_id,
'count' => $term->count,
);
}
// The description fields can contain any property.
$description = maybe_unserialize( $terms['language']->description );
if ( is_array( $description ) ) {
$description = array_intersect_key(
$description,
array( 'locale' => null, 'rtl' => null, 'flag_code' => null, 'active' => null, 'fallbacks' => null )
);
foreach ( $description as $prop => $value ) {
if ( 'rtl' === $prop ) {
$data['is_rtl'] = $value;
} else {
$data[ $prop ] = $value;
}
}
}
if ( ! empty( $data['locale'] ) ) {
if ( isset( $languages[ $data['locale'] ]['w3c'] ) ) {
$data['w3c'] = $languages[ $data['locale'] ]['w3c'];
} else {
$data['w3c'] = str_replace( '_', '-', $data['locale'] );
}
if ( isset( $languages[ $data['locale'] ]['facebook'] ) ) {
$data['facebook'] = $languages[ $data['locale'] ]['facebook'];
}
}
$flag_props = $this->get_flag( $data['flag_code'], $data['name'], $data['slug'], $data['locale'] );
$data = array_merge( $data, $flag_props );
$additional_data = array();
/**
* Filters additional data to add to the language before it is created.
*
* `home_url`, `search_url`, `page_on_front` and `page_for_posts` are only allowed.
*
* @since 3.4
*
* @param array $additional_data.
* @param array $data Language data.
*
* @phpstan-param array<non-empty-string, mixed> $additional_data
* @phpstan-param non-empty-array<non-empty-string, mixed> $data
*/
$additional_data = apply_filters( 'pll_additional_language_data', $additional_data, $data );
$allowed_additional_data = array(
'home_url' => '',
'search_url' => '',
'page_on_front' => 0,
'page_for_posts' => 0,
);
$data = array_merge( $data, array_intersect_key( $additional_data, $allowed_additional_data ) );
return new PLL_Language( $this->sanitize_data( $data ) );
}
/**
* Sanitizes data, to be ready to be used in the constructor.
* This doesn't verify that the language terms exist.
*
* @since 3.4
*
* @param array $data Data to process. See `PLL_Language::__construct()` for information on accepted data.
* @return array Sanitized Data.
*
* @phpstan-return LanguageData
*/
private function sanitize_data( array $data ) {
foreach ( $data['term_props'] as $tax => $props ) {
$data['term_props'][ $tax ] = array_map( 'absint', $props );
}
$data['is_rtl'] = ! empty( $data['is_rtl'] ) ? 1 : 0;
$positive_fields = array( 'term_group', 'page_on_front', 'page_for_posts' );
foreach ( $positive_fields as $field ) {
$data[ $field ] = ! empty( $data[ $field ] ) ? absint( $data[ $field ] ) : 0;
}
$data['active'] = isset( $data['active'] ) ? (bool) $data['active'] : true;
if ( array_key_exists( 'fallbacks', $data ) && ! is_array( $data['fallbacks'] ) ) {
unset( $data['fallbacks'] );
}
/**
* @var LanguageData
*/
return $data;
}
/**
* Returns predefined languages data.
*
* @since 3.4
*
* @return array[]
*
* @phpstan-return array<string, array<string, string>>
*/
private function get_languages() {
if ( empty( self::$languages ) ) {
self::$languages = include POLYLANG_DIR . '/settings/languages.php';
}
return self::$languages;
}
/**
* Creates flag_url and flag language properties. Also takes care of custom flag.
*
* @since 1.2
* @since 3.4 Moved from `PLL_Language`to `PLL_Language_Factory` and renamed
* in favor of `get_flag()` (formerly `set_flag()`).
*
* @param string $flag_code Flag code.
* @param string $name Language name.
* @param string $slug Language slug.
* @param string $locale Language locale.
* @return array {
* Array of the flag properties.
* @type string $flag_url URL of the flag.
* @type string $flag HTML markup of the flag.
* @type string $custom_flag_url Optional. URL of the custom flag if it exists.
* @type string $custom_flag Optional. HTML markup of the custom flag if it exists.
* }
*
* @phpstan-return array{
* flag_url: string,
* flag: string,
* custom_flag_url?: non-empty-string,
* custom_flag?: non-empty-string
* }
*/
private function get_flag( $flag_code, $name, $slug, $locale ) {
$flags = array(
'flag' => PLL_Language::get_flag_informations( $flag_code ),
);
// Custom flags?
$directories = array(
PLL_LOCAL_DIR,
get_stylesheet_directory() . '/polylang',
get_template_directory() . '/polylang',
);
foreach ( $directories as $dir ) {
if ( is_readable( $file = "{$dir}/{$locale}.png" ) || is_readable( $file = "{$dir}/{$locale}.jpg" ) || is_readable( $file = "{$dir}/{$locale}.jpeg" ) || is_readable( $file = "{$dir}/{$locale}.svg" ) ) {
$flags['custom_flag'] = array(
'url' => content_url( '/' . str_replace( WP_CONTENT_DIR, '', $file ) ),
);
break;
}
}
/**
* Filters the custom flag information.
*
* @since 2.4
*
* @param array|null $flag {
* Information about the custom flag.
*
* @type string $url Flag url.
* @type string $src Optional, src attribute value if different of the url, for example if base64 encoded.
* @type int $width Optional, flag width in pixels.
* @type int $height Optional, flag height in pixels.
* }
* @param string $code Flag code.
*/
$flags['custom_flag'] = apply_filters( 'pll_custom_flag', empty( $flags['custom_flag'] ) ? null : $flags['custom_flag'], $flag_code );
if ( ! empty( $flags['custom_flag']['url'] ) ) {
if ( empty( $flags['custom_flag']['src'] ) ) {
$flags['custom_flag']['src'] = esc_url( set_url_scheme( $flags['custom_flag']['url'], 'relative' ) );
}
$flags['custom_flag']['url'] = esc_url_raw( $flags['custom_flag']['url'] );
} else {
unset( $flags['custom_flag'] );
}
/**
* Filters the flag title attribute.
* Defaults to the language name.
*
* @since 0.7
*
* @param string $title The flag title attribute.
* @param string $slug The language code.
* @param string $locale The language locale.
*/
$title = apply_filters( 'pll_flag_title', $name, $slug, $locale );
$return = array();
/**
* @var array{
* flag: array{
* url: string,
* src: string,
* width?: positive-int,
* height?: positive-int
* },
* custom_flag?: array{
* url: non-empty-string,
* src: non-empty-string,
* width?: positive-int,
* height?: positive-int
* }
* } $flags
*/
foreach ( $flags as $key => $flag ) {
$return[ "{$key}_url" ] = $flag['url'];
/**
* Filters the html markup of a flag.
*
* @since 1.0.2
*
* @param string $flag Html markup of the flag or empty string.
* @param string $slug Language code.
*/
$return[ $key ] = apply_filters(
'pll_get_flag',
PLL_Language::get_flag_html( $flag, $title, $name ),
$slug
);
}
/**
* @var array{
* flag_url: string,
* flag: string,
* custom_flag_url?: non-empty-string,
* custom_flag?: non-empty-string
* } $return
*/
return $return;
}
}

View File

@@ -0,0 +1,675 @@
<?php
/**
* @package Polylang
*/
/**
* A language object is made of two terms in 'language' and 'term_language' taxonomies.
* Manipulating only one object per language instead of two terms should make things easier.
*
* @since 1.2
* @immutable
*
* @phpstan-type LanguagePropData array{
* term_id: positive-int,
* term_taxonomy_id: positive-int,
* count: int<0, max>
* }
* @phpstan-type LanguageData array{
* term_props: array{
* language: LanguagePropData,
* }&array<non-empty-string, LanguagePropData>,
* name: non-empty-string,
* slug: non-empty-string,
* locale: non-empty-string,
* w3c: non-empty-string,
* flag_code: non-empty-string,
* term_group: int,
* is_rtl: int<0, 1>,
* facebook?: string,
* home_url: non-empty-string,
* search_url: non-empty-string,
* host: non-empty-string,
* flag_url: non-empty-string,
* flag: non-empty-string,
* custom_flag_url?: string,
* custom_flag?: string,
* page_on_front: int<0, max>,
* page_for_posts: int<0, max>,
* active: bool,
* fallbacks?: array<non-empty-string>,
* is_default: bool
* }
*/
class PLL_Language extends PLL_Language_Deprecated {
/**
* Language name. Ex: English.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $name;
/**
* Language code used in URL. Ex: en.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $slug;
/**
* Order of the language when displayed in a list of languages.
*
* @var int
*/
public $term_group;
/**
* ID of the term in 'language' taxonomy.
* Duplicated from `$this->term_props['language']['term_id'],
* but kept to facilitate the use of it.
*
* @var int
*
* @phpstan-var int<1, max>
*/
public $term_id;
/**
* WordPress language locale. Ex: en_US.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $locale;
/**
* 1 if the language is rtl, 0 otherwise.
*
* @var int
*
* @phpstan-var int<0, 1>
*/
public $is_rtl;
/**
* W3C locale.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $w3c;
/**
* Facebook locale.
*
* @var string
*/
public $facebook = '';
/**
* Home URL in this language.
*
* @var string
*
* @phpstan-var non-empty-string
*/
private $home_url;
/**
* Home URL to use in search forms.
*
* @var string
*
* @phpstan-var non-empty-string
*/
private $search_url;
/**
* Host corresponding to this language.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $host;
/**
* ID of the page on front in this language (set from pll_additional_language_data filter).
*
* @var int
*
* @phpstan-var int<0, max>
*/
public $page_on_front = 0;
/**
* ID of the page for posts in this language (set from pll_additional_language_data filter).
*
* @var int
*
* @phpstan-var int<0, max>
*/
public $page_for_posts = 0;
/**
* Code of the flag.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $flag_code;
/**
* URL of the flag. Always set to the main domain.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $flag_url;
/**
* HTML markup of the flag.
*
* @var string
*
* @phpstan-var non-empty-string
*/
public $flag;
/**
* URL of the custom flag if it exists. Always set to the main domain.
*
* @var string
*/
public $custom_flag_url = '';
/**
* HTML markup of the custom flag if it exists.
*
* @var string
*/
public $custom_flag = '';
/**
* Whether or not the language is active. Default `true`.
*
* @var bool
*/
public $active = true;
/**
* List of WordPress language locales. Ex: array( 'en_GB' ).
*
* @var string[]
*
* @phpstan-var list<non-empty-string>
*/
public $fallbacks = array();
/**
* Whether the language is the default one.
*
* @var bool
*/
public $is_default;
/**
* Stores language term properties (like term IDs and counts) for each language taxonomy (`language`,
* `term_language`, etc).
* This stores the values of the properties `$term_id` + `$term_taxonomy_id` + `$count` (`language`), `$tl_term_id`
* + `$tl_term_taxonomy_id` + `$tl_count` (`term_language`), and the `term_id` + `term_taxonomy_id` + `count` for
* other language taxonomies.
*
* @var array[] Array keys are language term names.
*
* @example array(
* 'language' => array(
* 'term_id' => 7,
* 'term_taxonomy_id' => 8,
* 'count' => 11,
* ),
* 'term_language' => array(
* 'term_id' => 11,
* 'term_taxonomy_id' => 12,
* 'count' => 6,
* ),
* 'foo_language' => array(
* 'term_id' => 33,
* 'term_taxonomy_id' => 34,
* 'count' => 0,
* ),
* )
*
* @phpstan-var array{
* language: LanguagePropData,
* }
* &array<non-empty-string, LanguagePropData>
*/
protected $term_props;
/**
* Constructor: builds a language object given the corresponding data.
*
* @since 1.2
* @since 3.4 Only accepts one argument.
*
* @param array $language_data {
* Language object properties stored as an array.
*
* @type array[] $term_props An array of language term properties. Array keys are language taxonomy names
* (`language` and `term_language` are mandatory), array values are arrays of
* language term properties (`term_id`, `term_taxonomy_id`, and `count`).
* @type string $name Language name. Ex: English.
* @type string $slug Language code used in URL. Ex: en.
* @type string $locale WordPress language locale. Ex: en_US.
* @type string $w3c W3C locale.
* @type string $flag_code Code of the flag.
* @type int $term_group Order of the language when displayed in a list of languages.
* @type int $is_rtl `1` if the language is rtl, `0` otherwise.
* @type string $facebook Optional. Facebook locale.
* @type string $home_url Home URL in this language.
* @type string $search_url Home URL to use in search forms.
* @type string $host Host corresponding to this language.
* @type string $flag_url URL of the flag.
* @type string $flag HTML markup of the flag.
* @type string $custom_flag_url Optional. URL of the custom flag if it exists.
* @type string $custom_flag Optional. HTML markup of the custom flag if it exists.
* @type int $page_on_front Optional. ID of the page on front in this language.
* @type int $page_for_posts Optional. ID of the page for posts in this language.
* @type bool $active Whether or not the language is active. Default `true`.
* @type string[] $fallbacks List of WordPress language locales. Ex: array( 'en_GB' ).
* @type bool $is_default Whether or not the language is the default one.
* }
*
* @phpstan-param LanguageData $language_data
*/
public function __construct( array $language_data ) {
foreach ( $language_data as $prop => $value ) {
$this->$prop = $value;
}
$this->term_id = $this->term_props['language']['term_id'];
}
/**
* Returns a language term property value (term ID, term taxonomy ID, or count).
*
* @since 3.4
*
* @param string $taxonomy_name Name of the taxonomy.
* @param string $prop_name Name of the property: 'term_taxonomy_id', 'term_id', 'count'.
* @return int
*
* @phpstan-param non-empty-string $taxonomy_name
* @phpstan-param 'term_taxonomy_id'|'term_id'|'count' $prop_name
* @phpstan-return int<0, max>
*/
public function get_tax_prop( $taxonomy_name, $prop_name ) {
return isset( $this->term_props[ $taxonomy_name ][ $prop_name ] ) ? $this->term_props[ $taxonomy_name ][ $prop_name ] : 0;
}
/**
* Returns the language term props for all content types.
*
* @since 3.4
*
* @param string $property Name of the field to return. An empty string to return them all.
* @return (int[]|int)[] Array keys are taxonomy names, array values depend of `$property`.
*
* @phpstan-param 'term_taxonomy_id'|'term_id'|'count'|'' $property
* @phpstan-return array<non-empty-string, (
* $property is non-empty-string ?
* (
* $property is 'count' ?
* int<0, max> :
* positive-int
* ) :
* LanguagePropData
* )>
*/
public function get_tax_props( $property = '' ) {
if ( empty( $property ) ) {
return $this->term_props;
}
$term_props = array();
foreach ( $this->term_props as $taxonomy_name => $props ) {
$term_props[ $taxonomy_name ] = $props[ $property ];
}
return $term_props;
}
/**
* Returns the flag information.
*
* @since 2.6
*
* @param string $code Flag code.
* @return array {
* Flag information.
*
* @type string $url Flag url.
* @type string $src Optional, src attribute value if different of the url, for example if base64 encoded.
* @type int $width Optional, flag width in pixels.
* @type int $height Optional, flag height in pixels.
* }
*
* @phpstan-return array{
* url: string,
* src: string,
* width?: positive-int,
* height?: positive-int
* }
*/
public static function get_flag_informations( $code ) {
$default_flag = array(
'url' => '',
'src' => '',
);
// Polylang builtin flags.
if ( ! empty( $code ) && is_readable( POLYLANG_DIR . ( $file = '/flags/' . $code . '.png' ) ) ) {
$default_flag['url'] = plugins_url( $file, POLYLANG_FILE );
// If base64 encoded flags are preferred.
if ( pll_get_constant( 'PLL_ENCODED_FLAGS', true ) ) {
$imagesize = getimagesize( POLYLANG_DIR . $file );
if ( is_array( $imagesize ) ) {
list( $default_flag['width'], $default_flag['height'] ) = $imagesize;
}
$file_contents = file_get_contents( POLYLANG_DIR . $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
$default_flag['src'] = 'data:image/png;base64,' . base64_encode( $file_contents ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
}
/**
* Filters flag information:
*
* @since 2.4
*
* @param array $flag {
* Information about the flag.
*
* @type string $url Flag url.
* @type string $src Optional, src attribute value if different of the url, for example if base64 encoded.
* @type int $width Optional, flag width in pixels.
* @type int $height Optional, flag height in pixels.
* }
* @param string $code Flag code.
*/
$flag = apply_filters( 'pll_flag', $default_flag, $code );
$flag['url'] = esc_url_raw( $flag['url'] );
if ( empty( $flag['src'] ) || ( $flag['src'] === $default_flag['src'] && $flag['url'] !== $default_flag['url'] ) ) {
$flag['src'] = esc_url( set_url_scheme( $flag['url'], 'relative' ) );
}
return $flag;
}
/**
* Returns HTML code for flag.
*
* @since 2.7
*
* @param array $flag Flag properties: src, width and height.
* @param string $title Optional title attribute.
* @param string $alt Optional alt attribute.
* @return string
*
* @phpstan-param array{
* src: string,
* width?: int|numeric-string,
* height?: int|numeric-string
* } $flag
*/
public static function get_flag_html( $flag, $title = '', $alt = '' ) {
if ( empty( $flag['src'] ) ) {
return '';
}
$alt_attr = empty( $alt ) ? '' : sprintf( ' alt="%s"', esc_attr( $alt ) );
$width_attr = empty( $flag['width'] ) ? '' : sprintf( ' width="%s"', (int) $flag['width'] );
$height_attr = empty( $flag['height'] ) ? '' : sprintf( ' height="%s"', (int) $flag['height'] );
$style = '';
$sizes = array_intersect_key( $flag, array_flip( array( 'width', 'height' ) ) );
if ( ! empty( $sizes ) ) {
array_walk(
$sizes,
function ( &$value, $key ) {
$value = sprintf( '%s: %dpx;', esc_attr( $key ), (int) $value );
}
);
$style = sprintf( ' style="%s"', implode( ' ', $sizes ) );
}
return sprintf(
'<img src="%s"%s%s%s%s />',
$flag['src'],
$alt_attr,
$width_attr,
$height_attr,
$style
);
}
/**
* Returns the html of the custom flag if any, or the default flag otherwise.
*
* @since 2.8
* @since 3.5.3 Added the `$alt` parameter.
*
* @param string $alt Whether or not the alternative text should be set. Accepts 'alt' and 'no-alt'.
*
* @return string
*
* @phpstan-param 'alt'|'no-alt' $alt
*/
public function get_display_flag( $alt = 'alt' ) {
$flag = empty( $this->custom_flag ) ? $this->flag : $this->custom_flag;
if ( 'alt' === $alt ) {
return $flag;
}
return (string) preg_replace( '/(?<=\salt=\")([^"]+)(?=\")/', '', $flag );
}
/**
* Returns the url of the custom flag if any, or the default flag otherwise.
*
* @since 2.8
*
* @return string
*/
public function get_display_flag_url() {
$flag_url = empty( $this->custom_flag_url ) ? $this->flag_url : $this->custom_flag_url;
/**
* Filters `flag_url` property.
*
* @since 3.4.4
*
* @param string $flag_url Flag URL.
* @param PLL_Language $language Current `PLL_language` instance.
*/
return apply_filters( 'pll_language_flag_url', $flag_url, $this );
}
/**
* Updates post and term count.
*
* @since 1.2
*
* @return void
*/
public function update_count() {
foreach ( $this->term_props as $taxonomy => $props ) {
wp_update_term_count( $props['term_taxonomy_id'], $taxonomy );
}
}
/**
* Returns the language locale.
* Converts WP locales to W3C valid locales for display.
*
* @since 1.8
*
* @param string $filter Either 'display' or 'raw', defaults to raw.
* @return string
*
* @phpstan-param 'display'|'raw' $filter
* @phpstan-return non-empty-string
*/
public function get_locale( $filter = 'raw' ) {
return 'display' === $filter ? $this->w3c : $this->locale;
}
/**
* Returns the values of this instance's properties, which can be filtered if required.
*
* @since 3.4
*
* @param string $context Whether or not properties should be filtered. Accepts `db` or `display`.
* Default to `display` which filters some properties.
*
* @return array Array of language object properties.
*
* @phpstan-return LanguageData
*/
public function to_array( $context = 'display' ) {
$language = get_object_vars( $this );
if ( 'db' !== $context ) {
$language['home_url'] = $this->get_home_url();
$language['search_url'] = $this->get_search_url();
}
/** @phpstan-var LanguageData $language */
return $language;
}
/**
* Converts current `PLL_language` into a `stdClass` object. Mostly used to allow dynamic properties.
*
* @since 3.4
*
* @return stdClass Converted `PLL_Language` object.
*/
public function to_std_class() {
return (object) $this->to_array();
}
/**
* Returns a predefined HTML flag.
*
* @since 3.4
*
* @param string $flag_code Flag code to render.
* @return string HTML code for the flag.
*/
public static function get_predefined_flag( $flag_code ) {
$flag = self::get_flag_informations( $flag_code );
return self::get_flag_html( $flag );
}
/**
* Returns language's home URL. Takes care to render it dynamically if no cache is allowed.
*
* @since 3.4
*
* @return string Language home URL.
*/
public function get_home_url() {
if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) || ! pll_get_constant( 'PLL_CACHE_HOME_URL', true ) ) {
/**
* Filters current `PLL_Language` instance `home_url` property.
*
* @since 3.4.4
*
* @param string $home_url The `home_url` prop.
* @param array $language Current Array of `PLL_Language` properties.
*/
return apply_filters( 'pll_language_home_url', $this->home_url, $this->to_array( 'db' ) );
}
return $this->home_url;
}
/**
* Returns language's search URL. Takes care to render it dynamically if no cache is allowed.
*
* @since 3.4
*
* @return string Language search URL.
*/
public function get_search_url() {
if ( ! pll_get_constant( 'PLL_CACHE_LANGUAGES', true ) || ! pll_get_constant( 'PLL_CACHE_HOME_URL', true ) ) {
/**
* Filters current `PLL_Language` instance `search_url` property.
*
* @since 3.4.4
*
* @param string $search_url The `search_url` prop.
* @param array $language Current Array of `PLL_Language` properties.
*/
return apply_filters( 'pll_language_search_url', $this->search_url, $this->to_array( 'db' ) );
}
return $this->search_url;
}
/**
* Returns the value of a language property.
* This is handy to get a property's value without worrying about triggering a deprecation warning or anything.
*
* @since 3.4
*
* @param string $property A property name. A composite value can be used for language term property values, in the
* form of `{language_taxonomy_name}:{property_name}` (see {@see PLL_Language::get_tax_prop()}
* for the possible values). Ex: `term_language:term_taxonomy_id`.
* @return string|int|bool|string[] The requested property for the language, `false` if the property doesn't exist.
*
* @phpstan-return (
* $property is 'slug' ? non-empty-string : string|int|bool|list<non-empty-string>
* )
*/
public function get_prop( $property ) {
// Deprecated property.
if ( $this->is_deprecated_term_property( $property ) ) {
return $this->get_deprecated_term_property( $property );
}
if ( $this->is_deprecated_url_property( $property ) ) {
return $this->get_deprecated_url_property( $property );
}
// Composite property like 'term_language:term_taxonomy_id'.
if ( preg_match( '/^(?<tax>.{1,32}):(?<field>term_id|term_taxonomy_id|count)$/', $property, $matches ) ) {
/** @var array{tax:non-empty-string, field:'term_id'|'term_taxonomy_id'|'count'} $matches */
return $this->get_tax_prop( $matches['tax'], $matches['field'] );
}
// Any other public property.
if ( isset( $this->$property ) ) {
return $this->$property;
}
return false;
}
}

View File

@@ -0,0 +1,355 @@
<?php
/**
* @package Polylang
*/
/**
* A class to easily manage licenses for Polylang Pro and addons
*
* @since 1.9
*/
class PLL_License {
/**
* Sanitized plugin name.
*
* @var string
*/
public $id;
/**
* Plugin name.
*
* @var string
*/
public $name;
/**
* License key.
*
* @var string
*/
public $license_key;
/**
* License data, obtained from the API request.
*
* @var stdClass|null
*/
public $license_data;
/**
* Main plugin file.
*
* @var string
*/
private $file;
/**
* Current plugin version.
*
* @var string
*/
private $version;
/**
* Plugin author.
*
* @var string
*/
private $author;
/**
* API url.
*
* @var string.
*/
private $api_url = 'https://polylang.pro';
/**
* Constructor
*
* @since 1.9
*
* @param string $file The plugin file.
* @param string $item_name The plugin name.
* @param string $version The plugin version.
* @param string $author Author name.
* @param string $api_url Optional url of the site managing the license.
*/
public function __construct( $file, $item_name, $version, $author, $api_url = null ) {
$this->id = sanitize_title( $item_name );
$this->file = $file;
$this->name = $item_name;
$this->version = $version;
$this->author = $author;
$this->api_url = empty( $api_url ) ? $this->api_url : $api_url;
$licenses = (array) get_option( 'polylang_licenses', array() );
$license = isset( $licenses[ $this->id ] ) && is_array( $licenses[ $this->id ] ) ? $licenses[ $this->id ] : array();
$this->license_key = ! empty( $license['key'] ) ? (string) $license['key'] : '';
if ( ! empty( $license['data'] ) ) {
$this->license_data = (object) $license['data'];
}
// Updater
$this->auto_updater();
// Register settings
add_filter( 'pll_settings_licenses', array( $this, 'settings' ) );
// Weekly schedule
if ( ! wp_next_scheduled( 'polylang_check_licenses' ) ) {
wp_schedule_event( time(), 'weekly', 'polylang_check_licenses' );
}
add_action( 'polylang_check_licenses', array( $this, 'check_license' ) );
}
/**
* Auto updater
*
* @since 1.9
*
* @return void
*/
public function auto_updater() {
$args = array(
'version' => $this->version,
'license' => $this->license_key,
'author' => $this->author,
'item_name' => $this->name,
);
// Setup the updater
new PLL_Plugin_Updater( $this->api_url, $this->file, $args );
}
/**
* Registers the licence in the Settings.
*
* @since 1.9
*
* @param PLL_License[] $items Array of objects allowing to manage a license.
* @return PLL_License[]
*/
public function settings( $items ) {
$items[ $this->id ] = $this;
return $items;
}
/**
* Activates the license key.
*
* @since 1.9
*
* @param string $license_key Activation key.
* @return PLL_License Updated PLL_License object.
*/
public function activate_license( $license_key ) {
$this->license_key = $license_key;
$this->api_request( 'activate_license' );
// Tell WordPress to look for updates.
delete_site_transient( 'update_plugins' );
return $this;
}
/**
* Deactivates the license key.
*
* @since 1.9
*
* @return PLL_License Updated PLL_License object.
*/
public function deactivate_license() {
$this->api_request( 'deactivate_license' );
return $this;
}
/**
* Checks if the license key is valid.
*
* @since 1.9
*
* @return void
*/
public function check_license() {
$this->api_request( 'check_license' );
}
/**
* Sends an api request to check, activate or deactivate the license
* Updates the licenses option according to the status
*
* @since 1.9
*
* @param string $request check_license | activate_license | deactivate_license
* @return void
*/
private function api_request( $request ) {
$licenses = get_option( 'polylang_licenses' );
if ( is_array( $licenses ) ) {
unset( $licenses[ $this->id ] );
} else {
$licenses = array();
}
unset( $this->license_data );
if ( ! empty( $this->license_key ) ) {
// Data to send in our API request
$api_params = array(
'edd_action' => $request,
'license' => $this->license_key,
'item_name' => urlencode( $this->name ),
'url' => home_url(),
);
// Call the API
$response = wp_remote_post(
$this->api_url,
array(
'timeout' => 3,
'sslverify' => false,
'body' => $api_params,
)
);
// Update the option only if we got a response
if ( is_wp_error( $response ) ) {
return;
}
// Save new license info
$licenses[ $this->id ] = array( 'key' => $this->license_key );
$data = (object) json_decode( wp_remote_retrieve_body( $response ) );
if ( isset( $data->license ) && 'deactivated' !== $data->license ) {
$licenses[ $this->id ]['data'] = $this->license_data = $data;
}
}
update_option( 'polylang_licenses', $licenses ); // FIXME called multiple times when saving all licenses
}
/**
* Get the html form field in a table row (one per license key) for display
*
* @since 2.7
*
* @return string
*/
public function get_form_field() {
if ( ! empty( $this->license_data ) ) {
$license = $this->license_data;
}
$class = 'license-null';
$message = '';
$out = sprintf(
'<td><label for="pll-licenses[%1$s]">%2$s</label></td>' .
'<td><input name="licenses[%1$s]" id="pll-licenses[%1$s]" type="password" value="%3$s" class="regular-text code" />',
esc_attr( $this->id ),
esc_attr( $this->name ),
esc_html( $this->license_key )
);
if ( ! empty( $license ) && is_object( $license ) ) {
$now = time();
$expiration = isset( $license->expires ) ? strtotime( $license->expires ) : false;
// Special case: the license expired after the last check
if ( $license->success && $expiration && $expiration < $now ) {
$license->success = false;
$license->error = 'expired';
}
if ( false === $license->success ) {
$class = 'notice-error notice-alt';
switch ( $license->error ) {
case 'expired':
$message = sprintf(
/* translators: %1$s is a date, %2$s is link start tag, %3$s is link end tag. */
esc_html__( 'Your license key expired on %1$s. Please %2$srenew your license key%3$s.', 'polylang' ),
esc_html( date_i18n( get_option( 'date_format' ), $expiration ) ),
sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ),
'</a>'
);
break;
case 'disabled':
case 'revoked':
$message = esc_html__( 'Your license key has been disabled.', 'polylang' );
break;
case 'missing':
$message = sprintf(
/* translators: %1$s is link start tag, %2$s is link end tag. */
esc_html__( 'Invalid license. Please %1$svisit your account page%2$s and verify it.', 'polylang' ),
sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ),
'</a>'
);
break;
case 'invalid':
case 'site_inactive':
$message = sprintf(
/* translators: %1$s is a product name, %2$s is link start tag, %3$s is link end tag. */
esc_html__( 'Your %1$s license key is not active for this URL. Please %2$svisit your account page%3$s to manage your license key URLs.', 'polylang' ),
esc_html( $this->name ),
sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ),
'</a>'
);
break;
case 'item_name_mismatch':
/* translators: %s is a product name */
$message = sprintf( esc_html__( 'This is not a %s license key.', 'polylang' ), esc_html( $this->name ) );
break;
case 'no_activations_left':
$message = sprintf(
/* translators: %1$s is link start tag, %2$s is link end tag */
esc_html__( 'Your license key has reached its activation limit. %1$sView possible upgrades%2$s now.', 'polylang' ),
sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ),
'</a>'
);
break;
}
} else {
$class = 'license-valid';
$out .= sprintf( '<button id="deactivate_%s" type="button" class="button button-secondary pll-deactivate-license">%s</button>', esc_attr( $this->id ), esc_html__( 'Deactivate', 'polylang' ) );
if ( 'lifetime' === $license->expires ) {
$message = esc_html__( 'The license key never expires.', 'polylang' );
} elseif ( $expiration > $now && $expiration - $now < ( DAY_IN_SECONDS * 30 ) ) {
$class = 'notice-warning notice-alt';
$message = sprintf(
/* translators: %1$s is a date, %2$s is link start tag, %3$s is link end tag. */
esc_html__( 'Your license key will expire soon! Precisely, it will expire on %1$s. %2$sRenew your license key today!%3$s', 'polylang' ),
esc_html( date_i18n( get_option( 'date_format' ), $expiration ) ),
sprintf( '<a href="%s" target="_blank">', 'https://polylang.pro/account/' ),
'</a>'
);
} else {
$message = sprintf(
/* translators: %s is a date */
esc_html__( 'Your license key expires on %s.', 'polylang' ),
esc_html( date_i18n( get_option( 'date_format' ), $expiration ) )
);
}
}
}
if ( ! empty( $message ) ) {
$out .= '<p>' . $message . '</p>';
}
return sprintf( '<tr id="pll-license-%s" class="%s">%s</tr>', esc_attr( $this->id ), $class, $out );
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* @package Polylang
*/
/**
* Links model for use when using one domain or subdomain per language.
*
* @since 2.0
*/
abstract class PLL_Links_Abstract_Domain extends PLL_Links_Permalinks {
/**
* Constructor.
*
* @since 2.0
*
* @param PLL_Model $model Instance of PLL_Model.
*/
public function __construct( &$model ) {
parent::__construct( $model );
// Avoid cross domain requests (mainly for custom fonts).
add_filter( 'content_url', array( $this, 'site_url' ) );
add_filter( 'theme_root_uri', array( $this, 'site_url' ) ); // The above filter is not sufficient with WPMU Domain Mapping.
add_filter( 'plugins_url', array( $this, 'site_url' ) );
add_filter( 'rest_url', array( $this, 'site_url' ) );
add_filter( 'upload_dir', array( $this, 'upload_dir' ) );
// Set the correct domain for each language.
add_filter( 'pll_language_flag_url', array( $this, 'site_url' ) );
}
/**
* Returns the language based on the language code in url.
*
* @since 1.2
* @since 2.0 Add the $url argument.
*
* @param string $url Optional, defaults to the current url.
* @return string Language slug.
*/
public function get_language_from_url( $url = '' ) {
if ( empty( $url ) ) {
$url = pll_get_requested_url();
}
$host = wp_parse_url( $url, PHP_URL_HOST );
$lang = array_search( $host, $this->get_hosts() );
return is_string( $lang ) ? $lang : '';
}
/**
* Modifies an url to use the domain associated to the current language.
*
* @since 1.8
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function site_url( $url ) {
$lang = $this->get_language_from_url();
$lang = $this->model->get_language( $lang );
return $this->add_language_to_link( $url, $lang );
}
/**
* Fixes the domain for the upload directory.
*
* @since 2.0.6
*
* @param array $uploads Array of information about the upload directory. @see wp_upload_dir().
* @return array
*/
public function upload_dir( $uploads ) {
$lang = $this->get_language_from_url();
$lang = $this->model->get_language( $lang );
$uploads['url'] = $this->add_language_to_link( $uploads['url'], $lang );
$uploads['baseurl'] = $this->add_language_to_link( $uploads['baseurl'], $lang );
return $uploads;
}
/**
* Adds home and search URLs to language data before the object is created.
*
* @since 3.4.1
*
* @param array $additional_data Array of language additional data.
* @param array $language Language data.
* @return array Language data with home and search URLs added.
*/
public function set_language_home_urls( $additional_data, $language ) {
$language = array_merge( $language, $additional_data );
$additional_data['search_url'] = $this->home_url( $language['slug'] );
$additional_data['home_url'] = $additional_data['search_url'];
return $additional_data;
}
/**
* Returns language home URL property according to the current domain.
*
* @since 3.4.4
*
* @param string $url Home URL.
* @param array $language Array of language props.
* @return string Filtered home URL.
*/
public function set_language_home_url( $url, $language ) {
return $this->home_url( $language['slug'] );
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @package Polylang
*/
/**
* Links model for the default permalinks
* for example mysite.com/?somevar=something&lang=en.
*
* @since 1.2
*/
class PLL_Links_Default extends PLL_Links_Model {
/**
* Tells this child class of PLL_Links_Model does not use pretty permalinks.
*
* @var bool
*/
public $using_permalinks = false;
/**
* Adds the language code in a url.
*
* @since 1.2
* @since 3.4 Accepts now a language slug.
*
* @param string $url The url to modify.
* @param PLL_Language|string|false $language Language object or slug.
* @return string The modified url.
*/
public function add_language_to_link( $url, $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
return empty( $language ) || ( $this->options['hide_default'] && $this->options['default_lang'] === $language ) ? $url : add_query_arg( 'lang', $language, $url );
}
/**
* Removes the language information from an url.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_language_from_link( $url ) {
return remove_query_arg( 'lang', $url );
}
/**
* Returns the link to the first page.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_paged_from_link( $url ) {
return remove_query_arg( 'paged', $url );
}
/**
* Returns the link to the paged page.
*
* @since 1.5
*
* @param string $url The url to modify.
* @param int $page The page number.
* @return string The modified url.
*/
public function add_paged_to_link( $url, $page ) {
return add_query_arg( array( 'paged' => $page ), $url );
}
/**
* Gets the language slug from the url if present.
*
* @since 1.2
* @since 2.0 Add the $url argument.
*
* @param string $url Optional, defaults to the current url.
* @return string Language slug.
*/
public function get_language_from_url( $url = '' ) {
if ( empty( $url ) ) {
$url = pll_get_requested_url();
}
$pattern = sprintf(
'#[?&]lang=(?<lang>%s)(?:$|&)#',
implode( '|', $this->model->get_languages_list( array( 'fields' => 'slug' ) ) )
);
return preg_match( $pattern, $url, $matches ) ? $matches['lang'] : ''; // $matches['lang'] is the slug of the requested language.
}
/**
* Returns the static front page url in the given language.
*
* @since 1.8
* @since 3.4 Accepts now an array of language properties.
*
* @param PLL_Language|array $language Language object or array of language properties.
* @return string The static front page url.
*/
public function front_page_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->to_array();
}
if ( $this->options['hide_default'] && $language['is_default'] ) {
return trailingslashit( $this->home );
}
$url = home_url( '/?page_id=' . $language['page_on_front'] );
return $this->options['force_lang'] ? $this->add_language_to_link( $url, $language['slug'] ) : $url;
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* @package Polylang
*/
/**
* Links model for use when the language code is added in the url as a directory
* for example mysite.com/en/something.
*
* @since 1.2
*/
class PLL_Links_Directory extends PLL_Links_Permalinks {
/**
* Relative path to the home url.
*
* @var string
*/
protected $home_relative;
/**
* Constructor.
*
* @since 1.2
*
* @param PLL_Model $model PLL_Model instance.
*/
public function __construct( &$model ) {
parent::__construct( $model );
$this->home_relative = home_url( '/', 'relative' );
}
/**
* Adds hooks for rewrite rules.
*
* @since 1.6
*
* @return void
*/
public function init() {
add_action( 'pll_prepare_rewrite_rules', array( $this, 'prepare_rewrite_rules' ) ); // Ensure it's hooked before `self::do_prepare_rewrite_rules()` is called.
parent::init();
}
/**
* Adds the language code in a url.
*
* @since 1.2
* @since 3.4 Accepts now a language slug.
*
* @param string $url The url to modify.
* @param PLL_Language|string|false $language Language object or slug.
* @return string The modified url.
*/
public function add_language_to_link( $url, $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
if ( ! empty( $language ) ) {
$base = $this->options['rewrite'] ? '' : 'language/';
$slug = $this->options['default_lang'] === $language && $this->options['hide_default'] ? '' : $base . $language . '/';
$root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : preg_replace( '#^https?://#', '://', $this->home . '/' . $this->root );
if ( false === strpos( $url, $new = $root . $slug ) ) {
$pattern = preg_quote( $root, '#' );
$pattern = '#' . $pattern . '#';
return preg_replace( $pattern, $new, $url, 1 ); // Only once.
}
}
return $url;
}
/**
* Returns the url without the language code.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_language_from_link( $url ) {
$languages = $this->model->get_languages_list(
array(
'hide_default' => $this->options['hide_default'],
'fields' => 'slug',
)
);
if ( ! empty( $languages ) ) {
$root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : preg_replace( '#^https?://#', '://', $this->home . '/' . $this->root );
$pattern = preg_quote( $root, '@' );
$pattern = '@' . $pattern . ( $this->options['rewrite'] ? '' : 'language/' ) . '(' . implode( '|', $languages ) . ')(([?#])|(/|$))@';
$url = preg_replace( $pattern, $root . '$3', $url );
}
return $url;
}
/**
* Returns the language based on the language code in the url.
*
* @since 1.2
* @since 2.0 Add the $url argument.
*
* @param string $url Optional, defaults to the current url.
* @return string The language slug.
*/
public function get_language_from_url( $url = '' ) {
if ( empty( $url ) ) {
$url = pll_get_requested_url();
}
$path = (string) wp_parse_url( $url, PHP_URL_PATH );
$root = ( false === strpos( $url, '://' ) ) ? $this->home_relative . $this->root : $this->home . '/' . $this->root;
$pattern = (string) wp_parse_url( $root . ( $this->options['rewrite'] ? '' : 'language/' ), PHP_URL_PATH );
$pattern = preg_quote( $pattern, '#' );
$pattern = '#^' . $pattern . '(' . implode( '|', $this->model->get_languages_list( array( 'fields' => 'slug' ) ) ) . ')(/|$)#';
return preg_match( $pattern, trailingslashit( $path ), $matches ) ? $matches[1] : ''; // $matches[1] is the slug of the requested language.
}
/**
* Returns the home url in a given language.
*
* @since 1.3.1
* @since 3.4 Accepts now a language slug.
*
* @param PLL_Language|string $language Language object or slug.
* @return string
*/
public function home_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
$base = $this->options['rewrite'] ? '' : 'language/';
$slug = $this->options['default_lang'] === $language && $this->options['hide_default'] ? '' : '/' . $this->root . $base . $language;
return trailingslashit( $this->home . $slug );
}
/**
* Prepares the rewrite rules filters.
*
* @since 0.8.1
* @since 3.5 Hooked to `pll_prepare_rewrite_rules` and remove `$pre` parameter.
*
* @return void
*/
public function prepare_rewrite_rules() {
/*
* Don't modify the rules if there is no languages created yet and make sure
* to add the filters only once and if all custom post types and taxonomies
* have been registered.
*/
if ( ! $this->model->has_languages() || ! did_action( 'wp_loaded' ) || ! self::$can_filter_rewrite_rules ) {
return;
}
foreach ( $this->get_rewrite_rules_filters_with_callbacks() as $rule => $callback ) {
add_filter( $rule, $callback );
}
}
/**
* The rewrite rules !
*
* Always make sure that the default language is at the end in case the language information is hidden for default language.
* Thanks to brbrbr http://wordpress.org/support/topic/plugin-polylang-rewrite-rules-not-correct.
*
* @since 0.8.1
*
* @param string[] $rules Rewrite rules.
* @return string[] Modified rewrite rules.
*/
public function rewrite_rules( $rules ) {
$filter = str_replace( '_rewrite_rules', '', current_filter() );
global $wp_rewrite;
$newrules = array();
$languages = $this->model->get_languages_list( array( 'fields' => 'slug' ) );
if ( $this->options['hide_default'] ) {
$languages = array_diff( $languages, array( $this->options['default_lang'] ) );
}
if ( ! empty( $languages ) ) {
$slug = $wp_rewrite->root . ( $this->options['rewrite'] ? '' : 'language/' ) . '(' . implode( '|', $languages ) . ')/';
}
// For custom post type archives.
$cpts = array_intersect( $this->model->get_translated_post_types(), get_post_types( array( '_builtin' => false ) ) );
$cpts = $cpts ? '#post_type=(' . implode( '|', $cpts ) . ')#' : '';
foreach ( $rules as $key => $rule ) {
if ( ! is_string( $rule ) || ! is_string( $key ) ) {
// Protection against a bug in Sendinblue for WooCommerce. See: https://wordpress.org/support/topic/bug-introduced-in-rewrite-rules/
continue;
}
// Special case for translated post types and taxonomies to allow canonical redirection.
if ( $this->options['force_lang'] && in_array( $filter, array_merge( $this->model->get_translated_post_types(), $this->model->get_translated_taxonomies() ) ) ) {
/**
* Filters the rewrite rules to modify.
*
* @since 1.9.1
*
* @param bool $modify Whether to modify or not the rule, defaults to true.
* @param array $rule Original rewrite rule.
* @param string $filter Current set of rules being modified.
* @param string|bool $archive Custom post post type archive name or false if it is not a cpt archive.
*/
if ( isset( $slug ) && apply_filters( 'pll_modify_rewrite_rule', true, array( $key => $rule ), $filter, false ) ) {
$newrules[ $slug . str_replace( $wp_rewrite->root, '', ltrim( $key, '^' ) ) ] = str_replace(
array( '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]', '?' ),
array( '[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '?lang=$matches[1]&' ),
$rule
); // Should be enough!
}
$newrules[ $key ] = $rule;
}
// Rewrite rules filtered by language.
elseif ( in_array( $filter, $this->always_rewrite ) || in_array( $filter, $this->model->get_filtered_taxonomies() ) || ( $cpts && preg_match( $cpts, $rule, $matches ) && ! strpos( $rule, 'name=' ) ) || ( 'rewrite_rules_array' != $filter && $this->options['force_lang'] ) ) {
/** This filter is documented in include/links-directory.php */
if ( apply_filters( 'pll_modify_rewrite_rule', true, array( $key => $rule ), $filter, empty( $matches[1] ) ? false : $matches[1] ) ) {
if ( isset( $slug ) ) {
$newrules[ $slug . str_replace( $wp_rewrite->root, '', ltrim( $key, '^' ) ) ] = str_replace(
array( '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '[1]', '?' ),
array( '[9]', '[8]', '[7]', '[6]', '[5]', '[4]', '[3]', '[2]', '?lang=$matches[1]&' ),
$rule
); // Should be enough!
}
if ( $this->options['hide_default'] ) {
$newrules[ $key ] = str_replace( '?', '?lang=' . $this->options['default_lang'] . '&', $rule );
}
} else {
$newrules[ $key ] = $rule;
}
}
// Unmodified rules.
else {
$newrules[ $key ] = $rule;
}
}
// The home rewrite rule.
if ( 'root' == $filter && isset( $slug ) ) {
$newrules[ $slug . '?$' ] = $wp_rewrite->index . '?lang=$matches[1]';
}
return $newrules;
}
/**
* Removes hooks to filter rewrite rules, called when switching blog @see {PLL_Base::switch_blog()}.
* See `self::prepare_rewrite_rules()` for added hooks.
*
* @since 3.5
*
* @return void
*/
public function remove_filters() {
parent::remove_filters();
foreach ( $this->get_rewrite_rules_filters_with_callbacks() as $rule => $callback ) {
remove_filter( $rule, $callback );
}
}
/**
* Returns *all* rewrite rules filters with their associated callbacks.
*
* @since 3.5
*
* @return callable[] Array of hook names as key and callbacks as values.
*/
protected function get_rewrite_rules_filters_with_callbacks() {
$filters = array(
'rewrite_rules_array' => array( $this, 'rewrite_rules' ), // Needed for post type archives.
);
foreach ( $this->get_rewrite_rules_filters() as $type ) {
$filters[ $type . '_rewrite_rules' ] = array( $this, 'rewrite_rules' );
}
return $filters;
}
}

View File

@@ -0,0 +1,121 @@
<?php
/**
* @package Polylang
*/
/**
* Links model for use when using one domain per language
* for example mysite.com/something and mysite.fr/quelquechose.
*
* @since 1.2
*/
class PLL_Links_Domain extends PLL_Links_Abstract_Domain {
/**
* An array with language code as keys and the host as values.
*
* @var string[]
*/
protected $hosts;
/**
* Constructor.
*
* @since 1.8
*
* @param object $model PLL_Model instance.
*/
public function __construct( &$model ) {
parent::__construct( $model );
$this->hosts = $this->get_hosts();
// Filters the site url (mainly to get the correct login form).
add_filter( 'site_url', array( $this, 'site_url' ) );
}
/**
* Switches the primary domain to a secondary domain in the url.
*
* @since 1.2
* @since 3.4 Accepts now a language slug.
*
* @param string $url The url to modify.
* @param PLL_Language|string|false $language Language object or slug.
* @return string The modified url.
*/
public function add_language_to_link( $url, $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
if ( ! empty( $language ) && ! empty( $this->hosts[ $language ] ) ) {
$url = preg_replace( '#://(' . wp_parse_url( $this->home, PHP_URL_HOST ) . ')($|/.*)#', '://' . $this->hosts[ $language ] . '$2', $url );
}
return $url;
}
/**
* Returns the url with the primary domain.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_language_from_link( $url ) {
if ( ! empty( $this->hosts ) ) {
$url = preg_replace( '#://(' . implode( '|', $this->hosts ) . ')($|/.*)#', '://' . wp_parse_url( $this->home, PHP_URL_HOST ) . '$2', $url );
}
return $url;
}
/**
* Returns the home url in a given language.
*
* @since 1.3.1
* @since 3.4 Accepts now a language slug.
*
* @param PLL_Language|string $language Language object or slug.
* @return string
*/
public function home_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
return trailingslashit( empty( $this->options['domains'][ $language ] ) ? $this->home : $this->options['domains'][ $language ] );
}
/**
* Get the hosts managed on the website.
*
* @since 1.5
*
* @return string[] List of hosts.
*/
public function get_hosts() {
$hosts = array();
foreach ( $this->options['domains'] as $lang => $domain ) {
$host = wp_parse_url( $domain, PHP_URL_HOST );
if ( ! is_string( $host ) ) {
continue;
}
// The function idn_to_ascii() is much faster than the WordPress method.
if ( function_exists( 'idn_to_ascii' ) && defined( 'INTL_IDNA_VARIANT_UTS46' ) ) {
$hosts[ $lang ] = idn_to_ascii( $host, 0, INTL_IDNA_VARIANT_UTS46 );
} elseif ( class_exists( 'WpOrg\Requests\IdnaEncoder' ) ) {
// Since WP 6.2.
$hosts[ $lang ] = \WpOrg\Requests\IdnaEncoder::encode( $host );
} else {
// Backward compatibility with WP < 6.2.
$hosts[ $lang ] = Requests_IDNAEncoder::encode( $host );
}
}
return $hosts;
}
}

View File

@@ -0,0 +1,264 @@
<?php
/**
* @package Polylang
*/
/**
* Links model abstract class.
*
* @since 1.5
*/
abstract class PLL_Links_Model {
/**
* True if the child class uses pretty permalinks, false otherwise.
*
* @var bool
*/
public $using_permalinks;
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Stores the home url before it is filtered.
*
* @var string
*/
public $home;
/**
* Whether rewrite rules can be filtered or not. Default to `false`.
*
* @var boolean
*/
protected static $can_filter_rewrite_rules = false;
/**
* Constructor.
*
* @since 1.5
*
* @param PLL_Model $model PLL_Model instance.
*/
public function __construct( &$model ) {
$this->model = &$model;
$this->options = &$model->options;
$this->home = home_url();
// Hooked with normal priority because it needs to be run after static pages is set in language data. Must be done early (before languages objects are created).
add_filter( 'pll_additional_language_data', array( $this, 'set_language_home_urls' ), 10, 2 );
// Adds our domains or subdomains to allowed hosts for safe redirection.
add_filter( 'allowed_redirect_hosts', array( $this, 'allowed_redirect_hosts' ) );
// Allows secondary domains for home and search URLs in `PLL_Language`.
add_filter( 'pll_language_home_url', array( $this, 'set_language_home_url' ), 10, 2 );
add_filter( 'pll_language_search_url', array( $this, 'set_language_search_url' ), 10, 2 );
if ( did_action( 'pll_init' ) ) {
$this->init();
} else {
add_action( 'pll_init', array( $this, 'init' ) );
}
}
/**
* Initializes the links model.
* Does nothing by default.
*
* @since 3.5
*
* @return void
*/
public function init() {}
/**
* Adds the language code in url.
*
* @since 1.2
* @since 3.4 Accepts now a language slug.
*
* @param string $url The url to modify.
* @param PLL_Language|string|false $lang Language object or slug.
* @return string The modified url.
*/
abstract public function add_language_to_link( $url, $lang );
/**
* Returns the url without language code.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
abstract public function remove_language_from_link( $url );
/**
* Returns the link to the first page.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
abstract public function remove_paged_from_link( $url );
/**
* Returns the link to a paged page.
*
* @since 1.5
*
* @param string $url The url to modify.
* @param int $page The page number.
* @return string The modified url.
*/
abstract public function add_paged_to_link( $url, $page );
/**
* Returns the language based on the language code in the url.
*
* @since 1.2
* @since 2.0 Add the $url argument.
*
* @param string $url Optional, defaults to the current url.
* @return string The language slug.
*/
abstract public function get_language_from_url( $url = '' );
/**
* Returns the static front page url in a given language.
*
* @since 1.8
* @since 3.4 Accepts now an array of language properties.
*
* @param PLL_Language|array $language Language object or array of language properties.
* @return string The static front page url.
*/
abstract public function front_page_url( $language );
/**
* Changes the language code in url.
*
* @since 1.5
*
* @param string $url The url to modify.
* @param PLL_Language $lang The language object.
* @return string The modified url.
*/
public function switch_language_in_link( $url, $lang ) {
$url = $this->remove_language_from_link( $url );
return $this->add_language_to_link( $url, $lang );
}
/**
* Get the hosts managed on the website.
*
* @since 1.5
*
* @return string[] The list of hosts.
*/
public function get_hosts() {
return array( wp_parse_url( $this->home, PHP_URL_HOST ) );
}
/**
* Returns the home url in a given language.
*
* @since 1.3.1
* @since 3.4 Accepts now a language slug.
*
* @param PLL_Language|string $language Language object or slug.
* @return string
*/
public function home_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
$url = trailingslashit( $this->home );
return $this->options['hide_default'] && $language === $this->options['default_lang'] ? $url : $this->add_language_to_link( $url, $language );
}
/**
* Adds home and search URLs to language data before the object is created.
*
* @since 3.4
*
* @param array $additional_data Array of language additional data.
* @param array $language Language data.
* @return array Language data with home and search URLs added.
*/
public function set_language_home_urls( $additional_data, $language ) {
$language = array_merge( $language, $additional_data );
$additional_data['search_url'] = $this->set_language_search_url( '', $language );
$additional_data['home_url'] = $this->set_language_home_url( '', $language );
return $additional_data;
}
/**
* Adds our domains or subdomains to allowed hosts for safe redirect.
*
* @since 1.4.3
*
* @param string[] $hosts Allowed hosts.
* @return string[] Modified list of allowed hosts.
*/
public function allowed_redirect_hosts( $hosts ) {
return array_unique( array_merge( $hosts, array_values( $this->get_hosts() ) ) );
}
/**
* Returns language home URL property according to the current domain.
*
* @since 3.4.4
*
* @param string $url Home URL.
* @param array $language Array of language props.
* @return string Filtered home URL.
*/
public function set_language_home_url( $url, $language ) {
if ( empty( $language['page_on_front'] ) || $this->options['redirect_lang'] ) {
return $this->home_url( $language['slug'] );
}
return $this->front_page_url( $language );
}
/**
* Returns language search URL property according to the current domain.
*
* @since 3.4.4
*
* @param string $url Search URL.
* @param array $language Array of language props.
* @return string Filtered search URL.
*/
public function set_language_search_url( $url, $language ) {
return $this->home_url( $language['slug'] );
}
/**
* Used to remove hooks in child classes, called when switching blog @see {PLL_Base::switch_blog()}.
* Does nothing by default.
*
* @since 3.5
*
* @return void
*/
public function remove_filters() {
self::$can_filter_rewrite_rules = false;
}
}

View File

@@ -0,0 +1,219 @@
<?php
/**
* @package Polylang
*/
/**
* Links model base class when using pretty permalinks.
*
* @since 1.6
*/
abstract class PLL_Links_Permalinks extends PLL_Links_Model {
/**
* Tells this child class of PLL_Links_Model is for pretty permalinks.
*
* @var bool
*/
public $using_permalinks = true;
/**
* The name of the index file which is the entry point to all requests.
* We need this before the global $wp_rewrite is created.
* Also hardcoded in WP_Rewrite.
*
* @var string
*/
protected $index = 'index.php';
/**
* The prefix for all permalink structures.
*
* @var string
*/
protected $root;
/**
* Whether to add trailing slashes.
*
* @var bool
*/
protected $use_trailing_slashes;
/**
* The name of the rewrite rules to always modify.
*
* @var string[]
*/
protected $always_rewrite = array( 'date', 'root', 'comments', 'search', 'author' );
/**
* Constructor.
*
* @since 1.8
*
* @param PLL_Model $model PLL_Model instance.
*/
public function __construct( &$model ) {
parent::__construct( $model );
// Inspired by WP_Rewrite.
$permalink_structure = get_option( 'permalink_structure' );
$this->root = preg_match( '#^/*' . $this->index . '#', $permalink_structure ) ? $this->index . '/' : '';
$this->use_trailing_slashes = ( '/' == substr( $permalink_structure, -1, 1 ) );
}
/**
* Initializes permalinks.
*
* @since 3.5
*
* @return void
*/
public function init() {
parent::init();
if ( did_action( 'wp_loaded' ) ) {
$this->do_prepare_rewrite_rules();
} else {
add_action( 'wp_loaded', array( $this, 'do_prepare_rewrite_rules' ), 9 ); // Just before WordPress callback `WP_Rewrite::flush_rules()`.
}
}
/**
* Fires our own action telling Polylang plugins
* and third parties are able to prepare rewrite rules.
*
* @since 3.5
*
* @return void
*/
public function do_prepare_rewrite_rules() {
self::$can_filter_rewrite_rules = true;
/**
* Tells when Polylang is able to prepare rewrite rules filters.
* Action fired right after `wp_loaded` and just before WordPress `WP_Rewrite::flush_rules()` callback.
*
* @since 3.5
*
* @param PLL_Links_Permalinks $links Current links object.
*/
do_action( 'pll_prepare_rewrite_rules', $this );
}
/**
* Returns the link to the first page when using pretty permalinks.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_paged_from_link( $url ) {
/**
* Filters an url after the paged part has been removed.
*
* @since 2.0.6
*
* @param string $modified_url The link to the first page.
* @param string $original_url The link to the original paged page.
*/
return apply_filters( 'pll_remove_paged_from_link', preg_replace( '#/page/[0-9]+/?#', $this->use_trailing_slashes ? '/' : '', $url ), $url );
}
/**
* Returns the link to the paged page when using pretty permalinks.
*
* @since 1.5
*
* @param string $url The url to modify.
* @param int $page The page number.
* @return string The modified url.
*/
public function add_paged_to_link( $url, $page ) {
/**
* Filters an url after the paged part has been added.
*
* @since 2.0.6
*
* @param string $modified_url The link to the paged page.
* @param string $original_url The link to the original first page.
* @param int $page The page number.
*/
return apply_filters( 'pll_add_paged_to_link', user_trailingslashit( trailingslashit( $url ) . 'page/' . $page, 'paged' ), $url, $page );
}
/**
* Returns the home url in a given language.
*
* @since 1.3.1
* @since 3.4 Accepts now a language slug.
*
* @param PLL_Language|string $language Language object or slug.
* @return string
*/
public function home_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
return trailingslashit( parent::home_url( $language ) );
}
/**
* Returns the static front page url.
*
* @since 1.8
* @since 3.4 Accepts now an array of language properties.
*
* @param PLL_Language|array $language Language object or array of language properties.
* @return string The static front page url.
*/
public function front_page_url( $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->to_array();
}
if ( $this->options['hide_default'] && $language['is_default'] ) {
return trailingslashit( $this->home );
}
$url = home_url( $this->root . get_page_uri( $language['page_on_front'] ) );
$url = $this->use_trailing_slashes ? trailingslashit( $url ) : untrailingslashit( $url );
return $this->options['force_lang'] ? $this->add_language_to_link( $url, $language['slug'] ) : $url;
}
/**
* Prepares rewrite rules filters.
*
* @since 1.6
*
* @return string[]
*/
public function get_rewrite_rules_filters() {
// Make sure that we have the right post types and taxonomies.
$types = array_values( array_merge( $this->model->get_translated_post_types(), $this->model->get_translated_taxonomies(), $this->model->get_filtered_taxonomies() ) );
$types = array_merge( $this->always_rewrite, $types );
/**
* Filters the list of rewrite rules filters to be used by Polylang.
*
* @since 0.8.1
*
* @param array $types The list of filters (without '_rewrite_rules' at the end).
*/
return apply_filters( 'pll_rewrite_rules', $types );
}
/**
* Removes hooks to filter rewrite rules, called when switching blog @see {PLL_Base::switch_blog()}.
*
* @since 3.5
*
* @return void
*/
public function remove_filters() {
parent::remove_filters();
remove_all_actions( 'pll_prepare_rewrite_rules' );
}
}

View File

@@ -0,0 +1,92 @@
<?php
/**
* @package Polylang
*/
/**
* Links model for use when the language code is added in the url as a subdomain
* for example en.mysite.com/something.
*
* @since 1.2
*/
class PLL_Links_Subdomain extends PLL_Links_Abstract_Domain {
/**
* Stores whether the home url includes www. or not.
* Either '://' or '://www.'.
*
* @var string
*/
protected $www;
/**
* Constructor.
*
* @since 1.7.4
*
* @param PLL_Model $model Instance of PLL_Model.
*/
public function __construct( &$model ) {
parent::__construct( $model );
$this->www = ( false === strpos( $this->home, '://www.' ) ) ? '://' : '://www.';
}
/**
* Adds the language code in a url.
*
* @since 1.2
* @since 3.4 Accepts now a language slug.
*
* @param string $url The url to modify.
* @param PLL_Language|string|false $language Language object or slug.
* @return string The modified url.
*/
public function add_language_to_link( $url, $language ) {
if ( $language instanceof PLL_Language ) {
$language = $language->slug;
}
if ( ! empty( $language ) && false === strpos( $url, '://' . $language . '.' ) ) {
$url = $this->options['default_lang'] === $language && $this->options['hide_default'] ? $url : str_replace( $this->www, '://' . $language . '.', $url );
}
return $url;
}
/**
* Returns the url without the language code.
*
* @since 1.2
*
* @param string $url The url to modify.
* @return string The modified url.
*/
public function remove_language_from_link( $url ) {
$languages = $this->model->get_languages_list(
array(
'hide_default' => $this->options['hide_default'],
'fields' => 'slug',
)
);
if ( ! empty( $languages ) ) {
$url = preg_replace( '#://(' . implode( '|', $languages ) . ')\.#', $this->www, $url );
}
return $url;
}
/**
* Get the hosts managed on the website.
*
* @since 1.5
*
* @return string[] The list of hosts.
*/
public function get_hosts() {
$hosts = array();
foreach ( $this->model->get_languages_list() as $lang ) {
$host = wp_parse_url( $this->home_url( $lang ), PHP_URL_HOST );
$hosts[ $lang->slug ] = $host ? $host : '';
}
return $hosts;
}
}

View File

@@ -0,0 +1,71 @@
<?php
/**
* @package Polylang
*/
/**
* Manages links related functions
*
* @since 1.2
*/
class PLL_Links {
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
public $links_model;
/**
* Current language (used to filter the content).
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links_model = &$polylang->links_model;
$this->model = &$polylang->model;
$this->options = &$polylang->options;
}
/**
* Returns the home url in the requested language.
*
* @since 1.3
*
* @param PLL_Language|string $language The language.
* @param bool $is_search Optional, whether we need the home url for a search form, defaults to false.
* @return string
*/
public function get_home_url( $language, $is_search = false ) {
if ( ! $language instanceof PLL_Language ) {
$language = $this->model->get_language( $language );
}
if ( empty( $language ) ) {
return home_url( '/' );
}
return $is_search ? $language->get_search_url() : $language->get_home_url();
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @package Polylang
*/
/**
* Manages strings translations storage
*
* @since 1.2
* @since 2.1 Stores the strings in a post meta instead of post content to avoid unserialize issues (See #63)
* @since 3.4 Stores the strings into language taxonomy term meta instead of a post meta.
*/
class PLL_MO extends MO {
/**
* Writes the strings into a term meta.
*
* @since 1.2
*
* @param PLL_Language $lang The language in which we want to export strings.
* @return void
*/
public function export_to_db( $lang ) {
/*
* It would be convenient to store the whole object, but it would take a huge space in DB.
* So let's keep only the strings in an array.
* The strings are slashed to avoid breaking slashed strings in update_term_meta.
* @see https://codex.wordpress.org/Function_Reference/update_post_meta#Character_Escaping.
*/
$strings = array();
foreach ( $this->entries as $entry ) {
if ( '' !== $entry->singular ) {
$strings[] = wp_slash( array( $entry->singular, $this->translate( $entry->singular ) ) );
}
}
update_term_meta( $lang->term_id, '_pll_strings_translations', $strings );
}
/**
* Reads a PLL_MO object from the term meta.
*
* @since 1.2
* @since 3.4 Reads a PLL_MO from the term meta.
*
* @param PLL_Language $lang The language in which we want to get strings.
* @return void
*/
public function import_from_db( $lang ) {
$this->set_header( 'Language', $lang->slug );
$strings = get_term_meta( $lang->term_id, '_pll_strings_translations', true );
if ( empty( $strings ) || ! is_array( $strings ) ) {
return;
}
foreach ( $strings as $msg ) {
$entry = $this->make_entry( $msg[0], $msg[1] );
if ( '' !== $entry->singular ) {
$this->add_entry( $entry );
}
}
}
/**
* Deletes a string
*
* @since 2.9
*
* @param string $string The source string to remove from the translations.
* @return void
*/
public function delete_entry( $string ) {
unset( $this->entries[ $string ] );
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
<?php
/**
* @package Polylang
*/
/**
* Manages custom menus translations
* Common to admin and frontend for the customizer
*
* @since 1.7.7
*/
class PLL_Nav_Menu {
/**
* Stores the plugin options.
*
* @var array
*/
public $options;
/**
* @var PLL_Model
*/
public $model;
/**
* Theme name.
*
* @var string
*/
protected $theme;
/**
* Array of menu ids in a given language used when auto add pages to menus.
*
* @var int[]
*/
protected $auto_add_menus = array();
/**
* Constructor: setups filters and actions.
*
* @since 1.7.7
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->options = &$polylang->options;
$this->theme = get_option( 'stylesheet' );
add_filter( 'wp_setup_nav_menu_item', array( $this, 'wp_setup_nav_menu_item' ) );
// Integration with WP customizer
add_action( 'customize_register', array( $this, 'create_nav_menu_locations' ), 5 );
// Filter _wp_auto_add_pages_to_menu by language
add_action( 'transition_post_status', array( $this, 'auto_add_pages_to_menu' ), 5, 3 ); // before _wp_auto_add_pages_to_menu
}
/**
* Assigns the title and label to the language switcher menu items
*
* @since 2.6
*
* @param stdClass $item Menu item.
* @return stdClass
*/
public function wp_setup_nav_menu_item( $item ) {
if ( isset( $item->url ) && '#pll_switcher' === $item->url ) {
$item->post_title = __( 'Languages', 'polylang' );
$item->type_label = __( 'Language switcher', 'polylang' );
}
return $item;
}
/**
* Create temporary nav menu locations ( one per location and per language ) for all non-default language
* to do only one time
*
* @since 1.2
*
* @return void
*/
public function create_nav_menu_locations() {
static $once;
global $_wp_registered_nav_menus;
$arr = array();
if ( isset( $_wp_registered_nav_menus ) && ! $once ) {
foreach ( $_wp_registered_nav_menus as $loc => $name ) {
foreach ( $this->model->get_languages_list() as $lang ) {
$arr[ $this->combine_location( $loc, $lang ) ] = $name . ' ' . $lang->name;
}
}
$_wp_registered_nav_menus = $arr;
$once = true;
}
}
/**
* Creates a temporary nav menu location from a location and a language
*
* @since 1.8
*
* @param string $loc Nav menu location.
* @param PLL_Language $lang Language object.
* @return string
*/
public function combine_location( $loc, $lang ) {
return $loc . ( strpos( $loc, '___' ) || $lang->is_default ? '' : '___' . $lang->slug );
}
/**
* Get nav menu locations and language from a temporary location.
*
* @since 1.8
*
* @param string $loc Temporary location.
* @return string[] {
* @type string $location Nav menu location.
* @type string $lang Language code.
* }
*/
public function explode_location( $loc ) {
$infos = explode( '___', $loc );
if ( 1 == count( $infos ) ) {
$infos[] = $this->options['default_lang'];
}
return array_combine( array( 'location', 'lang' ), $infos );
}
/**
* Filters the option nav_menu_options for auto added pages to menu.
*
* @since 0.9.4
*
* @param array $options Options stored in the option nav_menu_options.
* @return array Modified options.
*/
public function nav_menu_options( $options ) {
$options['auto_add'] = array_intersect( $options['auto_add'], $this->auto_add_menus );
return $options;
}
/**
* Filters _wp_auto_add_pages_to_menu by language.
*
* @since 0.9.4
*
* @param string $new_status Transition to this post status.
* @param string $old_status Previous post status.
* @param WP_Post $post Post object.
* @return void
*/
public function auto_add_pages_to_menu( $new_status, $old_status, $post ) {
if ( 'publish' != $new_status || 'publish' == $old_status || 'page' != $post->post_type || ! empty( $post->post_parent ) ) {
return;
}
if ( ! empty( $this->options['nav_menus'][ $this->theme ] ) ) {
$lang = $this->model->post->get_language( $post->ID );
$lang = empty( $lang ) ? $this->options['default_lang'] : $lang->slug; // If the page has no language yet, the default language will be assigned
// Get all the menus in the page language
foreach ( $this->options['nav_menus'][ $this->theme ] as $loc ) {
if ( ! empty( $loc[ $lang ] ) ) {
$this->auto_add_menus[] = $loc[ $lang ];
}
}
add_filter( 'option_nav_menu_options', array( $this, 'nav_menu_options' ) );
}
}
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* @package Polylang
*/
/**
* It is best practice that plugins do nothing before plugins_loaded is fired
* so it is what Polylang intends to do
* but some plugins load their text domain as soon as loaded, thus before plugins_loaded is fired
* this class differs text domain loading until the language is defined
* either in a plugins_loaded action or in a wp action ( when the language is set from content on frontend )
*
* @since 1.2
*/
class PLL_OLT_Manager {
/**
* Singleton instance
*
* @var PLL_OLT_Manager|null
*/
protected static $instance;
/**
* Stores the default site locale before it is modified.
*
* @var string|null
*/
protected $default_locale;
/**
* Stores all loaded text domains and mo files.
*
* @var string[][]
*/
protected $list_textdomains = array();
/**
* Stores post types an taxonomies labels to translate.
*
* @var string[][]
*/
public $labels = array();
/**
* Constructor: setups relevant filters
*
* @since 1.2
*/
public function __construct() {
// Allows Polylang to be the first plugin loaded ;-)
add_filter( 'pre_update_option_active_plugins', array( $this, 'make_polylang_first' ) );
add_filter( 'pre_update_option_active_sitewide_plugins', array( $this, 'make_polylang_first' ) );
// Overriding load text domain only on front since WP 4.7.
if ( is_admin() && ! Polylang::is_ajax_on_front() ) {
return;
}
// Saves the default locale before we start any language manipulation
$this->default_locale = get_locale();
// Filters for text domain management
add_filter( 'load_textdomain_mofile', array( $this, 'load_textdomain_mofile' ), 10, 2 );
add_filter( 'gettext', array( $this, 'gettext' ), 10, 3 );
add_filter( 'gettext_with_context', array( $this, 'gettext_with_context' ), 10, 4 );
// Loads text domains
add_action( 'pll_language_defined', array( $this, 'load_textdomains' ), 2 ); // After PLL_Frontend::pll_language_defined
add_action( 'pll_no_language_defined', array( $this, 'load_textdomains' ) );
}
/**
* Access to the single instance of the class
*
* @since 1.7
*
* @return PLL_OLT_Manager
*/
public static function instance() {
if ( empty( self::$instance ) ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Loads text domains
*
* @since 0.1
*
* @return void
*/
public function load_textdomains() {
// Our load_textdomain_mofile filter has done its job. let's remove it before calling load_textdomain
remove_filter( 'load_textdomain_mofile', array( $this, 'load_textdomain_mofile' ) );
remove_filter( 'gettext', array( $this, 'gettext' ) );
remove_filter( 'gettext_with_context', array( $this, 'gettext_with_context' ) );
$new_locale = get_locale();
// Don't try to save time for en_US as some users have theme written in another language
// Now we can load all overridden text domains with the right language
if ( ! empty( $this->list_textdomains ) ) {
foreach ( $this->list_textdomains as $textdomain ) {
// Since WP 4.6, plugins translations are first loaded from wp-content/languages
if ( ! load_textdomain( $textdomain['domain'], str_replace( "{$this->default_locale}.mo", "$new_locale.mo", $textdomain['mo'] ) ) ) {
// Since WP 3.5 themes may store languages files in /wp-content/languages/themes
if ( ! load_textdomain( $textdomain['domain'], WP_LANG_DIR . "/themes/{$textdomain['domain']}-$new_locale.mo" ) ) {
// Since WP 3.7 plugins may store languages files in /wp-content/languages/plugins
load_textdomain( $textdomain['domain'], WP_LANG_DIR . "/plugins/{$textdomain['domain']}-$new_locale.mo" );
}
}
}
}
// First remove taxonomies and post_types labels that we don't need to translate
$taxonomies = get_taxonomies( array( '_pll' => true ) );
$post_types = get_post_types( array( '_pll' => true ) );
// We don't need to translate core taxonomies and post types labels when setting the language from the url
// As they will be translated when registered the second time
if ( ! did_action( 'setup_theme' ) ) {
$taxonomies = array_merge( get_taxonomies( array( '_builtin' => true ) ), $taxonomies );
$post_types = array_merge( get_post_types( array( '_builtin' => true ) ), $post_types );
}
// Translate labels of post types and taxonomies
foreach ( array_diff_key( $GLOBALS['wp_taxonomies'], array_flip( $taxonomies ) ) as $tax ) {
$this->translate_labels( $tax );
}
foreach ( array_diff_key( $GLOBALS['wp_post_types'], array_flip( $post_types ) ) as $pt ) {
$this->translate_labels( $pt );
}
// Act only if the language has not been set early ( before default textdomain loading and $wp_locale creation )
if ( did_action( 'after_setup_theme' ) ) {
// Reinitializes wp_locale for weekdays and months
unset( $GLOBALS['wp_locale'] );
$GLOBALS['wp_locale'] = new WP_Locale();
}
/**
* Fires after the post types and taxonomies labels have been translated
* This allows plugins to translate text the same way we do for post types and taxonomies labels
*
* @since 1.2
*
* @param array $labels list of strings to trnaslate
*/
do_action_ref_array( 'pll_translate_labels', array( &$this->labels ) );
// Free memory.
$this->default_locale = null;
$this->list_textdomains = array();
$this->labels = array();
}
/**
* Saves all text domains in a table for later usage.
* It replaces the 'override_load_textdomain' filter previously used.
*
* @since 2.0.4
*
* @param string $mofile The translation file name.
* @param string $domain The text domain name.
* @return string
*/
public function load_textdomain_mofile( $mofile, $domain ) {
// On multisite, 2 files are sharing the same domain so we need to distinguish them.
if ( 'default' === $domain && false !== strpos( $mofile, '/ms-' ) ) {
$this->list_textdomains['ms-default'] = array( 'mo' => $mofile, 'domain' => $domain );
} else {
$this->list_textdomains[ $domain ] = array( 'mo' => $mofile, 'domain' => $domain );
}
return ''; // Hack to prevent WP loading text domains as we will load them all later.
}
/**
* Saves post types and taxonomies labels for a later usage
*
* @since 0.9
*
* @param string $translation not used
* @param string $text string to translate
* @param string $domain text domain
* @return string unmodified $translation
*/
public function gettext( $translation, $text, $domain ) {
if ( is_string( $text ) ) { // Avoid a warning with some buggy plugins which pass an array
$this->labels[ $text ] = array( 'domain' => $domain );
}
return $translation;
}
/**
* Saves post types and taxonomies labels for a later usage
*
* @since 0.9
*
* @param string $translation not used
* @param string $text string to translate
* @param string $context some comment to describe the context of string to translate
* @param string $domain text domain
* @return string unmodified $translation
*/
public function gettext_with_context( $translation, $text, $context, $domain ) {
$this->labels[ $text ] = array( 'domain' => $domain, 'context' => $context );
return $translation;
}
/**
* Translates post types and taxonomies labels once the language is known.
*
* @since 0.9
*
* @param WP_Post_Type|WP_Taxonomy $type Either a post type or a taxonomy.
* @return void
*/
public function translate_labels( $type ) {
// Use static array to avoid translating several times the same ( default ) labels
static $translated = array();
foreach ( (array) $type->labels as $key => $label ) {
if ( is_string( $label ) && isset( $this->labels[ $label ] ) ) {
if ( empty( $translated[ $label ] ) ) {
// PHPCS:disable WordPress.WP.I18n
$type->labels->$key = $translated[ $label ] = isset( $this->labels[ $label ]['context'] ) ?
_x( $label, $this->labels[ $label ]['context'], $this->labels[ $label ]['domain'] ) :
__( $label, $this->labels[ $label ]['domain'] );
// PHPCS:enable
}
else {
$type->labels->$key = $translated[ $label ];
}
}
}
}
/**
* Allows Polylang to be the first plugin loaded ;-).
*
* @since 1.2
*
* @param string[] $plugins List of active plugins.
* @return string[] List of active plugins.
*/
public function make_polylang_first( $plugins ) {
if ( $key = array_search( POLYLANG_BASENAME, $plugins ) ) {
unset( $plugins[ $key ] );
array_unshift( $plugins, POLYLANG_BASENAME );
}
return $plugins;
}
}

View File

@@ -0,0 +1,234 @@
<?php
/**
* @package Polylang
*/
/**
* A class to manipulate the language query var in WP_Query
*
* @since 2.2
*/
class PLL_Query {
/**
* @var PLL_Model
*/
public $model;
/**
* @var WP_Query
*/
public $query;
/**
* Constructor
*
* @since 2.2
*
* @param WP_Query $query Reference to the WP_Query object.
* @param PLL_Model $model Instance of PLL_Model.
*/
public function __construct( &$query, &$model ) {
$this->query = &$query;
$this->model = &$model;
}
/**
* Checks if the query already includes a language taxonomy.
*
* @since 3.0
*
* @param array $qvars WP_Query query vars.
* @return bool
*/
protected function is_already_filtered( $qvars ) {
if ( isset( $qvars['lang'] ) ) {
return true;
}
if ( ! empty( $qvars['tax_query'] ) && is_array( $qvars['tax_query'] ) ) {
foreach ( $qvars['tax_query'] as $tax_query ) {
if ( isset( $tax_query['taxonomy'] ) && 'language' === $tax_query['taxonomy'] ) {
return true;
}
}
}
return false;
}
/**
* Check if translated taxonomy is queried
* Compatible with nested queries introduced in WP 4.1
*
* @see https://wordpress.org/support/topic/tax_query-bug
*
* @since 1.7
*
* @param array $tax_queries An array of tax queries.
* @return bool
*/
protected function have_translated_taxonomy( $tax_queries ) {
if ( is_array( $tax_queries ) ) {
foreach ( $tax_queries as $tax_query ) {
if ( isset( $tax_query['taxonomy'] ) && $this->model->is_translated_taxonomy( $tax_query['taxonomy'] ) && ! ( isset( $tax_query['operator'] ) && 'NOT IN' === $tax_query['operator'] ) ) {
return true;
}
// Nested queries
elseif ( is_array( $tax_query ) && $this->have_translated_taxonomy( $tax_query ) ) {
return true;
}
}
}
return false;
}
/**
* Get queried taxonomies
*
* @since 2.2
*
* @return array queried taxonomies
*/
public function get_queried_taxonomies() {
return ! empty( $this->query->tax_query->queried_terms ) ? array_keys( wp_list_filter( $this->query->tax_query->queried_terms, array( 'operator' => 'NOT IN' ), 'NOT' ) ) : array();
}
/**
* Sets the language in query.
* Optimized for (and requires) WP 3.5+.
*
* @since 2.2
* @since 3.3 Accepts now an array of languages.
*
* @param PLL_Language|PLL_Language[] $languages Language object(s).
* @return void
*/
public function set_language( $languages ) {
if ( ! is_array( $languages ) ) {
$languages = array( $languages );
}
$tt_ids = array();
foreach ( $languages as $language ) {
$tt_ids[] = $language->get_tax_prop( 'language', 'term_taxonomy_id' );
}
// Defining directly the tax_query (rather than setting 'lang' avoids transforming the query by WP).
$lang_query = array(
'taxonomy' => 'language',
'field' => 'term_taxonomy_id', // Since WP 3.5
'terms' => $tt_ids,
'operator' => 'IN',
);
$tax_query = &$this->query->query_vars['tax_query'];
if ( isset( $tax_query['relation'] ) && 'OR' === $tax_query['relation'] ) {
$tax_query = array(
$lang_query,
$tax_query,
'relation' => 'AND',
);
} elseif ( is_array( $tax_query ) ) {
// The tax query is expected to be *always* an array, but it seems that 3rd parties fill it with a string
// Causing a fatal error if we don't check it.
// See https://wordpress.org/support/topic/fatal-error-2947/
$tax_query[] = $lang_query;
} elseif ( empty( $tax_query ) ) {
// Supposing the tax query has been wrongly filled with an empty string
$tax_query = array( $lang_query );
}
}
/**
* Adds the language in the query after it has checked that it won't conflict with other query vars.
*
* @since 2.2
*
* @param PLL_Language|false $lang Language.
* @return void
*/
public function filter_query( $lang ) {
$qvars = &$this->query->query_vars;
if ( ! $this->is_already_filtered( $qvars ) ) {
$taxonomies = array_intersect( $this->model->get_translated_taxonomies(), get_taxonomies( array( '_builtin' => false ) ) );
foreach ( $taxonomies as $tax ) {
$tax_object = get_taxonomy( $tax );
if ( ! empty( $tax_object ) && ! empty( $qvars[ $tax_object->query_var ] ) ) {
return;
}
}
if ( ! empty( $qvars['tax_query'] ) && $this->have_translated_taxonomy( $qvars['tax_query'] ) ) {
return;
}
// Filter queries according to the requested language
if ( ! empty( $lang ) ) {
$taxonomies = $this->get_queried_taxonomies();
if ( $taxonomies && ( empty( $qvars['post_type'] ) || 'any' === $qvars['post_type'] ) ) {
foreach ( $taxonomies as $taxonomy ) {
$tax_object = get_taxonomy( $taxonomy );
if ( ! empty( $tax_object ) && $this->model->is_translated_post_type( $tax_object->object_type ) ) {
$this->set_language( $lang );
break;
}
}
} elseif ( empty( $qvars['post_type'] ) || $this->model->is_translated_post_type( $qvars['post_type'] ) ) {
$this->set_language( $lang );
}
}
} else {
$this->maybe_set_language_for_or_relation();
// Do not filter untranslatable post types such as nav_menu_item
if ( isset( $qvars['post_type'] ) && ! $this->model->is_translated_post_type( $qvars['post_type'] ) && ( empty( $qvars['tax_query'] ) || ! $this->have_translated_taxonomy( $qvars['tax_query'] ) ) ) {
unset( $qvars['lang'] );
}
// Unset 'all' query var (mainly for admin language filter).
if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
unset( $qvars['lang'] );
}
}
}
/**
* Sets the language correctly if the current query is a 'OR' relation,
* since WordPress merges the language with the other query vars when the relation is OR.
*
* @since 3.3
*
* @return void
*/
protected function maybe_set_language_for_or_relation() {
if ( ! $this->query->tax_query instanceof WP_Tax_Query ) {
return;
}
if ( 'OR' !== $this->query->tax_query->relation ) {
return;
}
if ( ! isset( $this->query->tax_query->queried_terms['language'] ) ) {
return;
}
$langs = $this->query->tax_query->queried_terms['language']['terms'];
if ( is_string( $langs ) ) {
$langs = explode( ',', $langs );
}
$langs = array_map( array( $this->model, 'get_language' ), $langs );
$langs = array_filter( $langs );
if ( ! empty( $langs ) ) {
$this->set_language( $langs );
unset( $this->query->query_vars['lang'] ); // Unset the language query var otherwise WordPress would add the language query by slug in WP_Query::parse_tax_query().
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* @package Polylang
*/
/**
* Main Polylang class for REST API requests, accessible from @see PLL().
*
* @since 2.6
*/
class PLL_REST_Request extends PLL_Base {
/**
* @var PLL_Language|false|null A `PLL_Language` when defined, `false` otherwise. `null` until the language
* definition process runs.
*/
public $curlang;
/**
* @var PLL_Filters|null
*/
public $filters;
/**
* @var PLL_Filters_Links|null
*/
public $filters_links;
/**
* @var PLL_Admin_Links|null
*/
public $links;
/**
* @var PLL_Nav_Menu|null
*/
public $nav_menu;
/**
* @var PLL_Static_Pages|null
*/
public $static_pages;
/**
* @var PLL_Filters_Widgets_Options|null
*/
public $filters_widgets_options;
/**
* Constructor.
*
* @since 3.4
*
* @param PLL_Links_Model $links_model Reference to the links model.
*/
public function __construct( &$links_model ) {
parent::__construct( $links_model );
// Static front page and page for posts.
// Early instantiated to be able to correctly initialize language properties.
if ( 'page' === get_option( 'show_on_front' ) ) {
$this->static_pages = new PLL_Static_Pages( $this );
}
$this->model->set_languages_ready();
}
/**
* Setup filters.
*
* @since 2.6
*
* @return void
*/
public function init() {
parent::init();
if ( ! $this->model->has_languages() ) {
return;
}
add_filter( 'rest_pre_dispatch', array( $this, 'set_language' ), 10, 3 );
$this->filters_links = new PLL_Filters_Links( $this );
$this->filters = new PLL_Filters( $this );
$this->filters_widgets_options = new PLL_Filters_Widgets_Options( $this );
$this->links = new PLL_Admin_Links( $this );
$this->nav_menu = new PLL_Frontend_Nav_Menu( $this ); // For auto added pages to menu.
}
/**
* Sets the current language during a REST request if sent.
*
* @since 3.3
*
* @param mixed $result Response to replace the requested version with. Remains untouched.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
* @return mixed Untouched $result.
*
* @phpstan-param WP_REST_Request<array{lang?: string}> $request
*/
public function set_language( $result, $server, $request ) {
$lang = $request->get_param( 'lang' );
if ( ! empty( $lang ) && is_string( $lang ) ) {
$this->curlang = $this->model->get_language( sanitize_key( $lang ) );
if ( empty( $this->curlang ) && ! empty( $this->options['default_lang'] ) && is_string( $this->options['default_lang'] ) ) {
// A lang has been requested but it is invalid, let's fall back to the default one.
$this->curlang = $this->model->get_language( sanitize_key( $this->options['default_lang'] ) );
}
}
if ( ! empty( $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.
}
return $result;
}
}

View File

@@ -0,0 +1,230 @@
<?php
/**
* @package Polylang
*/
/**
* Base class to manage the static front page and the page for posts.
*
* @since 1.8
*/
class PLL_Static_Pages {
/**
* Id of the page on front.
*
* @var int
*/
public $page_on_front = 0;
/**
* Id of the page for posts.
*
* @var int
*/
public $page_for_posts = 0;
/**
* @var PLL_Model
*/
protected $model;
/**
* Current language.
*
* @var PLL_Language|null
*/
protected $curlang;
/**
* Constructor: setups filters and actions.
*
* @since 1.8
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->curlang = &$polylang->curlang;
$this->init();
add_filter( 'pll_additional_language_data', array( $this, 'set_static_pages' ), 5, 2 ); // Before PLL_Links_Model.
// Clean the languages cache when editing page of front, page for posts.
add_action( 'update_option_show_on_front', array( $this, 'clean_cache' ) );
add_action( 'update_option_page_on_front', array( $this, 'clean_cache' ) );
add_action( 'update_option_page_for_posts', array( $this, 'clean_cache' ) );
// Refresh rewrite rules when the page on front is modified.
add_action( 'update_option_page_on_front', 'flush_rewrite_rules' );
// Add option filters when the current language is defined
add_action( 'pll_language_defined', array( $this, 'pll_language_defined' ) );
// Modifies the page link in case the front page is not in the default language.
add_filter( 'page_link', array( $this, 'page_link' ), 20, 2 );
// OEmbed.
add_filter( 'oembed_request_post_id', array( $this, 'oembed_request_post_id' ), 10, 2 );
}
/**
* Stores the page on front and page for posts ids.
*
* @since 1.8
*
* @return void
*/
public function init() {
$this->page_on_front = 0;
$this->page_for_posts = 0;
if ( 'page' !== get_option( 'show_on_front' ) ) {
return;
}
$page_on_front = get_option( 'page_on_front' );
if ( is_numeric( $page_on_front ) ) {
$this->page_on_front = (int) $page_on_front;
}
$page_for_posts = get_option( 'page_for_posts' );
if ( is_numeric( $page_for_posts ) ) {
$this->page_for_posts = (int) $page_for_posts;
}
}
/**
* Returns the ID of the static page translation.
*
* @since 3.4
*
* @param string $static_page Static page option name; `page_on_front` or `page_for_posts`.
* @param array $language Language data.
* @return int
*/
protected function get_translation( $static_page, $language ) {
$translations = $this->model->post->get_raw_translations( $this->$static_page );
// When the current static page doesn't have any translation, we must return itself for its language.
if ( empty( $translations ) ) {
$page_lang = $this->model->post->get_object_term( $this->$static_page, $this->model->post->get_tax_language() );
if ( ! empty( $page_lang ) && $page_lang->slug === $language['slug'] ) {
return $this->$static_page;
}
}
if ( ! isset( $translations[ $language['slug'] ] ) ) {
return 0;
}
return $translations[ $language['slug'] ];
}
/**
* Adds `page_on_front` and `page_for_posts` properties to language data before the object is created.
*
* @since 3.4
*
* @param array $additional_data Array of language additional data.
* @param array $language Language data.
* @return array Language data with additional `page_on_front` and `page_for_posts` options added.
*/
public function set_static_pages( $additional_data, $language ) {
$additional_data['page_on_front'] = $this->get_translation( 'page_on_front', $language );
$additional_data['page_for_posts'] = $this->get_translation( 'page_for_posts', $language );
return $additional_data;
}
/**
* Cleans the language cache and resets the internal properties when options are updated.
*
* @since 3.4
*
* @return void
*/
public function clean_cache() {
$this->model->clean_languages_cache();
$this->init();
}
/**
* Init the hooks that filter the "page on front" and "page for posts" options.
*
* @since 3.3
*
* @return void
*/
public function pll_language_defined() {
// Translates page for posts and page on front.
add_filter( 'option_page_on_front', array( $this, 'translate_page_id' ), 10, 2 );
add_filter( 'option_page_for_posts', array( $this, 'translate_page_id' ), 10, 2 );
}
/**
* Translates the page on front or page for posts option.
*
* @since 3.6 Replaces `translate_page_on_front()` and `translate_page_on_front()` methods.
*
* @param int $page_id ID of the page on front or page for posts.
* @param string $option Option name: `page_on_front` or `page_for_posts`.
* @return int
*/
public function translate_page_id( $page_id, $option ) {
if ( empty( $this->curlang->{$option} ) ) {
return $page_id;
}
if ( doing_action( "update_option_{$option}" ) || doing_action( 'switch_blog' ) || doing_action( 'before_delete_post' ) || doing_action( 'wp_trash_post' ) ) {
/*
* Don't attempt to translate in a 'switch_blog' action as there is a risk to call this function while initializing the languages cache.
* Don't translate while deleting a post or it will mess up `_reset_front_page_settings_for_post()`.
* Don't translate while updating the option itself.
*/
return $page_id;
}
return $this->curlang->{$option};
}
/**
* Modifies the page link in case the front page is not in the default language.
*
* @since 0.7.2
*
* @param string $link The link to the page.
* @param int $id The post ID of the page.
* @return string Modified link.
*/
public function page_link( $link, $id ) {
$lang = $this->model->post->get_language( $id );
if ( $lang && $id == $lang->page_on_front ) {
return $lang->get_home_url();
}
return $link;
}
/**
* Fixes the oembed for the translated static front page
* when the language page is redirected to the front page.
*
* @since 2.6
*
* @param int $post_id The post ID.
* @param string $url The requested URL.
* @return int
*/
public function oembed_request_post_id( $post_id, $url ) {
foreach ( $this->model->get_languages_list() as $lang ) {
if ( is_string( $lang->get_home_url() ) && trailingslashit( $url ) === trailingslashit( $lang->get_home_url() ) ) {
return (int) $lang->page_on_front;
}
}
return $post_id;
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* @package Polylang
*/
/**
* A class to display a language switcher on frontend
*
* @since 1.2
*/
class PLL_Switcher {
const DEFAULTS = array(
'dropdown' => 0, // Display as list and not as dropdown.
'echo' => 1, // Echoes the list.
'hide_if_empty' => 1, // Hides languages with no posts (or pages).
'show_flags' => 0, // Don't show flags.
'show_names' => 1, // Show language names.
'display_names_as' => 'name', // Display the language name.
'force_home' => 0, // Tries to find a translation.
'hide_if_no_translation' => 0, // Don't hide the link if there is no translation.
'hide_current' => 0, // Don't hide the current language.
'post_id' => null, // Link to the translations of the current page.
'raw' => 0, // Build the language switcher.
'item_spacing' => 'preserve', // Preserve whitespace between list items.
'admin_render' => 0, // Make the switcher in a frontend context.
'admin_current_lang' => null, // Use the global current language.
);
/**
* @var PLL_Links|null
*/
protected $links;
/**
* Returns options available for the language switcher - menu or widget
* either strings to display the options or default values
*
* @since 0.7
*
* @param string $type optional either 'menu', 'widget' or 'block', defaults to 'widget'
* @param string $key optional either 'string' or 'default', defaults to 'string'
* @return array list of switcher options strings or default values
*/
public static function get_switcher_options( $type = 'widget', $key = 'string' ) {
$options = array(
'dropdown' => array( 'string' => __( 'Displays as a dropdown', 'polylang' ), 'default' => 0 ),
'show_names' => array( 'string' => __( 'Displays language names', 'polylang' ), 'default' => 1 ),
'show_flags' => array( 'string' => __( 'Displays flags', 'polylang' ), 'default' => 0 ),
'force_home' => array( 'string' => __( 'Forces link to front page', 'polylang' ), 'default' => 0 ),
'hide_current' => array( 'string' => __( 'Hides the current language', 'polylang' ), 'default' => 0 ),
'hide_if_no_translation' => array( 'string' => __( 'Hides languages with no translation', 'polylang' ), 'default' => 0 ),
);
return wp_list_pluck( $options, $key );
}
/**
* Returns the current language code.
*
* @since 3.0
*
* @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}.
* @return string
*/
protected function get_current_language( $args ) {
if ( $args['admin_current_lang'] ) {
return $args['admin_current_lang'];
}
if ( isset( $this->links->curlang ) ) {
return $this->links->curlang->slug;
}
return $this->links->options['default_lang'];
}
/**
* Returns the link for a given language.
*
* @since 3.0
*
* @param PLL_Language $language Language.
* @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}.
* @return string|null
*/
protected function get_link( $language, $args ) {
global $post;
// Priority to the post passed in parameters.
if ( null !== $args['post_id'] ) {
$tr_id = $this->links->model->post->get( $args['post_id'], $language );
if ( $tr_id && $this->links->model->post->current_user_can_read( $tr_id ) ) {
return get_permalink( $tr_id );
}
}
// If we are on frontend.
if ( $this->links instanceof PLL_Frontend_Links ) {
return $this->links->get_translation_url( $language );
}
// For blocks in posts in REST requests.
if ( $post instanceof WP_Post ) {
$tr_id = $this->links->model->post->get( $post->ID, $language );
if ( $tr_id && $this->links->model->post->current_user_can_read( $tr_id ) ) {
return get_permalink( $tr_id );
}
}
return null;
}
/**
* Get the language elements for use in a walker
*
* @since 1.2
*
* @param array $args Arguments passed to {@see PLL_Switcher::the_languages()}.
* @return array Language switcher elements.
*/
protected function get_elements( $args ) {
$first = true;
$out = array();
foreach ( $this->links->model->get_languages_list( array( 'hide_empty' => $args['hide_if_empty'] ) ) as $language ) {
$id = (int) $language->term_id;
$order = (int) $language->term_group;
$slug = $language->slug;
$locale = $language->get_locale( 'display' );
$item_classes = array( 'lang-item', 'lang-item-' . $id, 'lang-item-' . esc_attr( $slug ) );
$classes = isset( $args['classes'] ) && is_array( $args['classes'] ) ?
array_merge(
$item_classes,
$args['classes']
) :
$item_classes;
$link_classes = isset( $args['link_classes'] ) ? $args['link_classes'] : array();
$current_lang = $this->get_current_language( $args ) === $slug;
if ( $current_lang ) {
if ( $args['hide_current'] && ! ( $args['dropdown'] && ! $args['raw'] ) ) {
continue; // Hide current language except for dropdown
} else {
$classes[] = 'current-lang';
}
}
$url = $this->get_link( $language, $args );
if ( $no_translation = empty( $url ) ) {
$classes[] = 'no-translation';
}
/**
* Filter the link in the language switcher
*
* @since 0.7
*
* @param string|null $url The link, null if no translation was found.
* @param string $slug The language code.
* @param string $locale The language locale
*/
$url = apply_filters( 'pll_the_language_link', $url, $slug, $language->locale );
// Hide if no translation exists
if ( empty( $url ) && $args['hide_if_no_translation'] ) {
continue;
}
$url = empty( $url ) || $args['force_home'] ? $this->links->get_home_url( $language ) : $url; // If the page is not translated, link to the home page
$name = $args['show_names'] || ! $args['show_flags'] || $args['raw'] ? ( 'slug' == $args['display_names_as'] ? $slug : $language->name ) : '';
if ( $args['raw'] && ! $args['show_flags'] ) {
$flag = $language->get_display_flag_url();
} elseif ( $args['show_flags'] ) {
$flag = $language->get_display_flag( empty( $args['show_names'] ) ? 'alt' : 'no-alt' );
} else {
$flag = '';
}
if ( $first ) {
$classes[] = 'lang-item-first';
$first = false;
}
$out[ $slug ] = compact( 'id', 'order', 'slug', 'locale', 'name', 'url', 'flag', 'current_lang', 'no_translation', 'classes', 'link_classes' );
}
return $out;
}
/**
* Displays a language switcher
* or returns the raw elements to build a custom language switcher.
*
* @since 0.1
*
* @param PLL_Links $links Instance of PLL_Links.
* @param array $args {
* Optional array of arguments.
*
* @type int $dropdown The list is displayed as dropdown if set, defaults to 0.
* @type int $echo Echoes the list if set to 1, defaults to 1.
* @type int $hide_if_empty Hides languages with no posts ( or pages ) if set to 1, defaults to 1.
* @type int $show_flags Displays flags if set to 1, defaults to 0.
* @type int $show_names Shows language names if set to 1, defaults to 1.
* @type string $display_names_as Whether to display the language name or its slug, valid options are 'slug' and 'name', defaults to name.
* @type int $force_home Will always link to home in translated language if set to 1, defaults to 0.
* @type int $hide_if_no_translation Hides the link if there is no translation if set to 1, defaults to 0.
* @type int $hide_current Hides the current language if set to 1, defaults to 0.
* @type int $post_id Returns links to the translations of the post defined by post_id if set, defaults not set.
* @type int $raw Return a raw array instead of html markup if set to 1, defaults to 0.
* @type string $item_spacing Whether to preserve or discard whitespace between list items, valid options are 'preserve' and 'discard', defaults to 'preserve'.
* @type int $admin_render Allows to force the current language code in an admin context if set, default to 0. Need to set the admin_current_lang argument below.
* @type string $admin_current_lang The current language code in an admin context. Need to set the admin_render to 1, defaults not set.
* @type string[] $classes A list of CSS classes to set to each elements outputted.
* @type string[] $link_classes A list of CSS classes to set to each link outputted.
* }
* @return string|array either the html markup of the switcher or the raw elements to build a custom language switcher
*/
public function the_languages( $links, $args = array() ) {
$this->links = $links;
$args = wp_parse_args( $args, self::DEFAULTS );
/**
* Filter the arguments of the 'pll_the_languages' template tag
*
* @since 1.5
*
* @param array $args
*/
$args = apply_filters( 'pll_the_languages_args', $args );
// Force not to hide the language for the widget preview even if the option is checked.
if ( $this->links instanceof PLL_Admin_Links ) {
$args['hide_if_no_translation'] = 0;
}
// Prevents showing empty options in `<select>`.
if ( $args['dropdown'] && ! $args['raw'] ) {
$args['show_names'] = 1;
}
$elements = $this->get_elements( $args );
if ( $args['raw'] ) {
return $elements;
}
if ( $args['dropdown'] ) {
$args['name'] = 'lang_choice_' . $args['dropdown'];
$args['class'] = 'pll-switcher-select';
$args['value'] = 'url';
$args['selected'] = $this->get_link( $this->links->model->get_language( $this->get_current_language( $args ) ), $args );
$walker = new PLL_Walker_Dropdown();
} else {
$walker = new PLL_Walker_List();
}
// Cast each element to stdClass because $walker::walk() expects an array of objects.
foreach ( $elements as $i => $element ) {
$elements[ $i ] = (object) $element;
}
/**
* Filter the whole html markup returned by the 'pll_the_languages' template tag
*
* @since 0.8
*
* @param string $html html returned/outputted by the template tag
* @param array $args arguments passed to the template tag
*/
$out = apply_filters( 'pll_the_languages', $walker->walk( $elements, -1, $args ), $args );
// Javascript to switch the language when using a dropdown list.
if ( $args['dropdown'] && 0 === $args['admin_render'] ) {
// Accept only few valid characters for the urls_x variable name (as the widget id includes '-' which is invalid).
$out .= sprintf(
'<script%1$s>
document.getElementById( "%2$s" ).addEventListener( "change", function ( event ) { location.href = event.currentTarget.value; } )
</script>',
current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"',
esc_js( $args['name'] )
);
}
if ( $args['echo'] ) {
echo $out; // phpcs:ignore WordPress.Security.EscapeOutput
}
return $out;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Interface to use for objects that can have one or more types.
*
* @since 3.4
*
* @phpstan-type DBInfoWithType array{
* table: non-empty-string,
* id_column: non-empty-string,
* type_column: non-empty-string,
* default_alias: non-empty-string
* }
*/
interface PLL_Translatable_Object_With_Types_Interface {
/**
* Returns object types that need to be translated.
*
* @since 3.4
*
* @param bool $filter True if we should return only valid registered object types.
* @return string[] Object type names for which Polylang manages languages.
*
* @phpstan-return array<non-empty-string, non-empty-string>
*/
public function get_translated_object_types( $filter = true );
/**
* Returns true if Polylang manages languages for this object type.
*
* @since 3.4
*
* @param string|string[] $object_type Object type name or array of object type names.
* @return bool
*
* @phpstan-param non-empty-string|non-empty-string[] $object_type
*/
public function is_translated_object_type( $object_type );
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Trait to use for objects that can have one or more types.
* This must be used with {@see PLL_Translatable_Object_With_Types_Interface}.
*
* @since 3.4
*/
trait PLL_Translatable_Object_With_Types_Trait {
/**
* Returns SQL query that fetches the IDs of the objects without language.
*
* @since 3.4
*
* @param int[] $language_ids List of language `term_taxonomy_id`.
* @param int $limit Max number of objects to return. `-1` to return all of them.
* @param array $args An array of translated object types.
* @return string
*
* @phpstan-param array<positive-int> $language_ids
* @phpstan-param -1|positive-int $limit
* @phpstan-param array<string> $args
*/
protected function get_objects_with_no_lang_sql( array $language_ids, $limit, array $args = array() ) {
if ( empty( $args ) ) {
$args = $this->get_translated_object_types();
}
$db = $this->get_db_infos();
return sprintf(
"SELECT {$db['table']}.{$db['id_column']} FROM {$db['table']}
WHERE {$db['table']}.{$db['id_column']} NOT IN (
SELECT object_id FROM {$GLOBALS['wpdb']->term_relationships} WHERE term_taxonomy_id IN (%s)
)
AND {$db['type_column']} IN (%s)
%s",
PLL_Db_Tools::prepare_values_list( $language_ids ),
PLL_Db_Tools::prepare_values_list( $args ),
$limit >= 1 ? sprintf( 'LIMIT %d', $limit ) : ''
);
}
/**
* Returns true if Polylang manages languages for this object type.
*
* @since 3.4
*
* @param string|string[] $object_type Object type (taxonomy name) name or array of object type names.
* @return bool
*
* @phpstan-param non-empty-string|non-empty-string[] $object_type
*/
public function is_translated_object_type( $object_type ) {
$object_types = $this->get_translated_object_types( false );
return ! empty( array_intersect( (array) $object_type, $object_types ) );
}
}

View File

@@ -0,0 +1,519 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Abstract class to use for object types that support at least one language.
*
* @since 3.4
*
* @phpstan-type DBInfo array{
* table: non-empty-string,
* id_column: non-empty-string,
* default_alias: non-empty-string
* }
*/
abstract class PLL_Translatable_Object {
/**
* @var PLL_Model
*/
public $model;
/**
* List of taxonomies to cache.
*
* @var string[]
* @see PLL_Translatable_Object::get_object_term()
*
* @phpstan-var list<non-empty-string>
*/
protected $tax_to_cache = array();
/**
* Taxonomy name for the languages.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_language;
/**
* Identifier that must be unique for each type of content.
* Also used when checking capabilities.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $type;
/**
* Identifier for each type of content to used for cache type.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $cache_type;
/**
* Object type to use when registering the taxonomy.
* Left empty for posts.
*
* @var string|null
*
* @phpstan-var non-empty-string|null
*/
protected $object_type = null;
/**
* Constructor.
*
* @since 3.4
*
* @param PLL_Model $model Instance of `PLL_Model`, passed by reference.
*/
public function __construct( PLL_Model &$model ) {
$this->model = $model;
$this->tax_to_cache[] = $this->tax_language;
/*
* Register our taxonomy as soon as possible.
* This is early registration, not ready for rewrite rules as $wp_rewrite will be setup later.
*/
register_taxonomy(
$this->tax_language,
(array) $this->object_type,
array(
'label' => false,
'public' => false,
'query_var' => false,
'rewrite' => false,
'_pll' => true,
)
);
}
/**
* Returns the language taxonomy name.
*
* @since 3.4
*
* @return string
*
* @phpstan-return non-empty-string
*/
public function get_tax_language() {
return $this->tax_language;
}
/**
* Returns the type of object.
*
* @since 3.4
*
* @return string
*
* @phpstan-return non-empty-string
*/
public function get_type() {
return $this->type;
}
/**
* Adds hooks.
*
* @since 3.4
*
* @return static
*/
public function init() {
return $this;
}
/**
* Stores the object's language into the database.
*
* @since 3.4
*
* @param int $id Object ID.
* @param PLL_Language|string|int $lang Language (object, slug, or term ID).
* @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
* the object).
*/
public function set_language( $id, $lang ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return false;
}
$old_lang = $this->get_language( $id );
$old_lang = $old_lang ? $old_lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0;
$lang = $this->model->get_language( $lang );
$lang = $lang ? $lang->get_tax_prop( $this->tax_language, 'term_id' ) : 0;
if ( $old_lang === $lang ) {
return false;
}
$term_taxonomy_ids = wp_set_object_terms( $id, $lang, $this->tax_language );
wp_cache_set( 'last_changed', microtime(), $this->cache_type );
return is_array( $term_taxonomy_ids );
}
/**
* Returns the language of an object.
*
* @since 0.1
* @since 3.4 Renamed the parameter $post_id into $id.
*
* @param int $id Object ID.
* @return PLL_Language|false A `PLL_Language` object. `false` if no language is associated to that object or if the
* ID is invalid.
*/
public function get_language( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return false;
}
// Get the language and make sure it is a PLL_Language object.
$lang = $this->get_object_term( $id, $this->tax_language );
if ( empty( $lang ) ) {
return false;
}
return $this->model->get_language( $lang->term_id );
}
/**
* Removes the term language from the database.
*
* @since 3.4
*
* @param int $id Term ID.
* @return void
*/
public function delete_language( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return;
}
wp_delete_object_term_relationships( $id, $this->tax_language );
}
/**
* Wraps `wp_get_object_terms()` to cache it and return only one object.
* Inspired by the WordPress function `get_the_terms()`.
*
* @since 1.2
*
* @param int $id Object ID.
* @param string $taxonomy Polylang taxonomy depending if we are looking for a post (or term, or else) language.
* @return WP_Term|false The term associated to the object in the requested taxonomy if it exists, `false` otherwise.
*/
public function get_object_term( $id, $taxonomy ) {
global $wp_version;
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return false;
}
$term = get_object_term_cache( $id, $taxonomy );
if ( is_array( $term ) ) {
return ! empty( $term ) ? reset( $term ) : false;
}
// Query terms.
$terms = array();
$term = false;
$object_terms = wp_get_object_terms( $id, $this->tax_to_cache, array( 'update_term_meta_cache' => false ) );
if ( is_array( $object_terms ) ) {
foreach ( $object_terms as $t ) {
$terms[ $t->taxonomy ] = $t;
if ( $t->taxonomy === $taxonomy ) {
$term = $t;
}
}
}
// Stores it the way WP expects it. Set an empty cache if no term was found in the taxonomy.
$store_only_term_ids = version_compare( $wp_version, '6.0', '>=' );
foreach ( $this->tax_to_cache as $tax ) {
if ( empty( $terms[ $tax ] ) ) {
$to_cache = array();
} elseif ( $store_only_term_ids ) {
$to_cache = array( $terms[ $tax ]->term_id );
} else {
// Backward compatibility with WP < 6.0.
$to_cache = array( $terms[ $tax ] );
}
wp_cache_add( $id, $to_cache, "{$tax}_relationships" );
}
return $term;
}
/**
* A JOIN clause to add to sql queries when filtering by language is needed directly in query.
*
* @since 3.4
*
* @param string $alias Optional alias for object table.
* @return string The JOIN clause.
*
* @phpstan-return non-empty-string
*/
public function join_clause( $alias = '' ) {
global $wpdb;
$db = $this->get_db_infos();
if ( empty( $alias ) ) {
$alias = $db['default_alias'];
}
return " INNER JOIN {$wpdb->term_relationships} AS pll_tr ON pll_tr.object_id = {$alias}.{$db['id_column']}";
}
/**
* A WHERE clause to add to sql queries when filtering by language is needed directly in query.
*
* @since 1.2
*
* @param PLL_Language|PLL_Language[]|string|string[] $lang A `PLL_Language` object, or a comma separated list of language slugs, or an array of language slugs or objects.
* @return string The WHERE clause.
*
* @phpstan-param PLL_Language|PLL_Language[]|non-empty-string|non-empty-string[] $lang
*/
public function where_clause( $lang ) {
/*
* $lang is an object.
* This is generally the case if the query is coming from Polylang.
*/
if ( $lang instanceof PLL_Language ) {
return ' AND pll_tr.term_taxonomy_id = ' . absint( $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
}
/*
* $lang is an array of objects, an array of slugs, or a comma separated list of slugs.
* The comma separated list of slugs can happen if the query is coming from outside with a 'lang' parameter.
*/
$languages = is_array( $lang ) ? $lang : explode( ',', $lang );
$languages_tt_ids = array();
foreach ( $languages as $language ) {
$language = $this->model->get_language( $language );
if ( ! empty( $language ) ) {
$languages_tt_ids[] = absint( $language->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ) );
}
}
if ( empty( $languages_tt_ids ) ) {
return '';
}
return ' AND pll_tr.term_taxonomy_id IN ( ' . implode( ',', $languages_tt_ids ) . ' )';
}
/**
* Returns the IDs of the objects without language.
*
* @since 3.4
*
* @param int $limit Max number of objects to return. `-1` to return all of them.
* @param array $args The object args.
* @return int[] Array of object IDs.
*
* @phpstan-param -1|positive-int $limit
* @phpstan-return list<positive-int>
*/
public function get_objects_with_no_lang( $limit, array $args = array() ) {
$language_ids = array();
foreach ( $this->model->get_languages_list() as $language ) {
$language_ids[] = $language->get_tax_prop( $this->get_tax_language(), 'term_taxonomy_id' );
}
$language_ids = array_filter( $language_ids );
if ( empty( $language_ids ) ) {
return array();
}
$sql = $this->get_objects_with_no_lang_sql( $language_ids, $limit, $args );
$object_ids = $this->query_objects_with_no_lang( $sql );
return array_values( $this->sanitize_int_ids_list( $object_ids ) );
}
/**
* Returns object IDs without language given a specific SQL query.
* Can be overridden by child classes in case queried object doesn't use
* `wp_cache_set_last_changed()` or another cache system.
*
* @since 3.4
*
* @param string $sql A prepared SQL query for object IDs with no language.
* @return string[] An array of numeric object IDs.
*/
protected function query_objects_with_no_lang( $sql ) {
$key = md5( $sql );
$last_changed = wp_cache_get_last_changed( $this->cache_type );
$cache_key = "{$this->cache_type}_no_lang:{$key}:{$last_changed}";
$object_ids = wp_cache_get( $cache_key, $this->cache_type );
if ( is_array( $object_ids ) ) {
return $object_ids;
}
$object_ids = $GLOBALS['wpdb']->get_col( $sql ); // PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
wp_cache_set( $cache_key, $object_ids, $this->cache_type );
return $object_ids;
}
/**
* Sanitizes an ID as positive integer.
* Kind of similar to `absint()`, but rejects negetive integers instead of making them positive.
*
* @since 3.2
*
* @param mixed $id A supposedly numeric ID.
* @return int A positive integer. `0` for non numeric values and negative integers.
*
* @phpstan-return int<0,max>
*/
public function sanitize_int_id( $id ) {
return is_numeric( $id ) && $id >= 1 ? abs( (int) $id ) : 0;
}
/**
* Sanitizes an array of IDs as positive integers.
* `0` values are removed.
*
* @since 3.2
*
* @param mixed $ids An array of numeric IDs.
* @return int[]
*
* @phpstan-return array<positive-int>
*/
public function sanitize_int_ids_list( $ids ) {
if ( empty( $ids ) || ! is_array( $ids ) ) {
return array();
}
$ids = array_map( array( $this, 'sanitize_int_id' ), $ids );
return array_filter( $ids );
}
/**
* Returns SQL query that fetches the IDs of the objects without language.
*
* @since 3.4
*
* @param int[] $language_ids List of language `term_taxonomy_id`.
* @param int $limit Max number of objects to return. `-1` to return all of them.
* @param array $args The object args.
* @return string
*
* @phpstan-param array<positive-int> $language_ids
* @phpstan-param -1|positive-int $limit
* @phpstan-param array<empty> $args
*/
protected function get_objects_with_no_lang_sql( array $language_ids, $limit, array $args = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$db = $this->get_db_infos();
return sprintf(
"SELECT {$db['table']}.{$db['id_column']} FROM {$db['table']}
WHERE {$db['table']}.{$db['id_column']} NOT IN (
SELECT object_id FROM {$GLOBALS['wpdb']->term_relationships} WHERE term_taxonomy_id IN (%s)
)
%s",
PLL_Db_Tools::prepare_values_list( $language_ids ),
$limit >= 1 ? sprintf( 'LIMIT %d', $limit ) : ''
);
}
/**
* Assigns a language to object in mass.
*
* @since 1.2
* @since 3.4 Moved from PLL_Admin_Model class.
*
* @param int[] $ids Array of post ids or term ids.
* @param PLL_Language $lang Language to assign to the posts or terms.
* @return void
*/
public function set_language_in_mass( $ids, $lang ) {
global $wpdb;
$tt_id = $lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' );
if ( empty( $tt_id ) ) {
return;
}
$ids = array_map( 'intval', $ids );
$ids = array_filter( $ids );
if ( empty( $ids ) ) {
return;
}
$values = array();
foreach ( $ids as $id ) {
$values[] = $wpdb->prepare( '( %d, %d )', $id, $tt_id );
}
// PHPCS:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( "INSERT INTO {$wpdb->term_relationships} ( object_id, term_taxonomy_id ) VALUES " . implode( ',', array_unique( $values ) ) );
// Updating term count is mandatory (thanks to AndyDeGroo).
$lang->update_count();
clean_term_cache( $ids, $this->tax_language );
// Invalidate our cache.
wp_cache_set( 'last_changed', microtime(), $this->cache_type );
}
/**
* Returns database-related information that can be used in some of this class methods.
* These are specific to the table containing the objects.
*
* @see PLL_Translatable_Object::join_clause()
* @see PLL_Translatable_Object::get_objects_with_no_lang_sql()
*
* @since 3.4.3
*
* @return string[] {
* @type string $table Name of the table.
* @type string $id_column Name of the column containing the object's ID.
* @type string $default_alias Default alias corresponding to the object's table.
* }
* @phpstan-return DBInfo
*/
abstract protected function get_db_infos();
}

View File

@@ -0,0 +1,130 @@
<?php
/**
* @package Polylang
*/
/**
* Registry for all translatable objects.
*
* @since 3.4
*
* @phpstan-implements IteratorAggregate<non-empty-string, PLL_Translatable_Object>
* @phpstan-type TranslatedObjectWithTypes PLL_Translated_Object&PLL_Translatable_Object_With_Types_Interface
* @phpstan-type TranslatableObjectWithTypes PLL_Translatable_Object&PLL_Translatable_Object_With_Types_Interface
*/
class PLL_Translatable_Objects implements IteratorAggregate {
/**
* Type of the main translatable object.
*
* @var string
*/
private $main_type = '';
/**
* List of registered objects.
*
* @var PLL_Translatable_Object[] Array keys are the type of translated content (post, term, etc).
*
* @phpstan-var array<non-empty-string, PLL_Translatable_Object>
*/
private $objects = array();
/**
* Registers a translatable object.
*
* @since 3.4
*
* @param PLL_Translatable_Object $object The translatable object to register.
* @return PLL_Translatable_Object
*/
public function register( PLL_Translatable_Object $object ) {
if ( empty( $this->main_type ) ) {
$this->main_type = $object->get_type();
}
if ( ! isset( $this->objects[ $object->get_type() ] ) ) {
$this->objects[ $object->get_type() ] = $object;
}
return $this->objects[ $object->get_type() ];
}
/**
* Returns all registered translatable objects.
*
* @since 3.4
*
* @return ArrayIterator Iterator on $objects array property. Keys are the type of translated content (post, term, etc).
*
* @phpstan-return ArrayIterator<string, PLL_Translatable_Object>
*/
#[\ReturnTypeWillChange]
public function getIterator() {
return new ArrayIterator( $this->objects );
}
/**
* Returns a translatable object, given an object type.
*
* @since 3.4
*
* @param string $object_type The object type.
* @return PLL_Translatable_Object|null
*
* @phpstan-return (
* $object_type is 'post' ? TranslatedObjectWithTypes : (
* $object_type is 'term' ? TranslatedObjectWithTypes : (
* TranslatedObjectWithTypes|TranslatableObjectWithTypes|PLL_Translated_Object|PLL_Translatable_Object|null
* )
* )
* )
*/
public function get( $object_type ) {
if ( ! isset( $this->objects[ $object_type ] ) ) {
return null;
}
return $this->objects[ $object_type ];
}
/**
* Returns all translatable objects except post one.
*
* @since 3.4
*
* @return PLL_Translatable_Object[] An array of secondary translatable objects. Array keys are the type of translated content (post, term, etc).
*
* @phpstan-return array<non-empty-string, PLL_Translatable_Object>
*/
public function get_secondary_translatable_objects() {
return array_diff_key( $this->objects, array( $this->main_type => null ) );
}
/**
* Returns taxonomy names to manage language and translations.
*
* @since 3.4
*
* @param string[] $filter An array on value to filter taxonomy names to return.
* @return string[] Taxonomy names.
*
* @phpstan-param array<'language'|'translations'> $filter
* @phpstan-return list<non-empty-string>
*/
public function get_taxonomy_names( $filter = array( 'language', 'translations' ) ) {
$taxonomies = array();
foreach ( $this->objects as $object ) {
if ( in_array( 'language', $filter, true ) ) {
$taxonomies[] = $object->get_tax_language();
}
if ( in_array( 'translations', $filter, true ) && $object instanceof PLL_Translated_Object ) {
$taxonomies[] = $object->get_tax_translations();
}
}
return $taxonomies;
}
}

View File

@@ -0,0 +1,404 @@
<?php
/**
* @package Polylang
*/
/**
* Registers and translates strings in an option.
* When a string is updated in an original option, the translations of the old string are assigned to the new original string.
*
* @since 2.9
*/
class PLL_Translate_Option {
/**
* Array of option keys to translate.
*
* @var array
*/
private $keys;
/**
* Used to prevent filtering when retrieving the raw value of the option.
*
* @var bool
*/
private static $raw = false;
/**
* Array of updated strings.
*
* @var array
*/
private $updated_strings = array();
/**
* @var PLL_MO[]
*/
private $translations;
/**
* Cache for the translated values.
*
* @var PLL_Cache<array|string>
*/
private $cache;
/**
* Constructor
*
* @since 2.9
*
* @param string $name Option name.
* @param array $keys Recursive array of option keys to translate in the form:
* @example array(
* 'option_key_to_translate_1' => 1,
* 'option_key_to_translate_2' => 1,
* 'my_group' => array(
* 'sub_key_to_translate_1' => 1,
* 'sub_key_to_translate_2' => 1,
* ),
* )
*
* Note: only keys are interpreted. Any scalar can be used as values.
* @param array $args {
* Optional. Array of arguments for registering the option.
*
* @type string $context The group in which the strings will be registered.
* @type string $sanitize_callback A callback function that sanitizes the option's value.
* }
*/
public function __construct( $name, $keys = array(), $args = array() ) {
$this->cache = new PLL_Cache();
// Registers the strings.
$context = isset( $args['context'] ) ? $args['context'] : 'Polylang';
$this->register_string_recursive( $context, $name, get_option( $name ), $keys );
// Translates the strings.
$this->keys = $keys;
add_filter( 'option_' . $name, array( $this, 'translate' ) ); // Make sure to add this filter after options are registered.
// Filters updated values.
add_filter( 'pre_update_option_' . $name, array( $this, 'pre_update_option' ), 10, 3 );
add_action( 'update_option_' . $name, array( $this, 'update_option' ) );
// Sanitizes translated strings.
if ( empty( $args['sanitize_callback'] ) ) {
add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_option' ), 10, 2 );
} else {
add_filter( 'pll_sanitize_string_translation', $args['sanitize_callback'], 10, 3 );
}
}
/**
* Translates the strings registered for an option.
*
* @since 1.0
*
* @param mixed $value Either a string to translate or a list of strings to translate.
* @return mixed Translated string(s).
*/
public function translate( $value ) {
if ( self::$raw ) {
return $value;
}
if ( empty( $GLOBALS['l10n']['pll_string'] ) || ! $GLOBALS['l10n']['pll_string'] instanceof PLL_MO ) {
return $value;
}
$lang = $GLOBALS['l10n']['pll_string']->get_header( 'Language' );
if ( ! is_string( $lang ) || '' === $lang ) {
return $value;
}
$cache = $this->cache->get( $lang );
if ( false === $cache ) {
$cache = $this->translate_string_recursive( $value, $this->keys );
$this->cache->set( $lang, $cache );
}
return $cache;
}
/**
* Recursively translates the strings registered for an option.
*
* @since 1.0
*
* @param mixed $values Either a string to translate or a list of strings to translate.
* @param array|bool $key Array of option keys to translate.
* @return array|string Translated string(s)
*/
protected function translate_string_recursive( $values, $key ) {
$children = is_array( $key ) ? $key : array();
if ( is_array( $values ) || is_object( $values ) ) {
if ( count( $children ) ) {
foreach ( $children as $name => $child ) {
if ( is_array( $values ) && isset( $values[ $name ] ) ) {
$values[ $name ] = $this->translate_string_recursive( $values[ $name ], $child );
continue;
}
if ( is_object( $values ) && isset( $values->$name ) ) {
$values->$name = $this->translate_string_recursive( $values->$name, $child );
continue;
}
$pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
foreach ( $values as $n => &$value ) {
// The first case could be handled by the next one, but we avoid calls to preg_match here.
if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
$value = $this->translate_string_recursive( $value, $child );
}
}
}
} else {
// Parent key is a wildcard and no sub-key has been whitelisted.
foreach ( $values as &$value ) {
$value = $this->translate_string_recursive( $value, $key );
}
}
} else {
$values = pll__( $values );
}
return $values;
}
/**
* Recursively registers strings for an option.
*
* @since 1.0
* @since 2.7 Signature modified
*
* @param string $context The group in which the strings will be registered.
* @param string $option Option name.
* @param mixed $values Option value.
* @param array|bool $key Array of option keys to translate.
* @return void
*/
protected function register_string_recursive( $context, $option, $values, $key ) {
if ( is_object( $values ) ) {
$values = (array) $values;
}
if ( is_array( $values ) ) {
$children = is_array( $key ) ? $key : array();
if ( count( $children ) ) {
foreach ( $children as $name => $child ) {
if ( isset( $values[ $name ] ) ) {
$this->register_string_recursive( $context, $name, $values[ $name ], $child );
continue;
}
$pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
foreach ( $values as $n => $value ) {
// The first case could be handled by the next one, but we avoid calls to preg_match here.
if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
$this->register_string_recursive( $context, $n, $value, $child );
}
}
}
} else {
foreach ( $values as $n => $value ) {
// Parent key is a wildcard and no sub-key has been whitelisted.
$this->register_string_recursive( $context, $n, $value, $key );
}
}
} else {
PLL_Admin_Strings::register_string( $option, $values, $context, true );
}
}
/**
* Returns the raw value of an option (without this class' filter).
*
* A static property is used to make sure that the option is not filtered
* whatever the number of instances of this class filtering the option.
*
* @since 3.3
*
* @param string $option_name Option name.
* @return mixed
*/
protected function get_raw_option( $option_name ) {
self::$raw = true;
$option_value = get_option( $option_name );
self::$raw = false;
return $option_value;
}
/**
* Filters an option before it is updated.
*
* This is the step 1 in the update process, in which we prevent the update of
* strings to their translations by filtering them out, and we store the updated strings
* for the next step.
*
* @since 2.9
*
* @param mixed $value The new, unserialized option value.
* @param mixed $old_value The old (filtered) option value.
* @param string $name Option name.
* @return mixed
*/
public function pre_update_option( $value, $old_value, $name ) {
// Stores the unfiltered old option value before it is updated in DB.
$unfiltered_old_value = $this->get_raw_option( $name );
$languages = PLL()->model->get_languages_list();
if ( empty( $languages ) ) {
return $value;
}
// Load translations in all languages.
foreach ( $languages as $language ) {
$this->translations[ $language->slug ] = new PLL_MO();
$this->translations[ $language->slug ]->import_from_db( $language );
}
$lang = pll_current_language();
if ( empty( $lang ) ) {
$lang = pll_default_language();
}
if ( empty( $lang ) ) {
return $value; // Something's wrong.
}
// Filters out the strings which would be updated to their translations and stores the updated strings.
$value = $this->check_value_recursive( $unfiltered_old_value, $value, $this->keys, $this->translations[ $lang ] );
return $value;
}
/**
* Updates the string translations to keep the same translated value when updating the original option.
*
* This is the step 2 in the update process. Knowing all strings that have been updated,
* we remove the old strings from the strings translations and replace them by
* the new strings with the old translations.
*
* @since 2.9
*
* @return void
*/
public function update_option() {
$curlang = pll_current_language();
if ( ! empty( $this->updated_strings ) ) {
foreach ( PLL()->model->get_languages_list() as $language ) {
$mo = &$this->translations[ $language->slug ];
foreach ( $this->updated_strings as $old_string => $string ) {
$translation = $mo->translate( $old_string );
if ( ( empty( $curlang ) && $translation === $old_string ) || $language->slug === $curlang ) {
$translation = $string;
}
// Add new entry with new string and old translation.
$mo->add_entry( $mo->make_entry( $string, $translation ) );
}
$mo->export_to_db( $language );
}
}
$this->cache->clean();
}
/**
* Recursively compares the updated strings to the translation of the old string.
*
* This is the heart of the update process. If an updated string is found to be
* the same as the translation of the old string, we restore the old string to
* prevent the update in {@see PLL_Translate_Option::pre_update_option()}, otherwise
* the updated string is stored in {@see PLL_Translate_Option::updated_strings} to be able to
* later assign the translations to the new value in {@see PLL_Translate_Option::update_option()}.
*
* @since 2.9
* @since 3.5 Added $mo parameter.
*
* @param mixed $old_values The old option value.
* @param mixed $values The new option value.
* @param array|bool $key Array of option keys to translate.
* @param PLL_MO $mo Translations used to compare the updated string to the translated old string.
* @return mixed
*/
protected function check_value_recursive( $old_values, $values, $key, $mo ) {
$children = is_array( $key ) ? $key : array();
if ( is_array( $values ) || is_object( $values ) ) {
if ( count( $children ) ) {
foreach ( $children as $name => $child ) {
if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $name ], $values[ $name ] ) ) {
$values[ $name ] = $this->check_value_recursive( $old_values[ $name ], $values[ $name ], $child, $mo );
continue;
}
if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$name, $values->$name ) ) {
$values->$name = $this->check_value_recursive( $old_values->$name, $values->$name, $child, $mo );
continue;
}
$pattern = '#^' . str_replace( '*', '(?:.+)', $name ) . '$#';
foreach ( $values as $n => $value ) {
// The first case could be handled by the next one, but we avoid calls to preg_match here.
if ( '*' === $name || ( false !== strpos( $name, '*' ) && preg_match( $pattern, $n ) ) ) {
if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
$values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $child, $mo );
}
if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
$values->$n = $this->check_value_recursive( $old_values->$n, $value, $child, $mo );
}
}
}
}
} else {
// Parent key is a wildcard and no sub-key has been whitelisted.
foreach ( $values as $n => $value ) {
if ( is_array( $values ) && is_array( $old_values ) && isset( $old_values[ $n ] ) ) {
$values[ $n ] = $this->check_value_recursive( $old_values[ $n ], $value, $key, $mo );
}
if ( is_object( $values ) && is_object( $old_values ) && isset( $old_values->$n ) ) {
$values->$n = $this->check_value_recursive( $old_values->$n, $value, $key, $mo );
}
}
}
} elseif ( $old_values !== $values ) {
if ( $mo->translate( $old_values ) === $values ) {
$values = $old_values; // Prevents updating the value to its translation.
} else {
$this->updated_strings[ $old_values ] = $values; // Stores the updated strings.
}
}
return $values;
}
/**
* Sanitizes the option value.
*
* @since 2.9
*
* @param string $value The unsanitised value.
* @param string $name The name of the option.
* @return string Sanitized value.
*/
public function sanitize_option( $value, $name ) {
return sanitize_option( $name, $value );
}
}

View File

@@ -0,0 +1,567 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Abstract class to use for object types that support translations.
*
* @since 1.8
*/
abstract class PLL_Translated_Object extends PLL_Translatable_Object {
/**
* Taxonomy name for the translation groups.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_translations;
/**
* Constructor.
*
* @since 1.8
*
* @param PLL_Model $model Instance of `PLL_Model`.
*/
public function __construct( PLL_Model &$model ) {
parent::__construct( $model );
$this->tax_to_cache[] = $this->tax_translations;
/*
* Register our taxonomy as soon as possible.
*/
register_taxonomy(
$this->tax_translations,
(array) $this->object_type,
array(
'label' => false,
'public' => false,
'query_var' => false,
'rewrite' => false,
'_pll' => true,
'update_count_callback' => '_update_generic_term_count', // Count *all* objects to correctly detect unused terms.
)
);
}
/**
* Returns the translations group taxonomy name.
*
* @since 3.4
*
* @return string
*
* @phpstan-return non-empty-string
*/
public function get_tax_translations() {
return $this->tax_translations;
}
/**
* Assigns a language to an object, taking care of the translations group.
*
* @since 3.4
*
* @param int $id Object ID.
* @param PLL_Language|string|int $lang Language to assign to the object.
* @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
* the object).
*/
public function set_language( $id, $lang ) {
if ( ! parent::set_language( $id, $lang ) ) {
return false;
}
$id = $this->sanitize_int_id( $id );
$translations = $this->get_translations( $id );
// Don't create translation groups with only 1 value.
if ( ! empty( $translations ) ) {
// Remove the object's former language from the new translations group before adding the new value.
$translations = array_diff( $translations, array( $id ) );
$this->save_translations( $id, $translations );
}
return true;
}
/**
* Returns a list of object translations, given a `tax_translations` term ID.
*
* @since 3.2
*
* @param int $term_id A `tax_translations` term ID.
* @return int[] An associative array of translations with language code as key and translation ID as value.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
public function get_translations_from_term_id( $term_id ) {
$term_id = $this->sanitize_int_id( $term_id );
if ( empty( $term_id ) ) {
return array();
}
$translations_term = get_term( $term_id, $this->tax_translations );
if ( ! $translations_term instanceof WP_Term || empty( $translations_term->description ) ) {
return array();
}
// Lang slugs as array keys, translation IDs as array values.
$translations = maybe_unserialize( $translations_term->description );
$translations = is_array( $translations ) ? $translations : array();
return $this->validate_translations( $translations, 0, 'display' );
}
/**
* Saves the object's translations.
*
* @since 0.5
*
* @param int $id Object ID.
* @param int[] $translations An associative array of translations with language code as key and translation ID as value.
* @return int[] An associative array with language codes as key and object IDs as values.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
public function save_translations( $id, array $translations = array() ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return array();
}
$lang = $this->get_language( $id );
if ( empty( $lang ) ) {
return array();
}
// Sanitize and validate the translations array.
$translations = $this->validate_translations( $translations, $id );
// Unlink removed translations.
$old_translations = $this->get_translations( $id );
foreach ( array_diff_assoc( $old_translations, $translations ) as $id ) {
$this->delete_translation( $id );
}
// Check ID we need to create or update the translation group.
if ( ! $this->should_update_translation_group( $id, $translations ) ) {
return $translations;
}
$terms = wp_get_object_terms( $translations, $this->tax_translations );
$term = is_array( $terms ) && ! empty( $terms ) ? reset( $terms ) : false;
if ( empty( $term ) ) {
// Create a new term if necessary.
$group = uniqid( 'pll_' );
wp_insert_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
} else {
// Take care not to overwrite extra data stored in the description field, if any.
$group = (int) $term->term_id;
$descr = maybe_unserialize( $term->description );
$descr = is_array( $descr ) ? array_diff_key( $descr, $old_translations ) : array(); // Remove old translations.
$descr = array_merge( $descr, $translations ); // Add new one.
wp_update_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) );
}
// Link all translations to the new term.
foreach ( $translations as $p ) {
wp_set_object_terms( $p, $group, $this->tax_translations );
}
if ( ! is_array( $terms ) ) {
return $translations;
}
// Clean now unused translation groups.
foreach ( $terms as $term ) {
// Get fresh count value.
$term = get_term( $term->term_id, $this->tax_translations );
if ( $term instanceof WP_Term && empty( $term->count ) ) {
wp_delete_term( $term->term_id, $this->tax_translations );
}
}
return $translations;
}
/**
* Deletes a translation of an object.
*
* @since 0.5
*
* @param int $id Object ID.
* @return void
*/
public function delete_translation( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return;
}
$term = $this->get_object_term( $id, $this->tax_translations );
if ( empty( $term ) ) {
return;
}
$descr = maybe_unserialize( $term->description );
if ( ! empty( $descr ) && is_array( $descr ) ) {
$slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
if ( false !== $slug ) {
unset( $descr[ $slug ] );
}
}
if ( empty( $descr ) || ! is_array( $descr ) ) {
wp_delete_term( (int) $term->term_id, $this->tax_translations );
} else {
wp_update_term( (int) $term->term_id, $this->tax_translations, array( 'description' => maybe_serialize( $descr ) ) );
}
}
/**
* Returns an array of valid translations of an object.
*
* @since 0.5
*
* @param int $id Object ID.
* @return int[] An associative array of translations with language code as key and translation ID as value.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
public function get_translations( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return array();
}
$translations = $this->get_raw_translations( $id );
return $this->validate_translations( $translations, $id, 'display' );
}
/**
* Returns an unvalidated array of translations of an object.
* It is generally preferable to use `get_translations()`.
*
* @since 3.4
*
* @param int $id Object ID.
* @return int[] An associative array of translations with language code as key and translation ID as value.
*
* @phpstan-return array<non-empty-string, positive-int>
*/
public function get_raw_translations( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return array();
}
$term = $this->get_object_term( $id, $this->tax_translations );
if ( empty( $term->description ) ) {
return array();
}
$translations = maybe_unserialize( $term->description );
$translations = is_array( $translations ) ? $translations : array();
return $translations;
}
/**
* Returns the ID of the translation of an object.
*
* @since 0.5
*
* @param int $id Object ID.
* @param PLL_Language|string $lang Language (slug or object).
* @return int|false Object ID of the translation, `false` if there is none.
*
* @phpstan-return positive-int|false
*/
public function get_translation( $id, $lang ) {
$lang = $this->model->get_language( $lang );
if ( empty( $lang ) ) {
return false;
}
$translations = $this->get_translations( $id );
return isset( $translations[ $lang->slug ] ) ? $translations[ $lang->slug ] : false;
}
/**
* Among the object and its translations, returns the ID of the object which is in `$lang`.
*
* @since 0.1
* @since 3.4 Returns 0 instead of false.
*
* @param int $id Object ID.
* @param PLL_Language|string|int $lang Language (object, slug, or term ID).
* @return int The translation object ID if exists, otherwise the passed ID. `0` if the passed object has no language.
*
* @phpstan-return int<0, max>
*/
public function get( $id, $lang ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return 0;
}
$lang = $this->model->get_language( $lang );
if ( empty( $lang ) ) {
return 0;
}
$obj_lang = $this->get_language( $id );
if ( empty( $obj_lang ) ) {
return 0;
}
return $obj_lang->term_id === $lang->term_id ? $id : (int) $this->get_translation( $id, $lang );
}
/**
* Checks if a user can synchronize translations.
*
* @since 2.6
*
* @param int $id Object ID.
* @return bool
*/
public function current_user_can_synchronize( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return false;
}
/**
* Filters whether a synchronization capability check should take place.
*
* @since 2.6
*
* @param bool|null $check Null to enable the capability check,
* true to always allow the synchronization,
* false to always disallow the synchronization.
* Defaults to true.
* @param int $id The synchronization source object ID.
*/
$check = apply_filters( "pll_pre_current_user_can_synchronize_{$this->type}", true, $id );
if ( null !== $check ) {
return (bool) $check;
}
if ( ! current_user_can( "edit_{$this->type}", $id ) ) {
return false;
}
foreach ( $this->get_translations( $id ) as $tr_id ) {
if ( $tr_id !== $id && ! current_user_can( "edit_{$this->type}", $tr_id ) ) {
return false;
}
}
return true;
}
/**
* Tells whether a translation term must be updated.
*
* @since 2.3
*
* @param int $id Object ID.
* @param int[] $translations An associative array of translations with language code as key and translation ID as
* value. Make sure to sanitize this.
* @return bool
*
* @phpstan-param array<non-empty-string, positive-int> $translations
*/
protected function should_update_translation_group( $id, $translations ) {
// Don't do anything if no translations have been added to the group.
$old_translations = $this->get_translations( $id ); // Includes at least $id itself.
return ! empty( array_diff_assoc( $translations, $old_translations ) );
}
/**
* Validates and sanitizes translations.
* This will:
* - Make sure to return only translations in existing languages (and only translations).
* - Sanitize the values.
* - Make sure the provided translation (`$id`) is in the list.
* - Check that the translated objects are in the right language, if `$context` is set to 'save'.
*
* @since 3.1
* @since 3.2 Doesn't return `0` ID values.
* @since 3.2 Added parameters `$id` and `$context`.
*
* @param int[] $translations An associative array of translations with language code as key and translation ID as
* value.
* @param int $id Optional. The object ID for which the translations are validated. When provided, the
* process makes sure it is added to the list. Default 0.
* @param string $context Optional. The operation for which the translations are validated. When set to
* 'save', a check is done to verify that the IDs and langs correspond.
* 'display' should be used otherwise. Default 'save'.
* @return int[]
*
* @phpstan-param non-empty-string $context
* @phpstan-return array<non-empty-string, positive-int>
*/
protected function validate_translations( $translations, $id = 0, $context = 'save' ) {
if ( ! is_array( $translations ) ) {
$translations = array();
}
/**
* Remove translations in non-existing languages, and non-translation data (we allow plugins to store other
* information in the array).
*/
$translations = array_intersect_key(
$translations,
array_flip( $this->model->get_languages_list( array( 'fields' => 'slug' ) ) )
);
// Make sure values are clean before working with them.
/** @phpstan-var array<non-empty-string, positive-int> $translations */
$translations = $this->sanitize_int_ids_list( $translations );
if ( 'save' === $context ) {
/**
* Check that the translated objects are in the right language.
* For better performance, this should be done only when saving the data into the database, not when
* retrieving data from it.
*/
$valid_translations = array();
foreach ( $translations as $lang_slug => $tr_id ) {
$tr_lang = $this->get_language( $tr_id );
if ( ! empty( $tr_lang ) && $tr_lang->slug === $lang_slug ) {
$valid_translations[ $lang_slug ] = $tr_id;
}
}
$translations = $valid_translations;
}
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return $translations;
}
// Make sure to return at least the passed object in its translation array.
$lang = $this->get_language( $id );
if ( empty( $lang ) ) {
return $translations;
}
/** @phpstan-var array<non-empty-string, positive-int> $translations */
return array_merge( array( $lang->slug => $id ), $translations );
}
/**
* Creates translations groups in mass.
*
* @since 1.6.3
* @since 3.4 Moved from PLL_Admin_Model class.
*
* @param int[][] $translations Array of translations arrays.
* @return void
*
* @phpstan-param array<array<string,int>> $translations
*/
public function set_translation_in_mass( $translations ) {
global $wpdb;
$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, $this->tax_translations, $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( array( 'taxonomy' => $this->tax_translations, 'hide_empty' => false ) );
$trs = array();
// Prepare objects relationships.
if ( is_array( $terms ) ) {
foreach ( $terms as $term ) {
$t = maybe_unserialize( $term->description );
if ( is_array( $t ) && 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, $this->tax_translations );
}
}

View File

@@ -0,0 +1,396 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Sets the posts languages and translations model up.
*
* @since 1.8
*
* @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface
*/
class PLL_Translated_Post extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface {
use PLL_Translatable_Object_With_Types_Trait;
/**
* Taxonomy name for the languages.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_language = 'language';
/**
* Identifier that must be unique for each type of content.
* Also used when checking capabilities.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $type = 'post';
/**
* Identifier for each type of content to used for cache type.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $cache_type = 'posts';
/**
* Taxonomy name for the translation groups.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_translations = 'post_translations';
/**
* Constructor.
*
* @since 1.8
*
* @param PLL_Model $model Instance of `PLL_Model`.
*/
public function __construct( PLL_Model &$model ) {
parent::__construct( $model );
// Keep hooks in constructor for backward compatibility.
$this->init();
}
/**
* Adds hooks.
*
* @since 3.4
*
* @return static
*/
public function init() {
// Registers completely the language taxonomy.
add_action( 'setup_theme', array( $this, 'register_taxonomy' ), 1 );
// Setups post types to translate.
add_action( 'registered_post_type', array( $this, 'registered_post_type' ) );
// Forces updating posts cache.
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
return parent::init();
}
/**
* Deletes a translation of a post.
*
* @since 0.5
*
* @param int $id Post ID.
* @return void
*/
public function delete_translation( $id ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return;
}
parent::delete_translation( $id );
wp_set_object_terms( $id, array(), $this->tax_translations );
}
/**
* Returns object types (post types) that need to be translated.
* The post types list is cached for better performance.
* The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php.
*
* @since 3.4
*
* @param bool $filter True if we should return only valid registered object types.
* @return string[] Object type names for which Polylang manages languages.
*
* @phpstan-return array<non-empty-string, non-empty-string>
*/
public function get_translated_object_types( $filter = true ) {
$post_types = $this->model->cache->get( 'post_types' );
if ( false === $post_types ) {
$post_types = array( 'post' => 'post', 'page' => 'page', 'wp_block' => 'wp_block' );
if ( ! empty( $this->model->options['post_types'] ) && is_array( $this->model->options['post_types'] ) ) {
$post_types = array_merge( $post_types, array_combine( $this->model->options['post_types'], $this->model->options['post_types'] ) );
}
if ( empty( $this->model->options['media_support'] ) ) {
// In case the post type attachment is stored in the option.
unset( $post_types['attachment'] );
} else {
$post_types['attachment'] = 'attachment';
}
/**
* Filters the list of post types available for translation.
* The default are post types which have the parameter public set to true.
* The filter must be added soon in the WordPress loading process:
* in a function hooked to plugins_loaded or directly in functions.php for themes.
*
* @since 0.8
*
* @param string[] $post_types List of post type names (as array keys and values).
* @param bool $is_settings True when displaying the list of custom post types in Polylang settings.
*/
$post_types = (array) apply_filters( 'pll_get_post_types', $post_types, false );
if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) {
$this->model->cache->set( 'post_types', $post_types );
}
}
/** @var array<non-empty-string, non-empty-string> $post_types */
return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
}
/**
* Returns true if Polylang manages languages for this object type.
*
* @since 3.4
*
* @param string|string[] $object_type Object type (post type) name or array of object type names.
* @return bool
*
* @phpstan-param non-empty-string|non-empty-string[] $object_type
*/
public function is_translated_object_type( $object_type ) {
$post_types = $this->get_translated_object_types( false );
return ( is_array( $object_type ) && array_intersect( $object_type, $post_types ) || in_array( $object_type, $post_types ) || 'any' === $object_type && ! empty( $post_types ) );
}
/**
* Registers the language taxonomy.
*
* @since 1.2
*
* @return void
*/
public function register_taxonomy() {
register_taxonomy(
$this->tax_language,
$this->model->get_translated_post_types(),
array(
'public' => false,
'show_ui' => false, // Hide the taxonomy on admin side, needed for WP 4.4.x.
'show_in_nav_menus' => false, // No metabox for nav menus, needed for WP 4.4.x.
'publicly_queryable' => true, // Since WP 4.5.
'query_var' => 'lang',
'rewrite' => false, // Rewrite rules are added through filters when needed.
'_pll' => true, // Polylang taxonomy.
)
);
}
/**
* Checks if registered post type must be translated.
*
* @since 1.2
*
* @param string $post_type Post type name.
* @return void
*
* @phpstan-param non-empty-string $post_type
*/
public function registered_post_type( $post_type ) {
if ( $this->model->is_translated_post_type( $post_type ) ) {
register_taxonomy_for_object_type( $this->tax_language, $post_type );
register_taxonomy_for_object_type( $this->tax_translations, $post_type );
}
}
/**
* Forces calling 'update_object_term_cache' when querying posts or pages.
* This is especially useful for nav menus with a lot of pages as, without doing this,
* we would have one query per page in the menu to get the page language for the permalink.
*
* @since 1.8
*
* @param WP_Query $query Reference to the query object.
* @return void
*/
public function pre_get_posts( $query ) {
if ( ! empty( $query->query['post_type'] ) && $this->model->is_translated_post_type( $query->query['post_type'] ) ) {
$query->query_vars['update_post_term_cache'] = true;
}
}
/**
* Checks if the current user can read the post.
*
* @since 1.5
* @since 3.4 Renamed the parameter $post_id into $id.
*
* @param int $id Post ID
* @param string $context Optional, 'edit' or 'view'. Defaults to 'view'.
* @return bool
*
* @phpstan-param non-empty-string $context
*/
public function current_user_can_read( $id, $context = 'view' ) {
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return false;
}
$post = get_post( $id );
if ( empty( $post ) ) {
return false;
}
if ( 'inherit' === $post->post_status && $post->post_parent ) {
$post = get_post( $post->post_parent );
if ( empty( $post ) ) {
return false;
}
}
if ( 'inherit' === $post->post_status || in_array( $post->post_status, get_post_stati( array( 'public' => true ) ) ) ) {
return true;
}
// Follow WP practices, which shows links to private posts ( when readable ), but not for draft posts ( ex: get_adjacent_post_link() )
if ( in_array( $post->post_status, get_post_stati( array( 'private' => true ) ) ) ) {
if ( ! is_user_logged_in() ) {
return false;
}
$user = wp_get_current_user();
if ( (int) $user->ID === (int) $post->post_author ) {
return true;
}
$post_type_object = get_post_type_object( $post->post_type );
return ! empty( $post_type_object ) && current_user_can( $post_type_object->cap->read_private_posts );
}
// In edit context, show draft and future posts.
if ( 'edit' === $context ) {
$states = get_post_stati(
array(
'protected' => true,
'show_in_admin_all_list' => true,
)
);
if ( in_array( $post->post_status, $states ) ) {
$user = wp_get_current_user();
return is_user_logged_in() && ( current_user_can( 'edit_posts' ) || (int) $user->ID === (int) $post->post_author );
}
}
return false;
}
/**
* Returns a list of posts in a language ($lang) not translated in another language ($untranslated_in).
*
* @since 2.6
*
* @param string $type Post type.
* @param PLL_Language $untranslated_in The language the posts must not be translated in.
* @param PLL_Language $lang Language of the searched posts.
* @param string $search Limit the results to the posts matching this string.
* @return WP_Post[] Array of posts.
*/
public function get_untranslated( $type, PLL_Language $untranslated_in, PLL_Language $lang, $search = '' ) {
global $wpdb;
$args = array( 'numberposts' => 20 ); // Limit to 20 posts by default.
/**
* Filters the query args when auto suggesting untranslated posts in the Languages metabox.
*
* @since 1.7
* @since 3.4 Handled arguments restricted to `numberposts` to limit queried posts.
* No `WP_Query` is made anymore, a custom one is used instead.
*
* @param array $args WP_Query arguments
*/
$args = apply_filters( 'pll_ajax_posts_not_translated_args', $args );
$limit = $args['numberposts'];
$search_like = '%' . $wpdb->esc_like( $search ) . '%';
$untranslated_like = '%' . $wpdb->esc_like( $untranslated_in->slug ) . '%';
$posts = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
INNER JOIN {$wpdb->term_relationships} AS tr1 ON ({$wpdb->posts}.ID = tr1.object_id)
AND tr1.term_taxonomy_id IN (%d)
AND (({$wpdb->posts}.post_title LIKE %s)
OR ({$wpdb->posts}.post_excerpt LIKE %s)
OR ({$wpdb->posts}.post_content LIKE %s)
)
AND {$wpdb->posts}.post_type = %s
AND {$wpdb->posts}.post_status NOT IN ('trash', 'auto-draft')
WHERE {$wpdb->posts}.ID NOT IN (
SELECT {$wpdb->posts}.ID FROM {$wpdb->posts}
LEFT JOIN {$wpdb->term_relationships} AS tr2 ON ({$wpdb->posts}.ID = tr2.object_id)
INNER JOIN {$wpdb->term_taxonomy} AS tt ON (tt.term_taxonomy_id = tr2.term_taxonomy_id)
AND (tt.taxonomy = 'post_translations')
AND tt.description LIKE %s
)
LIMIT 0, %d",
$lang->get_tax_prop( $this->tax_language, 'term_taxonomy_id' ),
$search_like,
$search_like,
$search_like,
$type,
$untranslated_like,
$limit
)
);
foreach ( $posts as $i => $post ) {
if ( ! $this->current_user_can_read( $post->ID, 'edit' ) ) {
unset( $posts[ $i ] );
continue;
}
$posts[ $i ] = new WP_Post( $post );
}
return $posts;
}
/**
* Returns database-related information that can be used in some of this class methods.
* These are specific to the table containing the objects.
*
* @see PLL_Translatable_Object::join_clause()
* @see PLL_Translatable_Object::get_objects_with_no_lang_sql()
*
* @since 3.4.3
*
* @return string[] {
* @type string $table Name of the table.
* @type string $id_column Name of the column containing the object's ID.
* @type string $type_column Name of the column containing the object's type.
* @type string $default_alias Default alias corresponding to the object's table.
* }
* @phpstan-return DBInfoWithType
*/
protected function get_db_infos() {
return array(
'table' => $GLOBALS['wpdb']->posts,
'id_column' => 'ID',
'type_column' => 'post_type',
'default_alias' => $GLOBALS['wpdb']->posts,
);
}
}

View File

@@ -0,0 +1,341 @@
<?php
/**
* @package Polylang
*/
defined( 'ABSPATH' ) || exit;
/**
* Sets the taxonomies languages and translations model up.
*
* @since 1.8
*
* @phpstan-import-type DBInfoWithType from PLL_Translatable_Object_With_Types_Interface
*/
class PLL_Translated_Term extends PLL_Translated_Object implements PLL_Translatable_Object_With_Types_Interface {
use PLL_Translatable_Object_With_Types_Trait;
/**
* Taxonomy name for the languages.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_language = 'term_language';
/**
* Object type to use when registering the taxonomy.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $object_type = 'term';
/**
* Identifier that must be unique for each type of content.
* Also used when checking capabilities.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $type = 'term';
/**
* Identifier for each type of content to used for cache type.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $cache_type = 'terms';
/**
* Taxonomy name for the translation groups.
*
* @var string
*
* @phpstan-var non-empty-string
*/
protected $tax_translations = 'term_translations';
/**
* Constructor.
*
* @since 1.8
*
* @param PLL_Model $model Instance of `PLL_Model`.
*/
public function __construct( PLL_Model &$model ) {
parent::__construct( $model );
// Keep hooks in constructor for backward compatibility.
$this->init();
}
/**
* Adds hooks.
*
* @since 3.4
*
* @return static
*/
public function init() {
add_filter( 'get_terms', array( $this, '_prime_terms_cache' ), 10, 2 );
add_action( 'clean_term_cache', array( $this, 'clean_term_cache' ) );
return parent::init();
}
/**
* Stores the term's language into the database.
*
* @since 0.6
* @since 3.4 Renamed the parameter $term_id into $id.
*
* @param int $id Term ID.
* @param PLL_Language|string|int $lang Language (object, slug, or term ID).
* @return bool True when successfully assigned. False otherwise (or if the given language is already assigned to
* the object).
*/
public function set_language( $id, $lang ) {
if ( ! parent::set_language( $id, $lang ) ) {
return false;
}
$id = $this->sanitize_int_id( $id );
// Add translation group for correct WXR export.
$translations = $this->get_translations( $id );
if ( ! empty( $translations ) ) {
$translations = array_diff( $translations, array( $id ) );
}
$this->save_translations( $id, $translations );
return true;
}
/**
* Returns the language of a term.
*
* @since 0.1
* @since 3.4 Renamed the parameter $value into $id.
* @since 3.4 Deprecated to retrieve the language by term slug + taxonomy anymore.
*
* @param int $id Term ID.
* @return PLL_Language|false A `PLL_Language` object. `false` if no language is associated to that term or if the
* ID is invalid.
*/
public function get_language( $id ) {
if ( func_num_args() > 1 ) {
// Backward compatibility.
_deprecated_argument( __METHOD__ . '()', '3.4' );
$term = get_term_by( 'slug', $id, func_get_arg( 1 ) ); // @phpstan-ignore-line
$id = $term instanceof WP_Term ? $term->term_id : 0;
}
return parent::get_language( $id );
}
/**
* Deletes a translation of a term.
*
* @since 0.5
*
* @param int $id Term ID.
* @return void
*/
public function delete_translation( $id ) {
global $wpdb;
$id = $this->sanitize_int_id( $id );
if ( empty( $id ) ) {
return;
}
$slug = array_search( $id, $this->get_translations( $id ) ); // In case some plugin stores the same value with different key.
parent::delete_translation( $id );
wp_delete_object_term_relationships( $id, $this->tax_translations );
if ( doing_action( 'pre_delete_term' ) ) {
return;
}
if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM $wpdb->terms WHERE term_id = %d;", $id ) ) ) {
return;
}
// Always keep a group for terms to allow relationships remap when importing from a WXR file.
$group = uniqid( 'pll_' );
$translations = array( $slug => $id );
wp_insert_term( $group, $this->tax_translations, array( 'description' => maybe_serialize( $translations ) ) );
wp_set_object_terms( $id, $group, $this->tax_translations );
}
/**
* Returns object types (taxonomy names) that need to be translated.
* The taxonomies list is cached for better performance.
* The method waits for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php.
*
* @since 3.4
*
* @param bool $filter True if we should return only valid registered object types.
* @return string[] Object type names for which Polylang manages languages.
*
* @phpstan-return array<non-empty-string, non-empty-string>
*/
public function get_translated_object_types( $filter = true ) {
$taxonomies = $this->model->cache->get( 'taxonomies' );
if ( false === $taxonomies ) {
$taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
if ( ! empty( $this->model->options['taxonomies'] ) && is_array( $this->model->options['taxonomies'] ) ) {
$taxonomies = array_merge( $taxonomies, array_combine( $this->model->options['taxonomies'], $this->model->options['taxonomies'] ) );
}
/**
* Filters the list of taxonomies available for translation.
* The default are taxonomies which have the parameter public set to true.
* The filter must be added soon in the WordPress loading process:
* in a function hooked to plugins_loaded or directly in functions.php for themes.
*
* @since 0.8
*
* @param string[] $taxonomies List of taxonomy names (as array keys and values).
* @param bool $is_settings True when displaying the list of custom taxonomies in Polylang settings.
*/
$taxonomies = (array) apply_filters( 'pll_get_taxonomies', $taxonomies, false );
if ( did_action( 'after_setup_theme' ) && ! doing_action( 'switch_blog' ) ) {
$this->model->cache->set( 'taxonomies', $taxonomies );
}
}
/** @var array<non-empty-string, non-empty-string> $taxonomies */
return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
}
/**
* Caches the language and translations when terms are queried by get_terms().
*
* @since 1.2
*
* @param WP_Term[]|int[] $terms Queried terms.
* @param string[] $taxonomies Queried taxonomies.
* @return WP_Term[]|int[] Unmodified $terms.
*
* @phpstan-param array<WP_Term|positive-int> $terms
* @phpstan-param array<non-empty-string> $taxonomies
* @phpstan-return array<WP_Term|positive-int>
*/
public function _prime_terms_cache( $terms, $taxonomies ) {
$ids = array();
if ( is_array( $terms ) && $this->model->is_translated_taxonomy( $taxonomies ) ) {
foreach ( $terms as $term ) {
$ids[] = is_object( $term ) ? $term->term_id : (int) $term;
}
}
if ( ! empty( $ids ) ) {
update_object_term_cache( array_unique( $ids ), 'term' ); // Adds language and translation of terms to cache.
}
return $terms;
}
/**
* When the term cache is cleaned, cleans the object term cache too.
*
* @since 2.0
*
* @param int[] $ids An array of term IDs.
* @return void
*
* @phpstan-param array<positive-int> $ids
*/
public function clean_term_cache( $ids ) {
clean_object_term_cache( $this->sanitize_int_ids_list( $ids ), 'term' );
}
/**
* Tells whether a translation term must be updated.
*
* @since 2.3
*
* @param int $id Term ID.
* @param int[] $translations An associative array of translations with language code as key and translation ID as
* value. Make sure to sanitize this.
* @return bool
*
* @phpstan-param array<non-empty-string, positive-int> $translations
*/
protected function should_update_translation_group( $id, $translations ) {
// Don't do anything if no translations have been added to the group.
$old_translations = $this->get_translations( $id );
if ( count( $translations ) > 1 && ! empty( array_diff_assoc( $translations, $old_translations ) ) ) {
return true;
}
// But we need a translation group for terms to allow relationships remap when importing from a WXR file
$term = $this->get_object_term( $id, $this->tax_translations );
return empty( $term ) || ! empty( array_diff_assoc( $translations, $old_translations ) );
}
/**
* Assigns a language to terms in mass.
*
* @since 1.2
* @since 3.4 Moved from PLL_Admin_Model class.
*
* @param int[] $ids Array of post ids or term ids.
* @param PLL_Language $lang Language to assign to the posts or terms.
* @return void
*/
public function set_language_in_mass( $ids, $lang ) {
parent::set_language_in_mass( $ids, $lang );
$translations = array();
foreach ( $ids as $id ) {
$translations[] = array( $lang->slug => $id );
}
if ( ! empty( $translations ) ) {
$this->set_translation_in_mass( $translations );
}
}
/**
* Returns database-related information that can be used in some of this class methods.
* These are specific to the table containing the objects.
*
* @see PLL_Translatable_Object::join_clause()
* @see PLL_Translatable_Object::get_objects_with_no_lang_sql()
*
* @since 3.4.3
*
* @return string[] {
* @type string $table Name of the table.
* @type string $id_column Name of the column containing the object's ID.
* @type string $type_column Name of the column containing the object's type.
* @type string $default_alias Default alias corresponding to the object's table.
* }
* @phpstan-return DBInfoWithType
*/
protected function get_db_infos() {
return array(
'table' => $GLOBALS['wpdb']->term_taxonomy,
'id_column' => 'term_id',
'type_column' => 'taxonomy',
'default_alias' => 't',
);
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @package Polylang
*/
/**
* Displays languages in a dropdown list
*
* @since 1.2
* @since 3.4 Extends `PLL_Walker` now.
*/
class PLL_Walker_Dropdown extends PLL_Walker {
/**
* Database fields to use.
*
* @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields.
*
* @var string[]
*/
public $db_fields = array( 'parent' => 'parent', 'id' => 'id' );
/**
* Outputs one element.
*
* @since 1.2
*
* @param string $output Passed by reference. Used to append additional content.
* @param stdClass $element The data object.
* @param int $depth Depth of the item.
* @param array $args An array of additional arguments.
* @param int $current_object_id ID of the current item.
* @return void
*/
public function start_el( &$output, $element, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$value_type = $args['value'];
$output .= sprintf(
"\t" . '<option value="%1$s"%2$s%3$s>%4$s</option>' . "\n",
'url' === $value_type ? esc_url( $element->$value_type ) : esc_attr( $element->$value_type ),
! empty( $element->locale ) ? sprintf( ' lang="%s"', esc_attr( $element->locale ) ) : '',
selected( isset( $args['selected'] ) && $args['selected'] === $element->$value_type, true, false ),
esc_html( $element->name )
);
}
/**
* Starts the output of the dropdown list
*
* @since 1.2
* @since 2.6.7 Use $max_depth and ...$args parameters to follow the move of WP 5.3
*
* List of parameters accepted in $args:
*
* flag => display the selected language flag in front of the dropdown if set to 1, defaults to 0
* value => the language field to use as value attribute, defaults to 'slug'
* selected => the selected value, mandatory
* name => the select name attribute, defaults to 'lang_choice'
* id => the select id attribute, defaults to $args['name']
* class => the class attribute
* disabled => disables the dropdown if set to 1
*
* @param array $elements An array of `PLL_language` or `stdClass` elements.
* @param int $max_depth The maximum hierarchical depth.
* @param mixed ...$args Additional arguments.
* @return string The hierarchical item output.
*
* @phpstan-param array<PLL_Language|stdClass> $elements
*/
public function walk( $elements, $max_depth, ...$args ) { // // phpcs:ignore WordPressVIPMinimum.Classes.DeclarationCompatibility.DeclarationCompatibility
$output = '';
$this->maybe_fix_walk_args( $max_depth, $args );
$args = wp_parse_args( $args, array( 'value' => 'slug', 'name' => 'lang_choice' ) );
if ( ! empty( $args['flag'] ) ) {
$current = wp_list_filter( $elements, array( $args['value'] => $args['selected'] ) );
$lang = reset( $current );
$output = sprintf(
'<span class="pll-select-flag">%s</span>',
empty( $lang->flag ) ? esc_html( $lang->slug ) : $lang->flag
);
}
$output .= sprintf(
'<select name="%1$s"%2$s%3$s%4$s>' . "\n" . '%5$s' . "\n" . '</select>' . "\n",
esc_attr( $args['name'] ),
isset( $args['id'] ) && ! $args['id'] ? '' : ' id="' . ( empty( $args['id'] ) ? esc_attr( $args['name'] ) : esc_attr( $args['id'] ) ) . '"',
empty( $args['class'] ) ? '' : ' class="' . esc_attr( $args['class'] ) . '"',
disabled( empty( $args['disabled'] ), false, false ),
parent::walk( $elements, $max_depth, $args )
);
return $output;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* @package Polylang
*/
/**
* Displays a language list
*
* @since 1.2
* @since 3.4 Extends `PLL_Walker` now.
*/
class PLL_Walker_List extends PLL_Walker {
/**
* Database fields to use.
*
* @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields.
*
* @var string[]
*/
public $db_fields = array( 'parent' => 'parent', 'id' => 'id' );
/**
* Outputs one element
*
* @since 1.2
*
* @param string $output Passed by reference. Used to append additional content.
* @param stdClass $element The data object.
* @param int $depth Depth of the item.
* @param array $args An array of additional arguments.
* @param int $current_object_id ID of the current item.
* @return void
*/
public function start_el( &$output, $element, $depth = 0, $args = array(), $current_object_id = 0 ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$output .= sprintf(
'%6$s<li class="%1$s"><a %8$s lang="%2$s" hreflang="%2$s" href="%3$s">%4$s%5$s</a></li>%7$s',
esc_attr( implode( ' ', $element->classes ) ),
esc_attr( $element->locale ),
esc_url( $element->url ),
$element->flag,
$args['show_flags'] && $args['show_names'] ? sprintf( '<span style="margin-%1$s:0.3em;">%2$s</span>', is_rtl() ? 'right' : 'left', esc_html( $element->name ) ) : esc_html( $element->name ),
'discard' === $args['item_spacing'] ? '' : "\t",
'discard' === $args['item_spacing'] ? '' : "\n",
empty( $element->link_classes ) ? '' : 'class="' . esc_attr( implode( ' ', $element->link_classes ) ) . '"'
);
}
/**
* Overrides Walker:walk to set depth argument
*
* @since 1.2
* @since 2.6.7 Use $max_depth and ...$args parameters to follow the move of WP 5.3
*
* @param array $elements An array of elements.
* @param int $max_depth The maximum hierarchical depth.
* @param mixed ...$args Additional arguments.
* @return string The hierarchical item output.
*/
public function walk( $elements, $max_depth, ...$args ) { // phpcs:ignore WordPressVIPMinimum.Classes.DeclarationCompatibility.DeclarationCompatibility
$this->maybe_fix_walk_args( $max_depth, $args );
return parent::walk( $elements, $max_depth, $args );
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* @package Polylang
*/
/**
* A class for displaying various tree-like language structures.
*
* Extend the `PLL_Walker` class to use it, and implement some of the methods from `Walker`.
* See: {https://developer.wordpress.org/reference/classes/walker/#methods}.
*
* @since 3.4
*/
class PLL_Walker extends Walker {
/**
* Database fields to use.
*
* @see https://developer.wordpress.org/reference/classes/walker/#properties Walker::$db_fields.
*
* @var string[]
*/
public $db_fields = array( 'parent' => 'parent', 'id' => 'id' );
/**
* Overrides Walker::display_element as it expects an object with a parent property.
*
* @since 1.2
* @since 3.4 Refactored and moved in `PLL_Walker`.
*
* @param PLL_Language|stdClass $element Data object. `PLL_language` in our case.
* @param array $children_elements List of elements to continue traversing.
* @param int $max_depth Max depth to traverse.
* @param int $depth Depth of current element.
* @param array $args An array of arguments.
* @param string $output Passed by reference. Used to append additional content.
* @return void
*/
public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
if ( $element instanceof PLL_Language ) {
$element = $element->to_std_class();
}
$element->parent = $element->id = 0; // Don't care about this.
parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
}
/**
* Sets `PLL_Walker::walk()` arguments as it should
* and triggers an error in case of misuse of them.
*
* @since 3.4
*
* @param array|int $max_depth The maximum hierarchical depth. Passed by reference.
* @param array $args Additional arguments. Passed by reference.
* @return void
*/
protected function maybe_fix_walk_args( &$max_depth, &$args ) {
if ( ! is_array( $max_depth ) ) {
$args = isset( $args[0] ) ? $args[0] : array();
return;
}
// Backward compatibility with Polylang < 2.6.7
_doing_it_wrong(
__CLASS__ . '::walk()',
'The method expects an integer as second parameter.',
'2.6.7'
);
$args = $max_depth;
$max_depth = -1;
}
}

View File

@@ -0,0 +1,282 @@
<?php
/**
* @package Polylang
*/
if ( ! class_exists( 'WP_Widget_Calendar' ) ) {
require_once ABSPATH . '/wp-includes/default-widgets.php';
}
/**
* This classes rewrite the whole Calendar widget functionality as there is no filter on sql queries and only a filter on final output.
* Code last checked: WP 5.5.
*
* A request to add filters on sql queries exists: http://core.trac.wordpress.org/ticket/15202.
* Method used in 0.4.x: use of the get_calendar filter and overwrite the output of get_calendar function -> not very efficient (add 4 to 5 sql queries).
* Method used since 0.5: remove the WP widget and replace it by our own -> our language filter will not work if get_calendar is called directly by a theme.
*
* @since 0.5
*/
class PLL_Widget_Calendar extends WP_Widget_Calendar {
protected static $pll_instance = 0; // Can't use $instance of WP_Widget_Calendar as it's private :/.
/**
* Outputs the content for the current Calendar widget instance.
* Modified version of the parent function to call our own get_calendar() method.
*
* @since 0.5
*
* @param array $args Display arguments including 'before_title', 'after_title',
* 'before_widget', and 'after_widget'.
* @param array $instance The settings for the particular instance of the widget.
*/
public function widget( $args, $instance ) {
$title = ! empty( $instance['title'] ) ? $instance['title'] : '';
/** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
echo $args['before_widget'];
if ( $title ) {
echo $args['before_title'] . $title . $args['after_title'];
}
if ( 0 === self::$pll_instance ) { #modified#
echo '<div id="calendar_wrap" class="calendar_wrap">';
} else {
echo '<div class="calendar_wrap">';
}
empty( PLL()->curlang ) ? get_calendar() : self::get_calendar(); #modified#
echo '</div>';
echo $args['after_widget'];
++self::$pll_instance; #modified#
}
/**
* Modified version of the WP get_calendar() function to filter the queries.
*
* @since 0.5
*
* @param bool $initial Optional, default is true. Use initial calendar names.
* @param bool $display Optional, default is true. Set to false for return.
* @return void|string Void if `$display` argument is true, calendar HTML if `$display` is false.
*/
static public function get_calendar( $initial = true, $display = true ) {
global $wpdb, $m, $monthnum, $year, $wp_locale, $posts;
$join_clause = PLL()->model->post->join_clause(); #added#
$where_clause = PLL()->model->post->where_clause( PLL()->curlang ); #added#
$key = md5( PLL()->curlang->slug . $m . $monthnum . $year ); #modified#
$cache = wp_cache_get( 'get_calendar', 'calendar' );
if ( $cache && is_array( $cache ) && isset( $cache[ $key ] ) ) {
/** This filter is documented in wp-includes/general-template.php */
$output = apply_filters( 'get_calendar', $cache[ $key ] );
if ( $display ) {
echo $output;
return;
}
return $output;
}
if ( ! is_array( $cache ) ) {
$cache = array();
}
// Quick check. If we have no posts at all, abort!
if ( ! $posts ) {
$gotsome = $wpdb->get_var( "SELECT 1 as test FROM $wpdb->posts WHERE post_type = 'post' AND post_status = 'publish' LIMIT 1" );
if ( ! $gotsome ) {
$cache[ $key ] = '';
wp_cache_set( 'get_calendar', $cache, 'calendar' );
return;
}
}
if ( isset( $_GET['w'] ) ) {
$w = (int) $_GET['w'];
}
// week_begins = 0 stands for Sunday.
$week_begins = (int) get_option( 'start_of_week' );
// Let's figure out when we are.
if ( ! empty( $monthnum ) && ! empty( $year ) ) {
$thismonth = zeroise( (int) $monthnum, 2 );
$thisyear = (int) $year;
} elseif ( ! empty( $w ) ) {
// We need to get the month from MySQL.
$thisyear = (int) substr( $m, 0, 4 );
// It seems MySQL's weeks disagree with PHP's.
$d = ( ( $w - 1 ) * 7 ) + 6;
$thismonth = $wpdb->get_var( "SELECT DATE_FORMAT((DATE_ADD('{$thisyear}0101', INTERVAL $d DAY) ), '%m')" );
} elseif ( ! empty( $m ) ) {
$thisyear = (int) substr( $m, 0, 4 );
if ( strlen( $m ) < 6 ) {
$thismonth = '01';
} else {
$thismonth = zeroise( (int) substr( $m, 4, 2 ), 2 );
}
} else {
$thisyear = current_time( 'Y' );
$thismonth = current_time( 'm' );
}
$unixmonth = mktime( 0, 0, 0, $thismonth, 1, $thisyear );
$last_day = gmdate( 't', $unixmonth );
// Get the next and previous month and year with at least one post.
$previous = $wpdb->get_row(
"SELECT MONTH(post_date) AS month, YEAR(post_date) AS year
FROM $wpdb->posts $join_clause
WHERE post_date < '$thisyear-$thismonth-01'
AND post_type = 'post' AND post_status = 'publish' $where_clause
ORDER BY post_date DESC
LIMIT 1"
); #modified#
$next = $wpdb->get_row(
"SELECT MONTH(post_date) AS month, YEAR(post_date) AS year
FROM $wpdb->posts $join_clause
WHERE post_date > '$thisyear-$thismonth-{$last_day} 23:59:59'
AND post_type = 'post' AND post_status = 'publish' $where_clause
ORDER BY post_date ASC
LIMIT 1"
); #modified#
/* translators: Calendar caption: 1: Month name, 2: 4-digit year. */
$calendar_caption = _x( '%1$s %2$s', 'calendar caption' );
$calendar_output = '<table id="wp-calendar" class="wp-calendar-table">
<caption>' . sprintf(
$calendar_caption,
$wp_locale->get_month( $thismonth ),
gmdate( 'Y', $unixmonth )
) . '</caption>
<thead>
<tr>';
$myweek = array();
for ( $wdcount = 0; $wdcount <= 6; $wdcount++ ) {
$myweek[] = $wp_locale->get_weekday( ( $wdcount + $week_begins ) % 7 );
}
foreach ( $myweek as $wd ) {
$day_name = $initial ? $wp_locale->get_weekday_initial( $wd ) : $wp_locale->get_weekday_abbrev( $wd );
$wd = esc_attr( $wd );
$calendar_output .= "\n\t\t<th scope=\"col\" title=\"$wd\">$day_name</th>";
}
$calendar_output .= '
</tr>
</thead>
<tbody>
<tr>';
$daywithpost = array();
// Get days with posts.
$dayswithposts = $wpdb->get_results(
"SELECT DISTINCT DAYOFMONTH(post_date)
FROM $wpdb->posts $join_clause WHERE post_date >= '{$thisyear}-{$thismonth}-01 00:00:00'
AND post_type = 'post' AND post_status = 'publish'
AND post_date <= '{$thisyear}-{$thismonth}-{$last_day} 23:59:59' $where_clause",
ARRAY_N
); #modified#
if ( $dayswithposts ) {
foreach ( (array) $dayswithposts as $daywith ) {
$daywithpost[] = (int) $daywith[0];
}
}
// See how much we should pad in the beginning.
$pad = calendar_week_mod( gmdate( 'w', $unixmonth ) - $week_begins );
if ( 0 != $pad ) {
$calendar_output .= "\n\t\t" . '<td colspan="' . esc_attr( $pad ) . '" class="pad">&nbsp;</td>';
}
$newrow = false;
$daysinmonth = (int) gmdate( 't', $unixmonth );
for ( $day = 1; $day <= $daysinmonth; ++$day ) {
if ( isset( $newrow ) && $newrow ) {
$calendar_output .= "\n\t</tr>\n\t<tr>\n\t\t";
}
$newrow = false;
if ( current_time( 'j' ) == $day &&
current_time( 'm' ) == $thismonth &&
current_time( 'Y' ) == $thisyear ) {
$calendar_output .= '<td id="today">';
} else {
$calendar_output .= '<td>';
}
if ( in_array( $day, $daywithpost, true ) ) {
// Any posts today?
$date_format = gmdate( _x( 'F j, Y', 'daily archives date format' ), strtotime( "{$thisyear}-{$thismonth}-{$day}" ) );
/* translators: Post calendar label. %s: Date. */
$label = sprintf( __( 'Posts published on %s' ), $date_format );
$calendar_output .= sprintf(
'<a href="%s" aria-label="%s">%s</a>',
get_day_link( $thisyear, $thismonth, $day ),
esc_attr( $label ),
$day
);
} else {
$calendar_output .= $day;
}
$calendar_output .= '</td>';
if ( 6 == calendar_week_mod( gmdate( 'w', mktime( 0, 0, 0, $thismonth, $day, $thisyear ) ) - $week_begins ) ) {
$newrow = true;
}
}
$pad = 7 - calendar_week_mod( gmdate( 'w', mktime( 0, 0, 0, $thismonth, $day, $thisyear ) ) - $week_begins );
if ( 0 != $pad && 7 != $pad ) {
$calendar_output .= "\n\t\t" . '<td class="pad" colspan="' . esc_attr( $pad ) . '">&nbsp;</td>';
}
$calendar_output .= "\n\t</tr>\n\t</tbody>";
$calendar_output .= "\n\t</table>";
$calendar_output .= '<nav aria-label="' . __( 'Previous and next months' ) . '" class="wp-calendar-nav">';
if ( $previous ) {
$calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-prev"><a href="' . get_month_link( $previous->year, $previous->month ) . '">&laquo; ' .
$wp_locale->get_month_abbrev( $wp_locale->get_month( $previous->month ) ) .
'</a></span>';
} else {
$calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-prev">&nbsp;</span>';
}
$calendar_output .= "\n\t\t" . '<span class="pad">&nbsp;</span>';
if ( $next ) {
$calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-next"><a href="' . get_month_link( $next->year, $next->month ) . '">' .
$wp_locale->get_month_abbrev( $wp_locale->get_month( $next->month ) ) .
' &raquo;</a></span>';
} else {
$calendar_output .= "\n\t\t" . '<span class="wp-calendar-nav-next">&nbsp;</span>';
}
$calendar_output .= '
</nav>';
$cache[ $key ] = $calendar_output;
wp_cache_set( 'get_calendar', $cache, 'calendar' );
if ( $display ) {
/** This filter is documented in wp-includes/general-template.php */
echo apply_filters( 'get_calendar', $calendar_output );
return;
}
/** This filter is documented in wp-includes/general-template.php */
return apply_filters( 'get_calendar', $calendar_output );
}
}

View File

@@ -0,0 +1,141 @@
<?php
/**
* @package Polylang
*/
/**
* The language switcher widget
*
* @since 0.1
*/
class PLL_Widget_Languages extends WP_Widget {
/**
* Constructor
*
* @since 0.1
*/
public function __construct() {
parent::__construct(
'polylang',
__( 'Language switcher', 'polylang' ),
array(
'description' => __( 'Displays a language switcher', 'polylang' ),
'customize_selective_refresh' => true,
)
);
}
/**
* Displays the widget
*
* @since 0.1
*
* @param array $args Display arguments including before_title, after_title, before_widget, and after_widget.
* @param array $instance The settings for the particular instance of the widget
* @return void
*/
public function widget( $args, $instance ) {
// Sets a unique id for dropdown.
$instance['dropdown'] = empty( $instance['dropdown'] ) ? 0 : $this->id;
$instance['echo'] = 0;
$instance['raw'] = 0;
$list = pll_the_languages( $instance );
if ( $list ) {
$title = empty( $instance['title'] ) ? '' : $instance['title'];
/** This filter is documented in wp-includes/widgets/class-wp-widget-pages.php */
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
if ( $title ) {
echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput
}
// The title may be filtered: Strip out HTML and make sure the aria-label is never empty.
$aria_label = trim( wp_strip_all_tags( $title ) );
if ( ! $aria_label ) {
$aria_label = __( 'Choose a language', 'polylang' );
}
if ( $instance['dropdown'] ) {
echo '<label class="screen-reader-text" for="' . esc_attr( 'lang_choice_' . $instance['dropdown'] ) . '">' . esc_html( $aria_label ) . '</label>';
echo $list; // phpcs:ignore WordPress.Security.EscapeOutput
} else {
$format = current_theme_supports( 'html5', 'navigation-widgets' ) ? 'html5' : 'xhtml';
/** This filter is documented in wp-includes/widgets/class-wp-nav-menu-widget.php */
$format = apply_filters( 'navigation_widgets_format', $format );
if ( 'html5' === $format ) {
echo '<nav aria-label="' . esc_attr( $aria_label ) . '">';
}
echo "<ul>\n" . $list . "</ul>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
if ( 'html5' === $format ) {
echo '</nav>';
}
}
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
}
}
/**
* Updates the widget options
*
* @since 0.4
*
* @param array $new_instance New settings for this instance as input by the user via form()
* @param array $old_instance Old settings for this instance
* @return array Settings to save or bool false to cancel saving
*/
public function update( $new_instance, $old_instance ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$instance = array( 'title' => sanitize_text_field( $new_instance['title'] ) );
foreach ( array_keys( PLL_Switcher::get_switcher_options( 'widget' ) ) as $key ) {
$instance[ $key ] = ! empty( $new_instance[ $key ] ) ? 1 : 0;
}
return $instance;
}
/**
* Displays the widget form.
*
* @since 0.4
*
* @param array $instance Current settings.
* @return string
*/
public function form( $instance ) {
// Default values
$instance = wp_parse_args( (array) $instance, array_merge( array( 'title' => '' ), PLL_Switcher::get_switcher_options( 'widget', 'default' ) ) );
// Title
printf(
'<p><label for="%1$s">%2$s</label><input class="widefat" id="%1$s" name="%3$s" type="text" value="%4$s" /></p>',
esc_attr( $this->get_field_id( 'title' ) ),
esc_html__( 'Title:', 'polylang' ),
esc_attr( $this->get_field_name( 'title' ) ),
esc_attr( $instance['title'] )
);
foreach ( PLL_Switcher::get_switcher_options( 'widget' ) as $key => $str ) {
printf(
'<div%5$s%6$s><input type="checkbox" class="checkbox %7$s" id="%1$s" name="%2$s"%3$s /><label for="%1$s">%4$s</label></div>',
esc_attr( $this->get_field_id( $key ) ),
esc_attr( $this->get_field_name( $key ) ),
checked( $instance[ $key ], true, false ),
esc_html( $str ),
in_array( $key, array( 'show_names', 'show_flags', 'hide_current' ) ) ? sprintf( ' class="no-dropdown-%s"', esc_attr( $this->id ) ) : '',
( ! empty( $instance['dropdown'] ) && in_array( $key, array( 'show_names', 'show_flags', 'hide_current' ) ) ? ' style="display:none;"' : '' ),
esc_attr( 'pll-' . $key )
);
}
return ''; // Because the parent class returns a string, however not used.
}
}