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,112 @@
<?php
/**
* @package Polylang
*/
/**
* Class Accept_Language.
*
* Represents an Accept-Language HTTP Header, as defined in RFC 2616 Section 14.4 {@see https://tools.ietf.org/html/rfc2616.html#section-14.4}.
*
* @since 3.0
*/
class PLL_Accept_Language {
const SUBTAG_PATTERNS = array(
'language' => '(\b[a-z]{2,3}|[a-z]{4}|[a-z]{5-8}\b)',
'language-extension' => '(?:-(\b[a-z]{3}){1,3}\b)?',
'script' => '(?:-(\b[a-z]{4})\b)?',
'region' => '(?:-(\b[a-z]{2}|[0-9]{3})\b)?',
'variant' => '(?:-(\b[0-9][a-z]{1,3}|[a-z][a-z0-9]{4,7})\b)?',
'extension' => '(?:-(\b[a-wy-z]-[a-z0-9]{2,8})\b)?',
'private-use' => '(?:-(\bx-[a-z0-9]{1,8})\b)?',
);
/**
* @var string[] {
* @type string $language Usually 2 or three letters (ISO 639).
* @type string $language-extension Up to three groups of 3 letters.
* @type string $script Four letters.
* @type string $region Either two letters of three digits.
* @type string $variant Either one digit followed by 1 to 3 letters, or a letter followed by 2 to 7 alphanumerical characters.
* @type string $extension One letter that cannot be an 'x', followed by 2 to 8 alphanumerical characters.
* @type string $private-use Starts by 'x-', followed by 1 to 8 alphanumerical characters.
* }
*/
protected $subtags;
/**
* @var float
*/
protected $quality;
/**
* PLL_Accept_Language constructor.
*
* @since 3.0
*
* @param string[] $subtags With subtag name as keys and subtag values as names.
* @param mixed $quality Floating point value from 0.0 to 1.0. Higher values indicates a user's preference.
*/
public function __construct( $subtags, $quality = 1.0 ) {
$this->subtags = $subtags;
$this->quality = is_numeric( $quality ) ? floatval( $quality ) : 1.0;
}
/**
* Creates a new instance from an array resulting of a PHP {@see preg_match()} or {@see preg_match_all()} call.
*
* @since 3.0
*
* @param string[] $matches Expects first entry to be full match, following entries to be subtags and last entry to be quality factor.
* @return PLL_Accept_Language
*/
public static function from_array( $matches ) {
$subtags = array_combine(
array_keys( array_slice( self::SUBTAG_PATTERNS, 0, count( $matches ) - 1 ) ),
array_slice( $matches, 1, count( self::SUBTAG_PATTERNS ) )
);
$quality = count( $matches ) === 9 ? $matches[8] : 1.0;
return new PLL_Accept_Language( $subtags, $quality );
}
/**
* Returns the full language tag.
*
* @since 3.0
*
* @return string
*/
public function __toString() {
$subtags = array_filter(
$this->subtags,
function ( $subtag ) {
return ! empty( trim( $subtag ) );
}
);
return implode( '-', $subtags );
}
/**
* Returns the quality factor as negotiated by the browser agent.
*
* @since 3.0
*
* @return float
*/
public function get_quality() {
return $this->quality;
}
/**
* Returns a subtag from the language tag.
*
* @since 3.0
*
* @param string $name A valid subtag name, {@see PLL_Accept_Language::SUBTAG_PATTERNS} for available subtag names.
* @return string
*/
public function get_subtag( $name ) {
return isset( $this->subtags[ $name ] ) ? $this->subtags[ $name ] : '';
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @package Polylang
*/
/**
* Class PLL_Accept_Languages_Collection.
*
* Represents a collection of values parsed from an Accept-Language HTTP header.
*
* @since 3.0
*/
class PLL_Accept_Languages_Collection {
/**
* @var PLL_Accept_Language[]
*/
protected $accept_languages = array();
/**
* Parse Accept-Language HTTP header according to IETF BCP 47.
*
* @since 3.0
*
* @param string $http_header Value of the Accept-Language HTTP Header. Formatted as stated BCP 47 for language tags {@see https://tools.ietf.org/html/bcp47}.
* @return PLL_Accept_Languages_Collection
*/
public static function from_accept_language_header( $http_header ) {
$lang_parse = array();
// Break up string into pieces ( languages and q factors ).
$language_pattern = implode( '', PLL_Accept_Language::SUBTAG_PATTERNS );
$quality_pattern = '\s*;\s*q\s*=\s*((?>1|0)(?>\.[0-9]+)?)';
$full_pattern = "/{$language_pattern}(?:{$quality_pattern})?/i";
preg_match_all(
$full_pattern,
$http_header,
$lang_parse,
PREG_SET_ORDER
);
return new PLL_Accept_Languages_Collection(
array_map(
array( PLL_Accept_Language::class, 'from_array' ),
$lang_parse
)
);
}
/**
* PLL_Accept_Languages_Collection constructor.
*
* @since 3.0
*
* @param PLL_Accept_Language[] $accept_languages Objects representing Accept-Language HTTP headers.
*/
public function __construct( $accept_languages = array() ) {
$this->accept_languages = $accept_languages;
}
/**
* Bubble sort (need a stable sort for Android, so can't use a PHP sort function).
*
* @since 3.0
*
* @return void
*/
public function bubble_sort() {
$k = $this->accept_languages;
$v = array_map(
function ( $accept_lang ) {
return $accept_lang->get_quality();
},
$this->accept_languages
);
if ( $n = count( $k ) ) {
if ( $n > 1 ) {
for ( $i = 2; $i <= $n; $i++ ) {
for ( $j = 0; $j <= $n - 2; $j++ ) {
if ( $v[ $j ] < $v[ $j + 1 ] ) {
// Swap values.
$temp = $v[ $j ];
$v[ $j ] = $v[ $j + 1 ];
$v[ $j + 1 ] = $temp;
// Swap keys.
$temp = $k[ $j ];
$k[ $j ] = $k[ $j + 1 ];
$k[ $j + 1 ] = $temp;
}
}
}
}
$this->accept_languages = array_filter(
$k,
function ( $accept_lang ) {
return $accept_lang->get_quality() > 0;
}
);
}
}
/**
* Looks through sorted list and use first one that matches our language list.
*
* @since 3.0
*
* @param PLL_Language[] $languages The language list.
* @return string|false A language slug if there's a match, false otherwise.
*/
public function find_best_match( $languages = array() ) {
foreach ( $this->accept_languages as $accept_lang ) {
// First loop to match the exact locale.
foreach ( $languages as $language ) {
if ( 0 === strcasecmp( $accept_lang, $language->get_locale( 'display' ) ) ) {
return $language->slug;
}
}
// In order of priority.
$subsets = array();
if ( ! empty( $accept_lang->get_subtag( 'region' ) ) ) {
$subsets[] = $accept_lang->get_subtag( 'language' ) . '-' . $accept_lang->get_subtag( 'region' );
$subsets[] = $accept_lang->get_subtag( 'region' );
}
if ( ! empty( $accept_lang->get_subtag( 'variant' ) ) ) {
$subsets[] = $accept_lang->get_subtag( 'language' ) . '-' . $accept_lang->get_subtag( 'variant' );
}
$subsets[] = $accept_lang->get_subtag( 'language' );
// More loops to match the subsets.
foreach ( $languages as $language ) {
foreach ( $subsets as $subset ) {
if ( 0 === stripos( $subset, $language->slug ) || 0 === stripos( $language->get_locale( 'display' ), $subset ) ) {
return $language->slug;
}
}
}
}
return false;
}
}

View File

@@ -0,0 +1,269 @@
<?php
/**
* @package Polylang
*/
/**
* Manages canonical redirect on frontend.
*
* @since 3.3
*/
class PLL_Canonical {
/**
* Stores the plugin options.
*
* @var array
*/
protected $options;
/**
* @var PLL_Model
*/
protected $model;
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
protected $links_model;
/**
* Current language.
*
* @var PLL_Language
*/
protected $curlang;
/**
* Constructor.
*
* @since 3.3
*
* @param object $polylang Main Polylang object.
*/
public function __construct( &$polylang ) {
$this->links_model = &$polylang->links_model;
$this->model = &$polylang->model;
$this->options = &$polylang->options;
$this->curlang = &$polylang->curlang;
}
/**
* If the language code is not in agreement with the language of the content,
* redirects incoming links to the proper URL to avoid duplicate content.
*
* @since 0.9.6
*
* @global WP_Query $wp_query WordPress Query object.
* @global bool $is_IIS
*
* @param string $requested_url Optional, defaults to requested url.
* @param bool $do_redirect Optional, whether to perform the redirect or not.
* @return string|void Returns if redirect is not performed.
*/
public function check_canonical_url( $requested_url = '', $do_redirect = true ) {
global $wp_query;
// Don't redirect in same cases as WP.
if ( is_trackback() || is_search() || is_admin() || is_preview() || is_robots() || ( $GLOBALS['is_IIS'] && ! iis7_supports_permalinks() ) ) {
return;
}
// Don't redirect mysite.com/?attachment_id= to mysite.com/en/?attachment_id=.
if ( 1 == $this->options['force_lang'] && is_attachment() && isset( $_GET['attachment_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return;
}
/*
* If the default language code is not hidden and the static front page url contains the page name,
* the customizer lands here and the code below would redirect to the list of posts.
*/
if ( is_customize_preview() ) {
return;
}
if ( empty( $requested_url ) ) {
$requested_url = pll_get_requested_url();
}
if ( ( is_single() && ( ! is_attachment() || get_option( 'wp_attachment_pages_enabled' ) ) ) || ( is_page() && ! is_front_page() ) ) {
$post = get_post();
if ( $post instanceof WP_Post && $this->model->is_translated_post_type( $post->post_type ) ) {
$language = $this->model->post->get_language( (int) $post->ID );
}
}
if ( ! empty( $wp_query->tax_query ) ) {
if ( $this->model->is_translated_taxonomy( $this->get_queried_taxonomy( $wp_query->tax_query ) ) ) {
$term_id = $this->get_queried_term_id( $wp_query->tax_query );
if ( $term_id ) {
$language = $this->model->term->get_language( $term_id );
}
}
}
if ( $wp_query->is_posts_page ) {
$page_id = get_query_var( 'page_id' );
if ( ! $page_id ) {
$page_id = get_queried_object_id();
}
if ( $page_id && is_numeric( $page_id ) ) {
$language = $this->model->post->get_language( (int) $page_id );
}
}
if ( 3 === $this->options['force_lang'] ) {
$requested_host = wp_parse_url( $requested_url, PHP_URL_HOST );
foreach ( $this->options['domains'] as $lang => $domain ) {
$host = wp_parse_url( $domain, PHP_URL_HOST );
if ( $requested_host && $host && ltrim( $requested_host, 'w.' ) === ltrim( $host, 'w.' ) ) {
$language = $this->model->get_language( $lang );
}
}
}
if ( empty( $language ) ) {
$language = $this->curlang;
$redirect_url = $requested_url;
} else {
$redirect_url = $this->redirect_canonical( $requested_url, $language );
$redirect_url = $this->options['force_lang'] ?
$this->links_model->switch_language_in_link( $redirect_url, $language ) :
$this->links_model->remove_language_from_link( $redirect_url ); // Works only for default permalinks.
}
/**
* Filters the canonical url detected by Polylang.
*
* @since 1.6
*
* @param string|false $redirect_url False or the url to redirect to.
* @param PLL_Language $language The language detected.
*/
$redirect_url = apply_filters( 'pll_check_canonical_url', $redirect_url, $language );
if ( ! $redirect_url || $requested_url === $redirect_url ) {
return $requested_url;
}
if ( ! $do_redirect ) {
return $redirect_url;
}
// Protect against chained redirects.
if ( $redirect_url === $this->check_canonical_url( $redirect_url, false ) && wp_validate_redirect( $redirect_url ) ) {
wp_safe_redirect( $redirect_url, 301, POLYLANG );
exit;
}
}
/**
* Returns the term_id of the requested term.
*
* @since 2.9
*
* @param WP_Tax_Query $tax_query An instance of WP_Tax_Query.
* @return int
*/
protected function get_queried_term_id( $tax_query ) {
$queried_terms = $tax_query->queried_terms;
$taxonomy = $this->get_queried_taxonomy( $tax_query );
if ( ! is_array( $queried_terms[ $taxonomy ]['terms'] ) ) {
return 0;
}
$field = $queried_terms[ $taxonomy ]['field'];
$term = reset( $queried_terms[ $taxonomy ]['terms'] );
$lang = isset( $queried_terms['language']['terms'] ) ? reset( $queried_terms['language']['terms'] ) : '';
// We can get a term_id when requesting a plain permalink, eg /?cat=1.
if ( 'term_id' === $field ) {
return $term;
}
// We get a slug when requesting a pretty permalink. Let's query all corresponding terms.
$args = array(
'lang' => '',
'taxonomy' => $taxonomy,
$field => $term,
'hide_empty' => false,
'fields' => 'ids',
);
$term_ids = get_terms( $args );
if ( ! is_array( $term_ids ) || empty( $term_ids ) ) {
return 0;
}
$term_ids = array_filter( $term_ids, 'is_numeric' );
$filtered_terms_by_lang = array_filter(
$term_ids,
function ( $term_id ) use ( $lang ) {
$term_lang = $this->model->term->get_language( (int) $term_id );
return ! empty( $term_lang ) && $term_lang->slug === $lang;
}
);
$tr_term = (int) reset( $filtered_terms_by_lang );
if ( ! empty( $tr_term ) ) {
// The queried term exists in the desired language.
return $tr_term;
}
// The queried term doesn't exist in the desired language, let's return the first one retrieved.
return (int) reset( $term_ids );
}
/**
* Find the taxonomy being queried.
*
* @since 2.9
*
* @param WP_Tax_Query $tax_query An instance of WP_Tax_Query.
* @return string A taxonomy slug
*/
protected function get_queried_taxonomy( $tax_query ) {
$queried_terms = $tax_query->queried_terms;
unset( $queried_terms['language'] );
return (string) key( $queried_terms );
}
/**
* Evaluates the canonical redirect url through the deidcated WP function.
*
* @since 3.3
*
* @global WP_Query $wp_query WordPress Query object.
*
* @param string $url Requested url.
* @param PLL_Language $language Language of the queried object.
* @return string
*/
protected function redirect_canonical( $url, $language ) {
/**
* @var WP_Query
*/
global $wp_query;
$this->curlang = $language; // Hack to filter the `page_for_posts` option in the correct language.
$backup_wp_query = $wp_query;
if ( isset( $wp_query->tax_query ) ) {
unset( $wp_query->tax_query->queried_terms['language'] );
unset( $wp_query->query['lang'] );
}
$redirect_url = redirect_canonical( $url, false );
$wp_query = $backup_wp_query;
return $redirect_url ? $redirect_url : $url;
}
}

View File

@@ -0,0 +1,166 @@
<?php
/**
* @package Polylang
*/
/**
* Choose the language when it is set from content
* The language is set either in parse_query with priority 2 or in wp with priority 5
*
* @since 1.2
*/
class PLL_Choose_Lang_Content extends PLL_Choose_Lang {
/**
* Defers the language choice to the 'wp' action (when the content is known)
*
* @since 1.8
*
* @return void
*/
public function init() {
parent::init();
if ( ! did_action( 'pll_language_defined' ) ) {
// Set the languages from content
add_action( 'wp', array( $this, 'wp' ), 5 ); // Priority 5 for post types and taxonomies registered in wp hook with default priority
// If no language found, choose the preferred one
add_filter( 'pll_get_current_language', array( $this, 'pll_get_current_language' ) );
}
}
/**
* Overwrites parent::set_language to remove the 'wp' action if the language is set before.
*
* @since 1.2
*
* @param PLL_Language $curlang Current language.
* @return void
*/
protected function set_language( $curlang ) {
parent::set_language( $curlang );
remove_action( 'wp', array( $this, 'wp' ), 5 ); // won't attempt to set the language a 2nd time
}
/**
* Returns the language based on the queried content
*
* @since 1.2
*
* @return PLL_Language|false detected language, false if none was found
*/
protected function get_language_from_content() {
// No language set for 404
if ( is_404() || ( is_attachment() && ! $this->options['media_support'] ) ) {
return $this->get_preferred_language();
}
if ( $var = get_query_var( 'lang' ) ) {
$lang = explode( ',', $var );
$lang = $this->model->get_language( reset( $lang ) ); // Choose the first queried language
}
elseif ( ( is_single() || is_page() || ( is_attachment() && $this->options['media_support'] ) ) && ( ( $var = get_queried_object_id() ) || ( $var = get_query_var( 'p' ) ) || ( $var = get_query_var( 'page_id' ) ) || ( $var = get_query_var( 'attachment_id' ) ) ) && is_numeric( $var ) ) {
$lang = $this->model->post->get_language( (int) $var );
}
else {
foreach ( $this->model->get_translated_taxonomies() as $taxonomy ) {
$tax_object = get_taxonomy( $taxonomy );
if ( empty( $tax_object ) ) {
continue;
}
$var = get_query_var( $tax_object->query_var );
if ( ! is_string( $var ) || empty( $var ) ) {
continue;
}
$term = get_term_by( 'slug', $var, $taxonomy );
if ( ! $term instanceof WP_Term ) {
continue;
}
$lang = $this->model->term->get_language( $term->term_id );
}
}
/**
* Filters the language before it is set from the content.
*
* @since 0.9
*
* @param PLL_Language|false $lang Language object or false if none was found.
*/
return apply_filters( 'pll_get_current_language', isset( $lang ) ? $lang : false );
}
/**
* Sets the language for the home page.
* Adds the lang query var when querying archives with no language code.
*
* @since 1.2
*
* @param WP_Query $query Instance of WP_Query.
* @return void
*/
public function parse_main_query( $query ) {
if ( empty( $GLOBALS['wp_the_query'] ) || $query !== $GLOBALS['wp_the_query'] ) {
return;
}
$qv = $query->query_vars;
// Homepage is requested, let's set the language
// Take care to avoid posts page for which is_home = 1
if ( empty( $query->query ) && ( is_home() || is_page() ) ) {
$this->home_language();
$this->home_requested();
}
parent::parse_main_query( $query );
$is_archive = ( count( $query->query ) == 1 && ! empty( $qv['paged'] ) ) ||
$query->is_date ||
$query->is_author ||
( ! empty( $qv['post_type'] ) && $query->is_post_type_archive && $this->model->is_translated_post_type( $qv['post_type'] ) );
// Sets the language in case we hide the default language
// Use $query->query['s'] as is_search is not set when search is empty
// http://wordpress.org/support/topic/search-for-empty-string-in-default-language
if ( $this->options['hide_default'] && ! isset( $qv['lang'] ) && ( $is_archive || isset( $query->query['s'] ) || ( count( $query->query ) == 1 && ! empty( $qv['feed'] ) ) ) ) {
$this->set_language( $this->model->get_default_language() );
$this->set_curlang_in_query( $query );
}
}
/**
* Sets the language from content
*
* @since 1.2
*
* @return void
*/
public function wp() {
// Nothing to do if the language has already been set ( although normally the filter has been removed )
if ( empty( $this->curlang ) && $curlang = $this->get_language_from_content() ) {
parent::set_language( $curlang );
}
}
/**
* If no language is found by {@see PLL_Choose_Lang_Content::get_language_from_content()}, returns the preferred one.
*
* @since 0.9
*
* @param PLL_Language|false $lang Language found by {@see PLL_Choose_Lang_Content::get_language_from_content()}.
* @return PLL_Language|false
*/
public function pll_get_current_language( $lang ) {
return ! $lang ? $this->get_preferred_language() : $lang;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* @package Polylang
*/
/**
* Choose the language when the language is managed by different domains
*
* @since 1.5
*/
class PLL_Choose_Lang_Domain extends PLL_Choose_Lang_Url {
/**
* Don't set any language cookie
*
* @since 1.5
*
* @return void
*/
public function maybe_setcookie() {}
/**
* Don't redirect according to browser preferences
*
* @since 1.5
*
* @return PLL_Language
*/
public function get_preferred_language() {
return $this->model->get_language( $this->links_model->get_language_from_url() );
}
/**
* Adds query vars to query for home pages in all languages
*
* @since 1.5
*
* @return void
*/
public function home_requested() {
$this->set_curlang_in_query( $GLOBALS['wp_query'] );
/** This action is documented in include/choose-lang.php */
do_action( 'pll_home_requested' );
}
}

View File

@@ -0,0 +1,118 @@
<?php
/**
* @package Polylang
*/
/**
* Choose the language when the language code is added to all urls
* The language is set in plugins_loaded with priority 1 as done by WPML
* Some actions have to be delayed to wait for $wp_rewrite availability
*
* @since 1.2
*/
class PLL_Choose_Lang_Url extends PLL_Choose_Lang {
/**
* 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';
/**
* Sets the language
*
* @since 1.8
*
* @return void
*/
public function init() {
parent::init();
if ( ! did_action( 'pll_language_defined' ) ) {
$this->set_language_from_url();
}
add_filter( 'request', array( $this, 'request' ) );
}
/**
* Finds the language according to information found in the url
*
* @since 1.2
*
* @return void
*/
public function set_language_from_url() {
$host = str_replace( 'www.', '', (string) wp_parse_url( $this->links_model->home, PHP_URL_HOST ) ); // Remove www. for the comparison
$home_path = (string) wp_parse_url( $this->links_model->home, PHP_URL_PATH );
$requested_url = pll_get_requested_url();
$requested_host = str_replace( 'www.', '', (string) wp_parse_url( $requested_url, PHP_URL_HOST ) ); // Remove www. for the comparison
$requested_path = rtrim( str_replace( $this->index, '', (string) wp_parse_url( $requested_url, PHP_URL_PATH ) ), '/' ); // Some PHP setups turn requests for / into /index.php in REQUEST_URI
$requested_query = wp_parse_url( $requested_url, PHP_URL_QUERY );
// Home is requested
if ( $requested_host === $host && $requested_path === $home_path && empty( $requested_query ) ) {
$this->home_language();
add_action( 'setup_theme', array( $this, 'home_requested' ) );
}
// Take care to post & page preview http://wordpress.org/support/topic/static-frontpage-url-parameter-url-language-information
elseif ( isset( $_GET['preview'] ) && ( ( isset( $_GET['p'] ) && $id = (int) $_GET['p'] ) || ( isset( $_GET['page_id'] ) && $id = (int) $_GET['page_id'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$curlang = ( $lg = $this->model->post->get_language( $id ) ) ? $lg : $this->model->get_default_language();
}
// Take care to ( unattached ) attachments
elseif ( isset( $_GET['attachment_id'] ) && $id = (int) $_GET['attachment_id'] ) { // phpcs:ignore WordPress.Security.NonceVerification
$curlang = ( $lg = $this->model->post->get_language( $id ) ) ? $lg : $this->get_preferred_language();
}
elseif ( $slug = $this->links_model->get_language_from_url() ) {
$curlang = $this->model->get_language( $slug );
}
elseif ( $this->options['hide_default'] ) {
$curlang = $this->model->get_default_language();
}
// If no language found, check_language_code_in_url() will attempt to find one and redirect to the correct url
// Otherwise a 404 will be fired in the preferred language
$this->set_language( empty( $curlang ) ? $this->get_preferred_language() : $curlang );
}
/**
* Adds the current language in query vars
* useful for subdomains and multiple domains
*
* @since 1.8
*
* @param array $qv main request query vars
* @return array modified query vars
*/
public function request( $qv ) {
// FIXME take care not to break untranslated content
// FIXME media ?
// Untranslated post types
if ( isset( $qv['post_type'] ) && ! $this->model->is_translated_post_type( $qv['post_type'] ) ) {
return $qv;
}
// Untranslated taxonomies
$tax_qv = array_filter( wp_list_pluck( get_taxonomies( array(), 'objects' ), 'query_var' ) ); // Get all taxonomies query vars
$tax_qv = array_intersect( $tax_qv, array_keys( $qv ) ); // Get all queried taxonomies query vars
if ( ! $this->model->is_translated_taxonomy( array_keys( $tax_qv ) ) ) {
return $qv;
}
if ( isset( $this->curlang ) && empty( $qv['lang'] ) ) {
$qv['lang'] = $this->curlang->slug;
}
return $qv;
}
}

View File

@@ -0,0 +1,351 @@
<?php
/**
* @package Polylang
*/
/**
* Base class to choose the language
*
* @since 1.2
*/
abstract class PLL_Choose_Lang {
/**
* 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
*
* @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;
$this->curlang = &$polylang->curlang;
}
/**
* Sets the language for ajax requests
* and setup actions
* Any child class must call this method if it overrides it
*
* @since 1.8
*
* @return void
*/
public function init() {
if ( Polylang::is_ajax_on_front() || ! wp_using_themes() ) {
$this->set_language( empty( $_REQUEST['lang'] ) ? $this->get_preferred_language() : $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
}
add_action( 'pre_comment_on_post', array( $this, 'pre_comment_on_post' ) ); // sets the language of comment
add_action( 'parse_query', array( $this, 'parse_main_query' ), 2 ); // sets the language in special cases
add_action( 'wp', array( $this, 'maybe_setcookie' ), 7 );
}
/**
* Sets the current language
* and fires the action 'pll_language_defined'.
*
* @since 1.2
*
* @param PLL_Language|false $curlang Current language.
* @return void
*/
protected function set_language( $curlang ) {
// Don't set the language a second time
if ( isset( $this->curlang ) ) {
return;
}
// Final check in case $curlang has an unexpected value
// See https://wordpress.org/support/topic/detect-browser-language-sometimes-setting-null-language
if ( ! $curlang instanceof PLL_Language ) {
$curlang = $this->model->get_default_language();
if ( ! $curlang instanceof PLL_Language ) {
return;
}
}
$this->curlang = $curlang;
$GLOBALS['text_direction'] = $this->curlang->is_rtl ? 'rtl' : 'ltr';
if ( did_action( 'wp_default_styles' ) ) {
wp_styles()->text_direction = $GLOBALS['text_direction'];
}
/**
* Fires when the current language is defined.
*
* @since 0.9.5
*
* @param string $slug Current language code.
* @param PLL_Language $curlang Current language object.
*/
do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
}
/**
* Set a cookie to remember the language.
* Setting PLL_COOKIE to false will disable cookie although it will break some functionalities
*
* @since 1.5
*
* @return void
*/
public function maybe_setcookie() {
// Don't set cookie in javascript when a cache plugin is active.
if ( ! pll_is_cache_active() && ! empty( $this->curlang ) && ! is_404() ) {
$args = array(
'domain' => 2 === $this->options['force_lang'] ? wp_parse_url( $this->links_model->home, PHP_URL_HOST ) : COOKIE_DOMAIN,
'samesite' => 3 === $this->options['force_lang'] ? 'None' : 'Lax',
);
PLL_Cookie::set( $this->curlang->slug, $args );
}
}
/**
* Get the preferred language according to the browser preferences.
*
* @since 1.8
*
* @return string|bool The preferred language slug or false.
*/
public function get_preferred_browser_language() {
if ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) {
$accept_langs = PLL_Accept_Languages_Collection::from_accept_language_header( sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) );
$accept_langs->bubble_sort();
$languages = $this->model->get_languages_list( array( 'hide_empty' => true ) ); // Hides languages with no post.
/**
* Filters the list of languages to use to match the browser preferences.
*
* @since 1.9.3
*
* @param array $languages Array of PLL_Language objects.
*/
$languages = apply_filters( 'pll_languages_for_browser_preferences', $languages );
return $accept_langs->find_best_match( $languages );
}
return false;
}
/**
* Returns the preferred language
* either from the cookie if it's a returning visit
* or according to browser preference
* or the default language
*
* @since 0.1
*
* @return PLL_Language|false browser preferred language or default language
*/
public function get_preferred_language() {
$language = false;
$cookie = false;
if ( isset( $_COOKIE[ PLL_COOKIE ] ) ) {
// Check first if the user was already browsing this site.
$language = sanitize_key( $_COOKIE[ PLL_COOKIE ] );
$cookie = true;
} elseif ( $this->options['browser'] ) {
$language = $this->get_preferred_browser_language();
}
/**
* Filter the visitor's preferred language (normally set first by cookie
* if this is not the first visit, then by the browser preferences).
* If no preferred language has been found or set by this filter,
* Polylang fallbacks to the default language
*
* @since 1.0
* @since 2.7 Added $cookie parameter.
*
* @param string|bool $language Preferred language code, false if none has been found.
* @param bool $cookie Whether the preferred language has been defined by the cookie.
*/
$slug = apply_filters( 'pll_preferred_language', $language, $cookie );
// Return default if there is no preferences in the browser or preferences does not match our languages or it is requested not to use the browser preference
return ( $lang = $this->model->get_language( $slug ) ) ? $lang : $this->model->get_default_language();
}
/**
* Sets the language when home page is requested
*
* @since 1.2
*
* @return void
*/
protected function home_language() {
// Test referer in case PLL_COOKIE is set to false. Since WP 3.6.1, wp_get_referer() validates the host which is exactly what we want
// Thanks to Ov3rfly http://wordpress.org/support/topic/enhance-feature-when-front-page-is-visited-set-language-according-to-browser
$language = $this->options['hide_default'] && ( wp_get_referer() || ! $this->options['browser'] ) ?
$this->model->get_default_language() :
$this->get_preferred_language(); // Sets the language according to browser preference or default language
$this->set_language( $language );
}
/**
* To call when the home page has been requested
* Make sure to call this after 'setup_theme' has been fired as we need $wp_query
* Performs a redirection to the home page in the current language if needed
*
* @since 0.9
*
* @return void
*/
public function home_requested() {
if ( empty( $this->curlang ) ) {
return;
}
// We are already on the right page
if ( $this->curlang->is_default && $this->options['hide_default'] ) {
$this->set_curlang_in_query( $GLOBALS['wp_query'] );
/**
* Fires when the site root page is requested
*
* @since 1.8
*/
do_action( 'pll_home_requested' );
}
// Redirect to the home page in the right language
// Test to avoid crash if get_home_url returns something wrong
// FIXME why this happens? http://wordpress.org/support/topic/polylang-crashes-1
// Don't redirect if $_POST is not empty as it could break other plugins
elseif ( is_string( $redirect = $this->curlang->get_home_url() ) && empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// Don't forget the query string which may be added by plugins
$query_string = wp_parse_url( pll_get_requested_url(), PHP_URL_QUERY );
if ( ! empty( $query_string ) ) {
$redirect .= ( $this->links_model->using_permalinks ? '?' : '&' ) . $query_string;
}
/**
* When a visitor reaches the site home, Polylang redirects to the home page in the correct language.
* This filter allows plugins to modify the redirected url or prevent this redirection
* /!\ this filter may be fired *before* the theme is loaded
*
* @since 1.1.1
*
* @param string $redirect the url the visitor will be redirected to
*/
$redirect = apply_filters( 'pll_redirect_home', $redirect );
if ( $redirect && wp_validate_redirect( $redirect ) ) {
$this->maybe_setcookie();
header( 'Vary: Accept-Language' );
wp_safe_redirect( $redirect, 302, POLYLANG );
exit;
}
}
}
/**
* Set the language when posting a comment
*
* @since 0.8.4
*
* @param int $post_id the post being commented
* @return void
*/
public function pre_comment_on_post( $post_id ) {
$this->set_language( $this->model->post->get_language( $post_id ) );
}
/**
* Modifies some main query vars for the home page and the page for posts
* to enable one home page (and one page for posts) per language.
*
* @since 1.2
*
* @param WP_Query $query Instance of WP_Query.
* @return void
*/
public function parse_main_query( $query ) {
if ( ! $query->is_main_query() ) {
return;
}
/**
* This filter allows to set the language based on information contained in the main query
*
* @since 1.8
*
* @param PLL_Language|false $lang Language object or false.
* @param WP_Query $query WP_Query object.
*/
if ( $lang = apply_filters( 'pll_set_language_from_query', false, $query ) ) {
$this->set_language( $lang );
$this->set_curlang_in_query( $query );
} elseif ( ( count( $query->query ) == 1 || ( is_paged() && count( $query->query ) == 2 ) ) && $lang = get_query_var( 'lang' ) ) {
$lang = $this->model->get_language( $lang );
$this->set_language( $lang ); // Set the language now otherwise it will be too late to filter sticky posts!
// Set is_home on translated home page when it displays posts. It must be true on page 2, 3... too.
$query->is_home = true;
$query->is_tax = false;
$query->is_archive = false;
// Filters is_front_page() in case a static front page is not translated in this language.
add_filter( 'option_show_on_front', array( $this, 'filter_option_show_on_front' ) );
}
}
/**
* Filters the option show_on_front when the current front page displays posts.
*
* This is useful when a static front page is not translated in all languages.
*
* @return string
*/
public function filter_option_show_on_front() {
return 'posts';
}
/**
* Sets the current language in the query.
*
* @since 2.2
*
* @param WP_Query $query Instance of WP_Query.
* @return void
*/
protected function set_curlang_in_query( &$query ) {
if ( ! empty( $this->curlang ) ) {
$pll_query = new PLL_Query( $query, $this->model );
$pll_query->set_language( $this->curlang );
}
}
}

View File

@@ -0,0 +1,331 @@
<?php
/**
* @package Polylang
*/
/**
* Auto translates the posts and terms ids
* Useful for example for themes querying a specific cat
*
* @since 1.1
*/
class PLL_Frontend_Auto_Translate {
/**
* @var PLL_Model
*/
public $model;
/**
* Current language.
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor
*
* @since 1.1
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->model = &$polylang->model;
$this->curlang = &$polylang->curlang;
add_action( 'parse_query', array( $this, 'translate_included_ids_in_query' ), 100 ); // After all Polylang filters.
add_filter( 'get_terms_args', array( $this, 'get_terms_args' ), 20, 2 );
}
/**
* Helper function to get the translated post in the current language.
*
* @since 1.8
*
* @param int $post_id The ID of the post to translate.
* @return int
*
* @phpstan-return int<0, max>
*/
protected function get_post( $post_id ) {
return $this->model->post->get( $post_id, $this->curlang );
}
/**
* Helper function to get the translated term in the current language.
*
* @since 1.8
*
* @param int $term_id The ID of the term to translate.
* @return int
*
* @phpstan-return int<0, max>
*/
protected function get_term( $term_id ) {
return $this->model->term->get( $term_id, $this->curlang );
}
/**
* Filters posts query to automatically translate included ids
*
* @since 1.1
*
* @param WP_Query $query WP_Query object
* @return void
*/
public function translate_included_ids_in_query( $query ) {
global $wpdb;
$qv = &$query->query_vars;
if ( $query->is_main_query() || isset( $qv['lang'] ) || ( ! empty( $qv['post_type'] ) && ! $this->model->is_translated_post_type( $qv['post_type'] ) ) ) {
return;
}
// /!\ always keep untranslated as is
// Term ids separated by a comma
$arr = array();
if ( ! empty( $qv['cat'] ) ) {
foreach ( explode( ',', $qv['cat'] ) as $cat ) {
$tr = $this->get_term( abs( $cat ) );
$arr[] = $cat < 0 ? -$tr : $tr;
}
$qv['cat'] = implode( ',', $arr );
}
// Category_name
$arr = array();
if ( ! empty( $qv['category_name'] ) ) {
foreach ( explode( ',', $qv['category_name'] ) as $slug ) {
$arr[] = $this->get_translated_term_by( 'slug', $slug, 'category' );
}
$qv['category_name'] = implode( ',', $arr );
}
// Array of term ids
foreach ( array( 'category__and', 'category__in', 'category__not_in', 'tag__and', 'tag__in', 'tag__not_in' ) as $key ) {
$arr = array();
if ( ! empty( $qv[ $key ] ) ) {
foreach ( $qv[ $key ] as $cat ) {
$arr[] = ( $tr = $this->get_term( $cat ) ) ? $tr : $cat;
}
$qv[ $key ] = $arr;
}
}
// Tag
if ( ! empty( $qv['tag'] ) ) {
$qv['tag'] = $this->translate_terms_list( $qv['tag'], 'post_tag' );
}
// tag_id can only take one id
if ( ! empty( $qv['tag_id'] ) && $tr_id = $this->get_term( $qv['tag_id'] ) ) {
$qv['tag_id'] = $tr_id;
}
// Array of tag slugs
foreach ( array( 'tag_slug__and', 'tag_slug__in' ) as $key ) {
$arr = array();
if ( ! empty( $qv[ $key ] ) ) {
foreach ( $qv[ $key ] as $slug ) {
$arr[] = $this->get_translated_term_by( 'slug', $slug, 'post_tag' );
}
$qv[ $key ] = $arr;
}
}
// Custom taxonomies
// According to the codex, this type of query is deprecated as of WP 3.1 but it does not appear in WP 3.5 source code
foreach ( array_intersect( $this->model->get_translated_taxonomies(), get_taxonomies( array( '_builtin' => false ) ) ) as $taxonomy ) {
$tax = get_taxonomy( $taxonomy );
if ( ! empty( $tax ) && ! empty( $qv[ $tax->query_var ] ) ) {
$qv[ $tax->query_var ] = $this->translate_terms_list( $qv[ $tax->query_var ], $taxonomy );
}
}
// Tax_query since WP 3.1
if ( ! empty( $qv['tax_query'] ) && is_array( $qv['tax_query'] ) ) {
$qv['tax_query'] = $this->translate_tax_query_recursive( $qv['tax_query'] );
}
// p, page_id, post_parent can only take one id
foreach ( array( 'p', 'page_id', 'post_parent' ) as $key ) {
if ( ! empty( $qv[ $key ] ) && $tr_id = $this->get_post( $qv[ $key ] ) ) {
$qv[ $key ] = $tr_id;
}
}
// name, can only take one slug
if ( ! empty( $qv['name'] ) && is_string( $qv['name'] ) ) {
if ( empty( $qv['post_type'] ) ) {
$post_types = array( 'post' );
} elseif ( 'any' === $qv['post_type'] ) {
$post_types = get_post_types( array( 'exclude_from_search' => false ) ); // May return a empty array
} else {
$post_types = (array) $qv['post_type'];
}
if ( ! empty( $post_types ) ) {
// No function to get post by name except get_posts itself
$id = $wpdb->get_var(
sprintf(
"SELECT ID from {$wpdb->posts}
WHERE {$wpdb->posts}.post_type IN ( '%s' )
AND post_name='%s'",
implode( "', '", esc_sql( $post_types ) ),
esc_sql( $qv['name'] )
)
);
$qv['name'] = ( $id && ( $tr_id = $this->get_post( $id ) ) && $tr = get_post( $tr_id ) ) ? $tr->post_name : $qv['name'];
}
}
// pagename, the page id is already available in queried_object_id
if ( ! empty( $qv['pagename'] ) && ! empty( $query->queried_object_id ) && $tr_id = $this->get_post( $query->queried_object_id ) ) {
$query->queried_object_id = $tr_id;
$qv['pagename'] = get_page_uri( $tr_id );
}
// Array of post ids
// post_parent__in & post_parent__not_in since WP 3.6
foreach ( array( 'post__in', 'post__not_in', 'post_parent__in', 'post_parent__not_in' ) as $key ) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn
$arr = array();
if ( ! empty( $qv[ $key ] ) ) {
// post__in used by the 2 functions below
// Useless to filter them as output is already in the right language and would result in performance loss
foreach ( debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ) as $trace ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions
if ( in_array( $trace['function'], array( 'wp_nav_menu', 'gallery_shortcode' ) ) ) {
return;
}
}
foreach ( $qv[ $key ] as $p ) {
$arr[] = ( $tr = $this->get_post( $p ) ) ? $tr : $p;
}
$qv[ $key ] = $arr;
}
}
}
/**
* Filters the terms query to automatically translate included ids.
*
* @since 1.1.1
*
* @param array $args An array of get_terms() arguments.
* @param array $taxonomies An array of taxonomy names.
* @return array
*/
public function get_terms_args( $args, $taxonomies ) {
if ( ! isset( $args['lang'] ) && ! empty( $args['include'] ) && ( empty( $taxonomies ) || $this->model->is_translated_taxonomy( $taxonomies ) ) ) {
$arr = array();
foreach ( wp_parse_id_list( $args['include'] ) as $id ) {
$arr[] = ( $tr = $this->get_term( $id ) ) ? $tr : $id;
}
$args['include'] = $arr;
}
return $args;
}
/**
* Translates tax queries
* Compatible with nested tax queries introduced in WP 4.1
*
* @since 1.7
*
* @param array $tax_queries An array of tax queries.
* @return array Translated tax queries.
*/
protected function translate_tax_query_recursive( $tax_queries ) {
foreach ( $tax_queries as $key => $q ) {
if ( ! is_array( $q ) ) {
continue;
}
if ( isset( $q['taxonomy'], $q['terms'] ) && $this->model->is_translated_taxonomy( $q['taxonomy'] ) ) {
$arr = array();
$field = isset( $q['field'] ) && in_array( $q['field'], array( 'slug', 'name' ) ) ? $q['field'] : 'term_id';
foreach ( (array) $q['terms'] as $t ) {
$arr[] = $this->get_translated_term_by( $field, $t, $q['taxonomy'] );
}
$tax_queries[ $key ]['terms'] = $arr;
} else {
// Nested queries.
$tax_queries[ $key ] = $this->translate_tax_query_recursive( $q );
}
}
return $tax_queries;
}
/**
* Translates a term given one field.
*
* @since 2.3.3
*
* @param string $field Either 'slug', 'name', 'term_id', or 'term_taxonomy_id'
* @param string|int $term Search for this term value
* @param string $taxonomy Taxonomy name.
* @return string|int Translated term slug, name, term_id or term_taxonomy_id
*/
protected function get_translated_term_by( $field, $term, $taxonomy ) {
if ( 'term_id' === $field ) {
if ( $tr_id = $this->get_term( $term ) ) {
return $tr_id;
}
} else {
$terms = get_terms( array( 'taxonomy' => $taxonomy, $field => $term, 'lang' => '' ) );
if ( ! empty( $terms ) && is_array( $terms ) ) {
$t = reset( $terms );
if ( ! $t instanceof WP_Term ) {
return $term;
}
$tr_id = $this->get_term( $t->term_id );
if ( ! is_wp_error( $tr = get_term( $tr_id, $taxonomy ) ) ) {
return $tr->$field;
}
}
}
return $term;
}
/**
* Translates a list of term slugs provided either as an array or a string
* with slugs separated by a comma or a '+'.
*
* @since 3.2.8
*
* @param string|string[] $query_var The list of term slugs.
* @param string $taxonomy The taxonomy for terms.
* @return string|string[] The translated list.
*/
protected function translate_terms_list( $query_var, $taxonomy ) {
$slugs = array();
if ( is_array( $query_var ) ) {
$slugs = &$query_var;
} elseif ( is_string( $query_var ) ) {
$sep = strpos( $query_var, ',' ) !== false ? ',' : '+'; // Two possible separators.
$slugs = explode( $sep, $query_var );
}
foreach ( $slugs as &$slug ) {
$slug = $this->get_translated_term_by( 'slug', $slug, $taxonomy );
}
if ( ! empty( $sep ) ) {
$query_var = implode( $sep, $slugs );
}
return $query_var;
}
}

View File

@@ -0,0 +1,350 @@
<?php
/**
* @package Polylang
*/
/**
* Manages links filters on frontend
*
* @since 1.8
*/
class PLL_Frontend_Filters_Links extends PLL_Filters_Links {
/**
* @var PLL_Frontend_Links|null
*/
public $links;
/**
* Our internal non persistent cache object
*
* @var PLL_Cache<string>
*/
public $cache;
/**
* Stores a list of files and functions that home_url() must not filter.
*
* @var array
*/
private $black_list = array();
/**
* Stores a list of files and functions that home_url() must filter.
*
* @var array
*/
private $white_list = array();
/**
* Constructor
* Adds filters once the language is defined
* Low priority on links filters to come after any other modification
*
* @since 1.8
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->curlang = &$polylang->curlang;
$this->cache = new PLL_Cache();
// Rewrites author and date links to filter them by language
foreach ( array( 'feed_link', 'author_link', 'search_link', 'year_link', 'month_link', 'day_link' ) as $filter ) {
add_filter( $filter, array( $this, 'archive_link' ), 20 );
}
// Meta in the html head section
add_action( 'wp_head', array( $this, 'wp_head' ), 1 );
// Modifies the home url
if ( pll_get_constant( 'PLL_FILTER_HOME_URL', true ) ) {
add_filter( 'home_url', array( $this, 'home_url' ), 10, 2 );
}
if ( $this->options['force_lang'] > 1 ) {
// Rewrites next and previous post links when not automatically done by WordPress
add_filter( 'get_pagenum_link', array( $this, 'archive_link' ), 20 );
add_filter( 'get_shortlink', array( $this, 'shortlink' ), 20, 2 );
// Rewrites ajax url
add_filter( 'admin_url', array( $this, 'admin_url' ), 10, 2 );
}
}
/**
* Modifies the author and date links to add the language parameter (as well as feed link).
*
* @since 0.4
*
* @param string $link The permalink to the archive.
* @return string The modified link.
*/
public function archive_link( $link ) {
return $this->links_model->switch_language_in_link( $link, $this->curlang );
}
/**
* Modifies page links
* and caches the result
*
* @since 1.7
*
* @param string $link The page link.
* @param int $post_id The post ID.
* @return string The modified page link.
*/
public function _get_page_link( $link, $post_id ) {
$cache_key = "post:{$post_id}:{$link}";
if ( false === $_link = $this->cache->get( $cache_key ) ) {
$_link = parent::_get_page_link( $link, $post_id );
$this->cache->set( $cache_key, $_link );
}
return $_link;
}
/**
* Modifies attachment links
* and caches the result
*
* @since 1.6.2
*
* @param string $link The attachment link.
* @param int $post_id The attachment post ID.
* @return string The modified attachment link.
*/
public function attachment_link( $link, $post_id ) {
$cache_key = "post:{$post_id}:{$link}";
if ( false === $_link = $this->cache->get( $cache_key ) ) {
$_link = parent::attachment_link( $link, $post_id );
$this->cache->set( $cache_key, $_link );
}
return $_link;
}
/**
* Modifies custom posts links
* and caches the result.
*
* @since 1.6
*
* @param string $link The post link.
* @param WP_Post $post The post object.
* @return string The modified post link.
*/
public function post_type_link( $link, $post ) {
$cache_key = "post:{$post->ID}:{$link}";
if ( false === $_link = $this->cache->get( $cache_key ) ) {
$_link = parent::post_type_link( $link, $post );
$this->cache->set( $cache_key, $_link );
}
return $_link;
}
/**
* Modifies filtered taxonomies ( post format like ) and translated taxonomies links
* and caches the result.
*
* @since 0.7
*
* @param string $link The term link.
* @param WP_Term $term The term object.
* @param string $tax The taxonomy name.
* @return string The modified link.
*/
public function term_link( $link, $term, $tax ) {
$cache_key = "term:{$term->term_id}:{$link}";
if ( false === $_link = $this->cache->get( $cache_key ) ) {
if ( in_array( $tax, $this->model->get_filtered_taxonomies() ) ) {
$_link = $this->links_model->switch_language_in_link( $link, $this->curlang );
/** This filter is documented in include/filters-links.php */
$_link = apply_filters( 'pll_term_link', $_link, $this->curlang, $term );
}
else {
$_link = parent::term_link( $link, $term, $tax );
}
$this->cache->set( $cache_key, $_link );
}
return $_link;
}
/**
* Modifies the post short link when using one domain or subdomain per language.
*
* @since 2.6.9
*
* @param string $link Post permalink.
* @param int $post_id Post id.
* @return string Post permalink with the correct domain.
*/
public function shortlink( $link, $post_id ) {
$post_type = get_post_type( $post_id );
return $this->model->is_translated_post_type( $post_type ) ? $this->links_model->switch_language_in_link( $link, $this->model->post->get_language( $post_id ) ) : $link;
}
/**
* Outputs references to translated pages ( if exists ) in the html head section
*
* @since 0.1
*
* @return void
*/
public function wp_head() {
// Don't output anything on paged archives: see https://wordpress.org/support/topic/hreflang-on-page2
// Don't output anything on paged pages and paged posts
if ( is_paged() || ( is_singular() && ( $page = get_query_var( 'page' ) ) && $page > 1 ) ) {
return;
}
$urls = array();
// Google recommends to include self link https://support.google.com/webmasters/answer/189077?hl=en
foreach ( $this->model->get_languages_list() as $language ) {
if ( $url = $this->links->get_translation_url( $language ) ) {
$urls[ $language->get_locale( 'display' ) ] = $url;
}
}
// Outputs the section only if there are translations ( $urls always contains self link )
if ( ! empty( $urls ) && count( $urls ) > 1 ) {
$languages = array();
$hreflangs = array();
// Prepare the list of languages to remove the country code
foreach ( array_keys( $urls ) as $locale ) {
$split = explode( '-', $locale );
$languages[ $locale ] = reset( $split );
}
$count = array_count_values( $languages );
foreach ( $urls as $locale => $url ) {
$lang = $count[ $languages[ $locale ] ] > 1 ? $locale : $languages[ $locale ]; // Output the country code only when necessary
$hreflangs[ $lang ] = $url;
}
// Adds the site root url when the default language code is not hidden
// See https://wordpress.org/support/topic/implementation-of-hreflangx-default
if ( is_front_page() && ! $this->options['hide_default'] && $this->options['force_lang'] < 3 ) {
$hreflangs['x-default'] = home_url( '/' );
}
/**
* Filters the list of rel hreflang attributes
*
* @since 2.1
*
* @param array $hreflangs Array of urls with language codes as keys
*/
$hreflangs = apply_filters( 'pll_rel_hreflang_attributes', $hreflangs );
foreach ( $hreflangs as $lang => $url ) {
printf( '<link rel="alternate" href="%s" hreflang="%s" />' . "\n", esc_url( $url ), esc_attr( $lang ) );
}
}
}
/**
* Filters the home url to get the right language.
*
* @since 0.4
*
* @param string $url The home URL including scheme and path.
* @param string $path Path relative to the home URL.
* @return string
*/
public function home_url( $url, $path ) {
if ( ! ( did_action( 'template_redirect' ) || did_action( 'login_init' ) ) || rtrim( $url, '/' ) != $this->links_model->home ) {
return $url;
}
// We *want* to filter the home url in these cases
if ( empty( $this->white_list ) ) {
// On Windows get_theme_root() mixes / and \
// We want only \ for the comparison with debug_backtrace
$theme_root = get_theme_root();
$theme_root = ( false === strpos( $theme_root, '\\' ) ) ? $theme_root : str_replace( '/', '\\', $theme_root );
$white_list = array(
array( 'file' => $theme_root ),
array( 'function' => 'wp_nav_menu' ),
array( 'function' => 'login_footer' ),
array( 'function' => 'get_custom_logo' ),
array( 'function' => 'render_block_core_site_title' ),
);
if ( 3 === $this->options['force_lang'] ) {
$white_list[] = array( 'function' => 'redirect_canonical' );
}
/**
* Filters the white list of the Polylang 'home_url' filter.
*
* @since 1.1.2
*
* @param string[][] $white_list An array of arrays each of them having a 'file' key
* and/or a 'function' key to decide which functions in
* which files using home_url() calls must be filtered.
*/
$this->white_list = apply_filters( 'pll_home_url_white_list', $white_list );
}
// We don't want to filter the home url in these cases.
if ( empty( $this->black_list ) ) {
$black_list = array(
array( 'file' => 'searchform.php' ), // Since WP 3.6 searchform.php is passed through get_search_form.
array( 'function' => 'get_search_form' ),
);
/**
* Filters the black list of the Polylang 'home_url' filter.
*
* @since 1.1.2
*
* @param string[][] $black_list An array of arrays each of them having a 'file' key
* and/or a 'function' key to decide which functions in
* which files using home_url() calls must be filtered.
*/
$this->black_list = apply_filters( 'pll_home_url_black_list', $black_list );
}
$traces = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
unset( $traces[0], $traces[1] ); // We don't need the last 2 calls: this function + call_user_func_array (or apply_filters on PHP7+)
foreach ( $traces as $trace ) {
// Black list first
foreach ( $this->black_list as $v ) {
if ( ( isset( $trace['file'], $v['file'] ) && false !== strpos( $trace['file'], $v['file'] ) ) || ( ! empty( $v['function'] ) && $trace['function'] === $v['function'] ) ) {
return $url;
}
}
foreach ( $this->white_list as $v ) {
if ( ( ! empty( $v['function'] ) && $trace['function'] === $v['function'] ) ||
( isset( $trace['file'], $v['file'] ) && false !== strpos( $trace['file'], $v['file'] ) && in_array( $trace['function'], array( 'home_url', 'get_home_url', 'bloginfo', 'get_bloginfo' ) ) ) ) {
$ok = true;
}
}
}
return empty( $ok ) ? $url : ( empty( $path ) ? rtrim( $this->links->get_home_url( $this->curlang ), '/' ) : $this->links->get_home_url( $this->curlang ) );
}
/**
* Rewrites the ajax url when using domains or subdomains.
*
* @since 1.5
*
* @param string $url The admin url with path evaluated by WordPress.
* @param string $path Path relative to the admin URL.
* @return string
*/
public function admin_url( $url, $path ) {
return 'admin-ajax.php' === $path ? $this->links_model->switch_language_in_link( $url, $this->curlang ) : $url;
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* @package Polylang
*/
/**
* Filters search forms when using permalinks
*
* @since 1.2
*/
class PLL_Frontend_Filters_Search {
/**
* 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.
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->links_model = &$polylang->links_model;
$this->curlang = &$polylang->curlang;
// Adds the language information in the search form
// Low priority in case the search form is created using the same filter as described in http://codex.wordpress.org/Function_Reference/get_search_form
add_filter( 'get_search_form', array( $this, 'get_search_form' ), 99 );
// Adds the language information in the search block.
add_filter( 'render_block_core/search', array( $this, 'get_search_form' ) );
// Adds the language information in admin bar search form
add_action( 'add_admin_bar_menus', array( $this, 'add_admin_bar_menus' ) );
// Adds javascript at the end of the document
// Was used for WP < 3.6. kept just in case
if ( defined( 'PLL_SEARCH_FORM_JS' ) && PLL_SEARCH_FORM_JS ) {
add_action( 'wp_footer', array( $this, 'wp_print_footer_scripts' ) );
}
}
/**
* Adds the language information in the search form.
*
* Does not work if searchform.php ( prior to WP 3.6 ) is used or if the search form is hardcoded in another template file
*
* @since 0.1
*
* @param string $form The search form HTML.
* @return string Modified search form.
*/
public function get_search_form( $form ) {
if ( empty( $form ) || empty( $this->curlang ) ) {
return $form;
}
if ( $this->links_model->using_permalinks ) {
// Take care to modify only the url in the <form> tag.
preg_match( '#<form.+?>#s', $form, $matches );
$old = reset( $matches );
if ( empty( $old ) ) {
return $form;
}
// Replace action attribute (a text with no space and no closing tag within double quotes or simple quotes or without quotes).
$new = preg_replace( '#\saction=("[^"\r\n]+"|\'[^\'\r\n]+\'|[^\'"][^>\s]+)#', ' action="' . esc_url( $this->curlang->get_search_url() ) . '"', $old );
if ( empty( $new ) ) {
return $form;
}
$form = str_replace( $old, $new, $form );
} else {
$form = str_replace( '</form>', '<input type="hidden" name="lang" value="' . esc_attr( $this->curlang->slug ) . '" /></form>', $form );
}
return $form;
}
/**
* Adds the language information in the admin bar search form.
*
* @since 1.2
*
* @return void
*/
public function add_admin_bar_menus() {
// Backward compatibility with WP < 6.6. The priority was 4 before this version, 9999 since then.
$priority = has_action( 'admin_bar_menu', 'wp_admin_bar_search_menu' );
if ( ! is_int( $priority ) ) {
return;
}
remove_action( 'admin_bar_menu', 'wp_admin_bar_search_menu', $priority );
add_action( 'admin_bar_menu', array( $this, 'admin_bar_search_menu' ), $priority );
}
/**
* Rewrites the admin bar search form to pass our get_search_form filter. See #21342.
* Code last checked: WP 5.4.1.
*
* @since 0.9
*
* @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance, passed by reference.
* @return void
*/
public function admin_bar_search_menu( $wp_admin_bar ) {
$form = '<form action="' . esc_url( home_url( '/' ) ) . '" method="get" id="adminbarsearch">';
$form .= '<input class="adminbar-input" name="s" id="adminbar-search" type="text" value="" maxlength="150" />';
$form .= '<label for="adminbar-search" class="screen-reader-text">' .
/* translators: Hidden accessibility text. */
esc_html__( 'Search', 'polylang' ) .
'</label>';
$form .= '<input type="submit" class="adminbar-button" value="' . esc_attr__( 'Search', 'polylang' ) . '" />';
$form .= '</form>';
$wp_admin_bar->add_node(
array(
'parent' => 'top-secondary',
'id' => 'search',
'title' => $this->get_search_form( $form ), // Pass the get_search_form filter.
'meta' => array(
'class' => 'admin-bar-search',
'tabindex' => -1,
),
)
);
}
/**
* Allows modifying the search form if it does not pass get_search_form.
*
* @since 0.1
*
* @return void
*/
public function wp_print_footer_scripts() {
/*
* Don't use directly e[0] just in case there is somewhere else an element named 's'
* Check before if the hidden input has not already been introduced by get_search_form ( FIXME: is there a way to improve this ) ?
* Thanks to AndyDeGroo for improving the code for compatibility with old browsers
* http://wordpress.org/support/topic/development-of-polylang-version-08?replies=6#post-2645559
*/
$lang = esc_js( $this->curlang->slug );
$js = "e = document.getElementsByName( 's' );
for ( i = 0; i < e.length; i++ ) {
if ( e[i].tagName.toUpperCase() == 'INPUT' ) {
s = e[i].parentNode.parentNode.children;
l = 0;
for ( j = 0; j < s.length; j++ ) {
if ( s[j].name == 'lang' ) {
l = 1;
}
}
if ( l == 0 ) {
var ih = document.createElement( 'input' );
ih.type = 'hidden';
ih.name = 'lang';
ih.value = '{$lang}';
e[i].parentNode.appendChild( ih );
}
}
}";
$type_attr = current_theme_supports( 'html5', 'script' ) ? '' : ' type="text/javascript"';
if ( $type_attr ) {
$js = "/* <![CDATA[ */\n{$js}\n/* ]]> */";
}
echo "<script{$type_attr}>\n{$js}\n</script>\n"; // phpcs:ignore WordPress.Security.EscapeOutput
}
}

View File

@@ -0,0 +1,153 @@
<?php
/**
* @package Polylang
*/
/**
* Filters widgets by language on frontend
*
* @since 3.1
*/
class PLL_Frontend_Filters_Widgets {
/**
* Internal non persistent cache object.
*
* @var PLL_Cache<array>
*/
public $cache;
/**
* Current language.
*
* @var PLL_Language|null
*/
public $curlang;
/**
* Constructor: setups filters and actions.
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
$this->curlang = &$polylang->curlang;
$this->cache = new PLL_Cache();
add_filter( 'sidebars_widgets', array( $this, 'sidebars_widgets' ) );
}
/**
* Remove widgets from sidebars if they are not visible in the current language
* Needed to allow is_active_sidebar() to return false if all widgets are not for the current language. See #54
*
* @since 2.1
* @since 2.4 The result is cached as the function can be very expensive in case there are a lot of widgets
*
* @param array $sidebars_widgets An associative array of sidebars and their widgets
* @return array
*/
public function sidebars_widgets( $sidebars_widgets ) {
global $wp_registered_widgets;
if ( empty( $wp_registered_widgets ) ) {
return $sidebars_widgets;
}
$cache_key = $this->cache->get_unique_key( 'sidebars_widgets_', $sidebars_widgets );
$_sidebars_widgets = $this->cache->get( $cache_key );
if ( false !== $_sidebars_widgets ) {
return $_sidebars_widgets;
}
$sidebars_widgets = $this->filter_widgets_sidebars( $sidebars_widgets, $wp_registered_widgets );
return $this->cache->set( $cache_key, $sidebars_widgets );
}
/**
* Method that handles the removal of widgets in the sidebars depending on their display language.
*
* @since 3.1
*
* @param array $widget_data An array containing the widget data
* @param array $sidebars_widgets An associative array of sidebars and their widgets
* @param string $sidebar Sidebar name
* @param int $key Widget number
* @return array An associative array of sidebars and their widgets
*/
public function handle_widget_in_sidebar_callback( $widget_data, $sidebars_widgets, $sidebar, $key ) {
// Remove the widget if not visible in the current language
if ( ! empty( $widget_data['settings'][ $widget_data['number'] ]['pll_lang'] ) && $widget_data['settings'][ $widget_data['number'] ]['pll_lang'] !== $this->curlang->slug ) {
unset( $sidebars_widgets[ $sidebar ][ $key ] );
}
return $sidebars_widgets;
}
/**
* Browse the widgets sidebars and sort the ones that should be displayed or not.
*
* @since 3.1
*
* @param array $sidebars_widgets An associative array of sidebars and their widgets
* @param array $wp_registered_widgets Array of all registered widgets.
* @return array An associative array of sidebars and their widgets
*/
public function filter_widgets_sidebars( $sidebars_widgets, $wp_registered_widgets ) {
foreach ( $sidebars_widgets as $sidebar => $widgets ) {
if ( 'wp_inactive_widgets' === $sidebar || empty( $widgets ) ) {
continue;
}
foreach ( $widgets as $key => $widget ) {
if ( ! $this->is_widget_object( $wp_registered_widgets, $widget ) ) {
continue;
}
$widget_data = $this->get_widget_data( $wp_registered_widgets, $widget );
$sidebars_widgets = $this->handle_widget_in_sidebar_callback( $widget_data, $sidebars_widgets, $sidebar, $key );
}
}
return $sidebars_widgets;
}
/**
* Test if the widget is an object.
*
* @since 3.1
*
* @param array $wp_registered_widgets Array of all registered widgets.
* @param string $widget String that identifies the widget.
* @return bool True if object, false otherwise.
*/
protected function is_widget_object( $wp_registered_widgets, $widget ) {
// Nothing can be done if the widget is created using pre WP2.8 API :(
// There is no object, so we can't access it to get the widget options
return isset( $wp_registered_widgets[ $widget ]['callback'] ) &&
is_array( $wp_registered_widgets[ $widget ]['callback'] ) &&
isset( $wp_registered_widgets[ $widget ]['callback'][0] ) &&
is_object( $wp_registered_widgets[ $widget ]['callback'][0] ) &&
method_exists( $wp_registered_widgets[ $widget ]['callback'][0], 'get_settings' );
}
/**
* Get widgets settings and number.
*
* @since 3.1
*
* @param array $wp_registered_widgets Array of all registered widgets.
* @param string $widget String that identifies the widget.
* @return array An array containing the widget settings and number.
*/
protected function get_widget_data( $wp_registered_widgets, $widget ) {
$widget_settings = $wp_registered_widgets[ $widget ]['callback'][0]->get_settings();
$number = $wp_registered_widgets[ $widget ]['params'][0]['number'];
return array(
'settings' => $widget_settings,
'number' => $number,
);
}
}

View File

@@ -0,0 +1,210 @@
<?php
/**
* @package Polylang
*/
/**
* Filters content by language on frontend
*
* @since 1.2
*/
class PLL_Frontend_Filters extends PLL_Filters {
/**
* Constructor: setups filters and actions
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
// Filters the WordPress locale
add_filter( 'locale', array( $this, 'get_locale' ) );
// Filter sticky posts by current language
add_filter( 'option_sticky_posts', array( $this, 'option_sticky_posts' ) );
// Rewrites archives links to filter them by language
add_filter( 'getarchives_join', array( $this, 'getarchives_join' ), 10, 2 );
add_filter( 'getarchives_where', array( $this, 'getarchives_where' ), 10, 2 );
// Filters the widgets according to the current language
add_filter( 'widget_display_callback', array( $this, 'widget_display_callback' ) );
if ( $this->options['media_support'] ) {
add_filter( 'widget_media_image_instance', array( $this, 'widget_media_instance' ), 1 ); // Since WP 4.8
}
// Strings translation ( must be applied before WordPress applies its default formatting filters )
foreach ( array( 'widget_text', 'widget_title' ) as $filter ) {
add_filter( $filter, 'pll__', 1 );
}
// Translates biography
add_filter( 'get_user_metadata', array( $this, 'get_user_metadata' ), 10, 4 );
if ( Polylang::is_ajax_on_front() ) {
add_filter( 'load_textdomain_mofile', array( $this, 'load_textdomain_mofile' ) );
}
}
/**
* Returns the locale based on current language
*
* @since 0.1
*
* @return string
*/
public function get_locale() {
return $this->curlang->locale;
}
/**
* Filters sticky posts by current language.
*
* @since 0.8
*
* @param int[] $posts List of sticky posts ids.
* @return int[] Modified list of sticky posts ids
*/
public function option_sticky_posts( $posts ) {
global $wpdb;
// Do not filter sticky posts on REST requests as $this->curlang is *not* the 'lang' parameter set in the request
if ( ! defined( 'REST_REQUEST' ) && ! empty( $this->curlang ) && ! empty( $posts ) ) {
$_posts = wp_cache_get( 'sticky_posts', 'options' ); // This option is usually cached in 'all_options' by WP.
$tt_id = $this->curlang->get_tax_prop( 'language', 'term_taxonomy_id' );
if ( empty( $_posts ) || ! is_array( $_posts ) || empty( $_posts[ $tt_id ] ) || ! is_array( $_posts[ $tt_id ] ) ) {
$posts = array_map( 'intval', $posts );
$posts = implode( ',', $posts );
$languages = array();
foreach ( $this->model->get_languages_list() as $language ) {
$languages[] = $language->get_tax_prop( 'language', 'term_taxonomy_id' );
}
$_posts = array_fill_keys( $languages, array() ); // Init with empty arrays
$languages = implode( ',', $languages );
// PHPCS:ignore WordPress.DB.PreparedSQL
$relations = $wpdb->get_results( "SELECT object_id, term_taxonomy_id FROM {$wpdb->term_relationships} WHERE object_id IN ({$posts}) AND term_taxonomy_id IN ({$languages})" );
foreach ( $relations as $relation ) {
$_posts[ $relation->term_taxonomy_id ][] = (int) $relation->object_id;
}
wp_cache_add( 'sticky_posts', $_posts, 'options' );
}
$posts = $_posts[ $tt_id ];
}
return $posts;
}
/**
* Modifies the sql request for wp_get_archives to filter by the current language
*
* @since 1.9
*
* @param string $sql JOIN clause
* @param array $r wp_get_archives arguments
* @return string modified JOIN clause
*/
public function getarchives_join( $sql, $r ) {
return ! empty( $r['post_type'] ) && $this->model->is_translated_post_type( $r['post_type'] ) ? $sql . $this->model->post->join_clause() : $sql;
}
/**
* Modifies the sql request for wp_get_archives to filter by the current language
*
* @since 1.9
*
* @param string $sql WHERE clause
* @param array $r wp_get_archives arguments
* @return string modified WHERE clause
*/
public function getarchives_where( $sql, $r ) {
if ( ! $this->curlang instanceof PLL_Language ) {
return $sql;
}
if ( empty( $r['post_type'] ) || ! $this->model->is_translated_post_type( $r['post_type'] ) ) {
return $sql;
}
return $sql . $this->model->post->where_clause( $this->curlang );
}
/**
* Filters the widgets according to the current language
* Don't display if a language filter is set and this is not the current one
* Needed for {@see https://developer.wordpress.org/reference/functions/the_widget/ the_widget()}.
*
* @since 0.3
*
* @param array $instance Widget settings
* @return bool|array false if we hide the widget, unmodified $instance otherwise
*/
public function widget_display_callback( $instance ) {
return ! empty( $instance['pll_lang'] ) && $instance['pll_lang'] != $this->curlang->slug ? false : $instance;
}
/**
* Translates media in media widgets
*
* @since 2.1.5
*
* @param array $instance Widget instance data
* @return array
*/
public function widget_media_instance( $instance ) {
if ( empty( $instance['pll_lang'] ) && $instance['attachment_id'] && $tr_id = pll_get_post( $instance['attachment_id'] ) ) {
$instance['attachment_id'] = $tr_id;
$attachment = get_post( $tr_id );
if ( $instance['caption'] && ! empty( $attachment->post_excerpt ) ) {
$instance['caption'] = $attachment->post_excerpt;
}
if ( $instance['alt'] && $alt_text = get_post_meta( $tr_id, '_wp_attachment_image_alt', true ) ) {
$instance['alt'] = $alt_text;
}
if ( $instance['image_title'] && ! empty( $attachment->post_title ) ) {
$instance['image_title'] = $attachment->post_title;
}
}
return $instance;
}
/**
* Translates the biography.
*
* @since 0.9
*
* @param null $null Expecting the default null value.
* @param int $id The user id.
* @param string $meta_key The metadata key.
* @param bool $single Whether to return only the first value of the specified $meta_key.
* @return string|null
*/
public function get_user_metadata( $null, $id, $meta_key, $single ) {
return 'description' === $meta_key && ! empty( $this->curlang ) && ! $this->curlang->is_default ? get_user_meta( $id, 'description_' . $this->curlang->slug, $single ) : $null;
}
/**
* Filters the translation files to load when doing ajax on front
* This is needed because WP the language files associated to the user locale when a user is logged in
*
* @since 2.2.6
*
* @param string $mofile Translation file name
* @return string
*/
public function load_textdomain_mofile( $mofile ) {
$user_locale = get_user_locale();
return str_replace( "{$user_locale}.mo", "{$this->curlang->locale}.mo", $mofile );
}
}

View File

@@ -0,0 +1,228 @@
<?php
/**
* @package Polylang
*/
/**
* Manages links filters and url of translations on frontend
*
* @since 1.2
*/
class PLL_Frontend_Links extends PLL_Links {
/**
* Internal non persistent cache object.
*
* @var PLL_Cache<string>
*/
public $cache;
/**
* Constructor
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->curlang = &$polylang->curlang;
$this->cache = new PLL_Cache();
}
/**
* Returns the url of the translation (if it exists) of the current page.
*
* @since 0.1
*
* @param PLL_Language $language Language object.
* @return string
*/
public function get_translation_url( $language ) {
global $wp_query;
if ( false !== $translation_url = $this->cache->get( 'translation_url:' . $language->slug ) ) {
return $translation_url;
}
// Make sure that we have the queried object
// See https://wordpress.org/support/topic/patch-for-fixing-a-notice
$queried_object_id = $wp_query->get_queried_object_id();
/**
* Filters the translation url before Polylang attempts to find one.
* Internally used by Polylang for the static front page and posts page.
*
* @since 1.8
*
* @param string $url Empty string or the url of the translation of the current page.
* @param PLL_Language $language Language of the translation.
* @param int $queried_object_id Queried object ID.
*/
if ( ! $url = apply_filters( 'pll_pre_translation_url', '', $language, $queried_object_id ) ) {
$qv = $wp_query->query_vars;
// Post and attachment
if ( is_single() && ( $this->options['media_support'] || ! is_attachment() ) && ( $id = $this->model->post->get( $queried_object_id, $language ) ) && $this->model->post->current_user_can_read( $id ) ) {
$url = get_permalink( $id );
}
// Page
elseif ( is_page() && ( $id = $this->model->post->get( $queried_object_id, $language ) ) && $this->model->post->current_user_can_read( $id ) ) {
$url = get_page_link( $id );
}
elseif ( is_search() ) {
$url = $this->get_archive_url( $language );
// Special case for search filtered by translated taxonomies: taxonomy terms are translated in the translation url
if ( ! empty( $wp_query->tax_query->queries ) ) {
foreach ( $wp_query->tax_query->queries as $tax_query ) {
if ( ! empty( $tax_query['taxonomy'] ) && $this->model->is_translated_taxonomy( $tax_query['taxonomy'] ) ) {
$tax = get_taxonomy( $tax_query['taxonomy'] );
$terms = get_terms( array( 'taxonomy' => $tax->name, 'fields' => 'id=>slug' ) ); // Filtered by current language
foreach ( $tax_query['terms'] as $slug ) {
$term_id = array_search( $slug, $terms ); // What is the term_id corresponding to taxonomy term?
if ( $term_id && $term_id = $this->model->term->get_translation( $term_id, $language ) ) { // Get the translated term_id
$term = get_term( $term_id, $tax->name );
if ( ! $term instanceof WP_Term ) {
continue;
}
$url = str_replace( $slug, $term->slug, $url );
}
}
}
}
}
}
// Translated taxonomy
// Take care that is_tax() is false for categories and tags
elseif ( ( is_category() || is_tag() || is_tax() ) && ( $term = get_queried_object() ) && $this->model->is_translated_taxonomy( $term->taxonomy ) ) {
$lang = $this->model->term->get_language( $term->term_id );
if ( ! $lang || $language->slug == $lang->slug ) {
$url = get_term_link( $term, $term->taxonomy ); // Self link
}
elseif ( $tr_id = $this->model->term->get_translation( $term->term_id, $language ) ) {
$tr_term = get_term( $tr_id, $term->taxonomy );
if ( $tr_term instanceof WP_Term ) {
// Check if translated term ( or children ) have posts
$count = $tr_term->count || ( is_taxonomy_hierarchical( $term->taxonomy ) && array_sum( wp_list_pluck( get_terms( array( 'taxonomy' => $term->taxonomy, 'child_of' => $tr_term->term_id, 'lang' => $language->slug ) ), 'count' ) ) );
/**
* Filter whether to hide an archive translation url
*
* @since 2.2.4
*
* @param bool $hide True to hide the translation url.
* defaults to true when the translated archive is empty, false otherwise.
* @param string $lang The language code of the translation
* @param array $args Arguments used to evaluated the number of posts in the archive
*/
if ( ! apply_filters( 'pll_hide_archive_translation_url', ! $count, $language->slug, array( 'taxonomy' => $term->taxonomy ) ) ) {
$url = get_term_link( $tr_term, $term->taxonomy );
}
}
}
}
// Post type archive
elseif ( is_post_type_archive() ) {
if ( $this->model->is_translated_post_type( $qv['post_type'] ) ) {
$args = array( 'post_type' => $qv['post_type'] );
$count = $this->model->count_posts( $language, $args );
/** This filter is documented in frontend/frontend-links.php */
if ( ! apply_filters( 'pll_hide_archive_translation_url', ! $count, $language->slug, $args ) ) {
$url = $this->get_archive_url( $language );
}
}
}
// Other archives
elseif ( is_archive() ) {
$keys = array( 'post_type', 'm', 'year', 'monthnum', 'day', 'author', 'author_name' );
$keys = array_merge( $keys, $this->model->get_filtered_taxonomies_query_vars() );
$args = array_intersect_key( $qv, array_flip( $keys ) );
$count = $this->model->count_posts( $language, $args );
/** This filter is documented in frontend/frontend-links.php */
if ( ! apply_filters( 'pll_hide_archive_translation_url', ! $count, $language->slug, $args ) ) {
$url = $this->get_archive_url( $language );
}
}
// Front page when it is the list of posts
elseif ( is_front_page() ) {
$url = $this->get_home_url( $language );
}
}
$url = ! empty( $url ) && ! is_wp_error( $url ) ? $url : null;
/**
* Filter the translation url of the current page before Polylang caches it
*
* @since 1.1.2
*
* @param null|string $url The translation url, null if none was found
* @param string $language The language code of the translation
*/
$translation_url = (string) apply_filters( 'pll_translation_url', $url, $language->slug );
// Don't cache before template_redirect to avoid a conflict with Barrel + WP Bakery Page Builder
if ( did_action( 'template_redirect' ) ) {
$this->cache->set( 'translation_url:' . $language->slug, $translation_url );
}
return $translation_url;
}
/**
* Get the translation of the current archive url
* used also for search
*
* @since 1.2
*
* @param PLL_Language $language An object representing a language.
* @return string
*/
public function get_archive_url( $language ) {
$url = pll_get_requested_url();
$url = $this->links_model->switch_language_in_link( $url, $language );
$url = $this->links_model->remove_paged_from_link( $url );
/**
* Filter the archive url
*
* @since 1.6
*
* @param string $url Url of the archive
* @param object $language Language of the archive
*/
return apply_filters( 'pll_get_archive_url', $url, $language );
}
/**
* Returns the home url in the right language.
*
* @since 0.1
*
* @param PLL_Language|string $language Optional, defaults to current language.
* @param bool $is_search Optional, whether we need the home url for a search form, defaults to false.
*/
public function get_home_url( $language = '', $is_search = false ) {
if ( empty( $language ) ) {
$language = $this->curlang;
}
return parent::get_home_url( $language, $is_search );
}
}

View File

@@ -0,0 +1,339 @@
<?php
/**
* @package Polylang
*/
/**
* Manages custom menus translations as well as the language switcher menu item on frontend
*
* @since 1.2
*/
class PLL_Frontend_Nav_Menu extends PLL_Nav_Menu {
/**
* Current language.
*
* @var PLL_Language|null|false
*/
public $curlang;
/**
* Constructor
*
* @since 1.2
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->curlang = &$polylang->curlang;
// Split the language switcher menu item in several language menu items
add_filter( 'wp_get_nav_menu_items', array( $this, 'wp_get_nav_menu_items' ), 20 ); // after the customizer menus
add_filter( 'wp_nav_menu_objects', array( $this, 'wp_nav_menu_objects' ) );
add_filter( 'nav_menu_link_attributes', array( $this, 'nav_menu_link_attributes' ), 10, 2 );
// Filters menus by language
add_filter( 'theme_mod_nav_menu_locations', array( $this, 'nav_menu_locations' ), 20 );
add_filter( 'wp_nav_menu_args', array( $this, 'wp_nav_menu_args' ) );
// The customizer
if ( isset( $_POST['wp_customize'], $_POST['customized'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_before_customizer' ) );
add_filter( 'wp_nav_menu_args', array( $this, 'filter_args_after_customizer' ), 2000 );
}
}
/**
* Sorts menu items by menu order.
*
* @since 1.7.9
*
* @param stdClass $a The first object to compare.
* @param stdClass $b The second object to compare.
* @return int -1 or 1 if $a is considered to be respectively less than or greater than $b.
*/
protected function usort_menu_items( $a, $b ) {
return ( $a->menu_order < $b->menu_order ) ? -1 : 1;
}
/**
* Format a language switcher menu item title based on options
*
* @since 2.2.6
*
* @param string $flag Formatted flag
* @param string $name Language name
* @param array $options Language switcher options
* @return string Formatted menu item title
*/
protected function get_item_title( $flag, $name, $options ) {
if ( $options['show_flags'] ) {
if ( $options['show_names'] ) {
$title = sprintf( '%1$s<span style="margin-%2$s:0.3em;">%3$s</span>', $flag, is_rtl() ? 'right' : 'left', esc_html( $name ) );
} else {
$title = $flag;
}
} else {
$title = esc_html( $name );
}
return $title;
}
/**
* Splits the one language switcher menu item of backend in several menu items on frontend.
* Takes care to menu_order as it is used later in wp_nav_menu().
*
* @since 1.1.1
*
* @param stdClass[] $items Menu items.
* @return stdClass[] Modified menu items.
*/
public function wp_get_nav_menu_items( $items ) {
if ( empty( $this->curlang ) ) {
return $items;
}
if ( doing_action( 'customize_register' ) ) { // needed since WP 4.3, doing_action available since WP 3.9
return $items;
}
// The customizer menus does not sort the items and we need them to be sorted before splitting the language switcher
usort( $items, array( $this, 'usort_menu_items' ) );
$new_items = array();
$offset = 0;
foreach ( $items as $item ) {
if ( $options = get_post_meta( $item->ID, '_pll_menu_item', true ) ) {
/** This filter is documented in include/switcher.php */
$options = apply_filters( 'pll_the_languages_args', $options ); // Honor the filter here for 'show_flags', 'show_names' and 'dropdown'.
$switcher = new PLL_Switcher();
$args = array_merge( array( 'raw' => 1 ), $options );
/** @var array */
$the_languages = $switcher->the_languages( PLL()->links, $args );
// parent item for dropdown
if ( ! empty( $options['dropdown'] ) ) {
$name = isset( $options['display_names_as'] ) && 'slug' === $options['display_names_as'] ? $this->curlang->slug : $this->curlang->name;
$item->title = $this->get_item_title( $this->curlang->get_display_flag( empty( $options['show_names'] ) ? 'alt' : 'no-alt' ), $name, $options );
$item->attr_title = '';
$item->classes = array( 'pll-parent-menu-item' );
$item->menu_order += $offset;
$new_items[] = $item;
++$offset;
}
$i = 0; // for incrementation of menu order only in case of dropdown
foreach ( $the_languages as $lang ) {
++$i;
$lang_item = clone $item;
$lang_item->ID = $lang_item->ID . '-' . $lang['slug']; // A unique ID
$lang_item->title = $this->get_item_title( $lang['flag'], $lang['name'], $options );
$lang_item->attr_title = '';
$lang_item->url = $lang['url'];
$lang_item->lang = $lang['locale']; // Save this for use in nav_menu_link_attributes
$lang_item->classes = $lang['classes'];
if ( ! empty( $options['dropdown'] ) ) {
$lang_item->menu_order = $item->menu_order + $i;
$lang_item->menu_item_parent = $item->db_id;
$lang_item->db_id = 0; // to avoid recursion
} else {
$lang_item->menu_order += $offset;
}
$new_items[] = $lang_item;
++$offset;
}
--$offset;
} else {
$item->menu_order += $offset;
$new_items[] = $item;
}
}
return $new_items;
}
/**
* Returns the ancestors of a menu item.
*
* @since 1.1.1
*
* @param stdClass $item Menu item.
* @return int[] Ancestors ids.
*/
public function get_ancestors( $item ) {
$ids = array();
$_anc_id = (int) $item->db_id;
while ( ( $_anc_id = get_post_meta( $_anc_id, '_menu_item_menu_item_parent', true ) ) && ! in_array( $_anc_id, $ids ) ) {
$ids[] = $_anc_id;
}
return $ids;
}
/**
* Removes current-menu and current-menu-ancestor classes to lang switcher when not on the home page.
*
* @since 1.1.1
*
* @param stdClass[] $items An array of menu items.
* @return stdClass[]
*/
public function wp_nav_menu_objects( $items ) {
$r_ids = $k_ids = array();
foreach ( $items as $item ) {
if ( ! empty( $item->classes ) && is_array( $item->classes ) ) {
if ( in_array( 'current-lang', $item->classes ) ) {
$item->current = false;
$item->classes = array_diff( $item->classes, array( 'current-menu-item' ) );
$r_ids = array_merge( $r_ids, $this->get_ancestors( $item ) ); // Remove the classes for these ancestors
} elseif ( in_array( 'current-menu-item', $item->classes ) ) {
$k_ids = array_merge( $k_ids, $this->get_ancestors( $item ) ); // Keep the classes for these ancestors
}
}
}
$r_ids = array_diff( $r_ids, $k_ids );
foreach ( $items as $item ) {
if ( ! empty( $item->db_id ) && in_array( $item->db_id, $r_ids ) ) {
$item->classes = array_diff( $item->classes, array( 'current-menu-ancestor', 'current-menu-parent', 'current_page_parent', 'current_page_ancestor' ) );
}
}
return $items;
}
/**
* Adds hreflang attribute for the language switcher menu items.
* available since WP 3.6.
*
* @since 1.1
*
* @param string[] $atts HTML attributes applied to the menu item's `<a>` element.
* @param stdClass $item Menu item.
* @return string[] Modified attributes.
*/
public function nav_menu_link_attributes( $atts, $item ) {
if ( isset( $item->lang ) ) {
$atts['lang'] = $atts['hreflang'] = esc_attr( $item->lang );
}
return $atts;
}
/**
* Fills the theme nav menus locations with the right menu in the right language
* Needs to wait for the language to be defined
*
* @since 1.2
*
* @param array|bool $menus list of nav menus locations, false if menu locations have not been filled yet
* @return array|bool modified list of nav menus locations
*/
public function nav_menu_locations( $menus ) {
if ( is_array( $menus ) && ! empty( $this->curlang ) ) {
// First get multilingual menu locations from DB
$theme = get_option( 'stylesheet' );
foreach ( array_keys( $menus ) as $loc ) {
$menus[ $loc ] = empty( $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ] ) ? 0 : $this->options['nav_menus'][ $theme ][ $loc ][ $this->curlang->slug ];
}
// Support for theme customizer
if ( is_customize_preview() ) {
global $wp_customize;
foreach ( $wp_customize->unsanitized_post_values() as $key => $value ) {
if ( false !== strpos( $key, 'nav_menu_locations[' ) ) {
$loc = substr( trim( $key, ']' ), 19 );
$infos = $this->explode_location( $loc );
if ( $infos['lang'] === $this->curlang->slug ) {
$menus[ $infos['location'] ] = (int) $value;
} elseif ( $this->curlang->is_default ) {
$menus[ $loc ] = (int) $value;
}
}
}
}
}
return $menus;
}
/**
* Attempts to translate the nav menu when it is hardcoded or when no location is defined in wp_nav_menu().
*
* @since 1.7.10
*
* @param array $args Array of `wp_nav_menu()` arguments.
* @return array
*/
public function wp_nav_menu_args( $args ) {
$theme = get_option( 'stylesheet' );
if ( empty( $this->curlang ) || empty( $this->options['nav_menus'][ $theme ] ) ) {
return $args;
}
// Get the nav menu based on the requested menu
$menu = wp_get_nav_menu_object( $args['menu'] );
// Attempt to find a translation of this menu
// This obviously does not work if the nav menu has no associated theme location
if ( $menu ) {
foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
if ( in_array( $menu->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
$args['menu'] = $menus[ $this->curlang->slug ];
return $args;
}
}
}
// Get the first menu that has items and and is in the current language if we still can't find a menu
if ( ! $menu && ! $args['theme_location'] ) {
$menus = wp_get_nav_menus();
foreach ( $menus as $menu_maybe ) {
if ( wp_get_nav_menu_items( $menu_maybe->term_id, array( 'update_post_term_cache' => false ) ) ) {
foreach ( $this->options['nav_menus'][ $theme ] as $menus ) {
if ( in_array( $menu_maybe->term_id, $menus ) && ! empty( $menus[ $this->curlang->slug ] ) ) {
$args['menu'] = $menus[ $this->curlang->slug ];
return $args;
}
}
}
}
}
return $args;
}
/**
* Filters the nav menu location before the customizer so that it matches the temporary location in the customizer
*
* @since 1.8
*
* @param array $args wp_nav_menu $args
* @return array modified $args
*/
public function filter_args_before_customizer( $args ) {
if ( ! empty( $this->curlang ) ) {
$args['theme_location'] = $this->combine_location( $args['theme_location'], $this->curlang );
}
return $args;
}
/**
* Filters the nav menu location after the customizer to get back the true nav menu location for the theme
*
* @since 1.8
*
* @param array $args wp_nav_menu $args
* @return array modified $args
*/
public function filter_args_after_customizer( $args ) {
$infos = $this->explode_location( $args['theme_location'] );
$args['theme_location'] = $infos['location'];
return $args;
}
}

View File

@@ -0,0 +1,318 @@
<?php
/**
* @package Polylang
*/
/**
* Manages the static front page and the page for posts on frontend
*
* @since 1.8
*/
class PLL_Frontend_Static_Pages extends PLL_Static_Pages {
/**
* Instance of a child class of PLL_Links_Model.
*
* @var PLL_Links_Model
*/
protected $links_model;
/**
* @var PLL_Frontend_Links|null
*/
protected $links;
/**
* Stores plugin's options.
*
* @var array
*/
protected $options;
/**
* Constructor: setups filters and actions.
*
* @since 1.8
*
* @param object $polylang The Polylang object.
*/
public function __construct( &$polylang ) {
parent::__construct( $polylang );
$this->links_model = &$polylang->links_model;
$this->links = &$polylang->links;
$this->options = &$polylang->options;
add_action( 'pll_home_requested', array( $this, 'pll_home_requested' ) );
// Manages the redirection of the homepage.
add_filter( 'redirect_canonical', array( $this, 'redirect_canonical' ) );
add_filter( 'pll_pre_translation_url', array( $this, 'pll_pre_translation_url' ), 10, 3 );
add_filter( 'pll_check_canonical_url', array( $this, 'pll_check_canonical_url' ) );
add_filter( 'pll_set_language_from_query', array( $this, 'page_on_front_query' ), 10, 2 );
add_filter( 'pll_set_language_from_query', array( $this, 'page_for_posts_query' ), 10, 2 );
// Specific cases for the customizer.
add_action( 'customize_register', array( $this, 'filter_customizer' ) );
}
/**
* Translates the page_id query var when the site root page is requested
*
* @since 1.8
*
* @return void
*/
public function pll_home_requested() {
set_query_var( 'page_id', $this->curlang->page_on_front );
}
/**
* Manages the canonical redirect of the homepage when using a page on front.
*
* @since 0.1
*
* @param string $redirect_url The redirect url.
* @return string|false The modified url, false if the redirect is canceled.
*/
public function redirect_canonical( $redirect_url ) {
if ( is_page() && ! is_feed() && get_queried_object_id() == $this->curlang->page_on_front ) {
$url = is_paged() ? $this->links_model->add_paged_to_link( $this->links->get_home_url(), get_query_var( 'page' ) ) : $this->links->get_home_url();
// Don't forget additional query vars
$query = wp_parse_url( $redirect_url, PHP_URL_QUERY );
if ( ! empty( $query ) ) {
parse_str( $query, $query_vars );
$query_vars = rawurlencode_deep( $query_vars ); // WP encodes query vars values
$url = add_query_arg( $query_vars, $url );
}
return $url;
}
return $redirect_url;
}
/**
* Translates the url of the page on front and page for posts.
*
* @since 1.8
*
* @param string $url Empty string or the url of the translation of the current page.
* @param PLL_Language $language Language of the translation.
* @param int $queried_object_id Queried object ID.
* @return string The translation url.
*/
public function pll_pre_translation_url( $url, $language, $queried_object_id ) {
if ( empty( $queried_object_id ) ) {
return $url;
}
// Page for posts.
if ( $GLOBALS['wp_query']->is_posts_page ) {
$id = $this->model->post->get( $queried_object_id, $language );
if ( ! empty( $id ) ) {
return (string) get_permalink( $id );
}
}
// Page on front.
if ( is_front_page() && ! empty( $language->page_on_front ) ) {
$id = $this->model->post->get( $queried_object_id, $language );
if ( $language->page_on_front === $id ) {
return $language->get_home_url();
}
}
return $url;
}
/**
* Prevents the canonical redirect if we are on a static front page.
*
* @since 1.8
*
* @param string $redirect_url The redirect url.
* @return string|false
*/
public function pll_check_canonical_url( $redirect_url ) {
return $this->options['redirect_lang'] && ! $this->options['force_lang'] && ! empty( $this->curlang->page_on_front ) && is_page( $this->curlang->page_on_front ) ? false : $redirect_url;
}
/**
* Is the query for a the static front page (redirected from the language page)?
*
* @since 2.3
*
* @param WP_Query $query The WP_Query object.
* @return bool
*/
protected function is_front_page( $query ) {
$query = array_diff( array_keys( $query->query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) );
return 1 === count( $query ) && in_array( 'lang', $query );
}
/**
* Setups query vars when requesting a static front page
*
* @since 1.8
*
* @param PLL_Language|false $lang The current language, false if it is not set yet.
* @param WP_Query $query The main WP query.
* @return PLL_Language|false
*/
public function page_on_front_query( $lang, $query ) {
if ( ! empty( $lang ) || ! $this->page_on_front ) {
return $lang;
}
// Redirect the language page to the homepage when using a static front page
if ( ( $this->options['redirect_lang'] || $this->options['hide_default'] ) && $this->is_front_page( $query ) && $lang = $this->model->get_language( get_query_var( 'lang' ) ) ) {
$query->is_archive = $query->is_tax = false;
if ( 'page' === get_option( 'show_on_front' ) && ! empty( $lang->page_on_front ) ) {
$query->set( 'page_id', $lang->page_on_front );
$query->is_singular = $query->is_page = true;
unset( $query->query_vars['lang'], $query->queried_object ); // Reset queried object
} else {
// Handle case where the static front page hasn't be translated to avoid a possible infinite redirect loop.
$query->is_home = true;
}
}
// Fix paged static front page in plain permalinks when Settings > Reading doesn't match the default language
elseif ( ! $this->links_model->using_permalinks && count( $query->query ) === 1 && ! empty( $query->query['page'] ) ) {
$lang = $this->model->get_default_language();
if ( empty( $lang ) ) {
return $lang;
}
$query->set( 'page_id', $lang->page_on_front );
$query->is_singular = $query->is_page = true;
$query->is_archive = $query->is_tax = false;
unset( $query->query_vars['lang'], $query->queried_object ); // Reset queried object
}
// Set the language when requesting a static front page
else {
$page_id = $this->get_page_id( $query );
$languages = $this->model->get_languages_list();
$pages = wp_list_pluck( $languages, 'page_on_front' );
if ( ! empty( $page_id ) && false !== $n = array_search( $page_id, $pages ) ) {
$lang = $languages[ $n ];
}
}
// Fix <!--nextpage--> for page_on_front
if ( ( $this->options['force_lang'] < 2 || ! $this->options['redirect_lang'] ) && $this->links_model->using_permalinks && ! empty( $lang ) && isset( $query->query['paged'] ) ) {
$query->set( 'page', $query->query['paged'] );
unset( $query->query['paged'] );
} elseif ( ! $this->links_model->using_permalinks && ! empty( $query->query['page'] ) ) {
$query->is_paged = true;
}
return $lang;
}
/**
* Setups query vars when requesting a posts page
*
* @since 1.8
*
* @param PLL_Language|false $lang The current language, false if it is not set yet.
* @param WP_Query $query The main WP query.
* @return PLL_Language|false
*/
public function page_for_posts_query( $lang, $query ) {
if ( ! empty( $lang ) || ! $this->page_for_posts ) {
return $lang;
}
$page_id = $this->get_page_id( $query );
if ( empty( $page_id ) ) {
return $lang;
}
$pages = $this->model->get_languages_list( array( 'fields' => 'page_for_posts' ) );
$pages = array_filter( $pages );
if ( in_array( $page_id, $pages ) ) {
_prime_post_caches( $pages ); // Fill the cache with all pages for posts to avoid one query per page later.
$lang = $this->model->post->get_language( $page_id );
$query->is_singular = $query->is_page = false;
$query->is_home = $query->is_posts_page = true;
}
return $lang;
}
/**
* Get the queried page_id (if it exists ).
*
* If permalinks are used, WordPress does set and use `$query->queried_object_id` and sets `$query->query_vars['page_id']` to 0,
* and does set and use `$query->query_vars['page_id']` if permalinks are not used :(.
*
* @since 1.5
*
* @param WP_Query $query Instance of WP_Query.
* @return int The page_id.
*/
protected function get_page_id( $query ) {
if ( ! empty( $query->query_vars['pagename'] ) && isset( $query->queried_object_id ) ) {
return $query->queried_object_id;
}
if ( isset( $query->query_vars['page_id'] ) ) {
return $query->query_vars['page_id'];
}
return 0; // No page queried.
}
/**
* Adds support for the theme customizer.
*
* @since 3.4.2
*
* @return void
*/
public function filter_customizer() {
add_filter( 'pre_option_page_on_front', array( $this, 'customize_page' ), 20 ); // After the customizer.
add_filter( 'pre_option_page_for_post', array( $this, 'customize_page' ), 20 );
add_filter( 'pll_pre_translation_url', array( $this, 'customize_translation_url' ), 20, 2 ); // After the generic hook in this class.
}
/**
* Translates the page ID when customized.
*
* @since 3.4.2
*
* @param int|false $pre A page ID if the setting is customized, false otherwise.
* @return int|false
*/
public function customize_page( $pre ) {
return is_numeric( $pre ) ? pll_get_post( (int) $pre ) : $pre;
}
/**
* Fixes the translation URL if the option 'show_on_front' is customized.
*
* @since 3.4.2
*
* @param string $url An empty string or the URL of the translation of the current page.
* @param PLL_Language $language The language of the translation.
* @return string
*/
public function customize_translation_url( $url, $language ) {
if ( 'posts' === get_option( 'show_on_front' ) && is_front_page() ) {
// When the page on front displays posts, the home URL is the same as the search URL.
return $language->get_search_url();
}
return $url;
}
}

View File

@@ -0,0 +1,287 @@
<?php
/**
* @package Polylang
*/
/**
* Main Polylang class when on frontend, accessible from @see PLL().
*
* @since 1.2
*/
class PLL_Frontend extends PLL_Base {
/**
* Current language.
*
* @var PLL_Language|null
*/
public $curlang;
/**
* @var PLL_Frontend_Auto_Translate|null
*/
public $auto_translate;
/**
* The class selecting the current language.
*
* @var PLL_Choose_Lang|null
*/
public $choose_lang;
/**
* @var PLL_Frontend_Filters|null
*/
public $filters;
/**
* @var PLL_Frontend_Filters_Links|null
*/
public $filters_links;
/**
* @var PLL_Frontend_Filters_Search|null
*/
public $filters_search;
/**
* @var PLL_Frontend_Links|null
*/
public $links;
/**
* @var PLL_Frontend_Nav_Menu|null
*/
public $nav_menu;
/**
* @var PLL_Frontend_Static_Pages|null
*/
public $static_pages;
/**
* @var PLL_Frontend_Filters_Widgets|null
*/
public $filters_widgets;
/**
* Constructor.
*
* @since 1.2
*
* @param PLL_Links_Model $links_model Reference to the links model.
*/
public function __construct( &$links_model ) {
parent::__construct( $links_model );
add_action( 'pll_language_defined', array( $this, 'pll_language_defined' ), 1 );
// Avoids the language being the queried object when querying multiple taxonomies
add_action( 'parse_tax_query', array( $this, 'parse_tax_query' ), 1 );
// Filters posts by language
add_action( 'parse_query', array( $this, 'parse_query' ), 6 );
// Not before 'check_canonical_url'
if ( ! defined( 'PLL_AUTO_TRANSLATE' ) || PLL_AUTO_TRANSLATE ) {
add_action( 'template_redirect', array( $this, 'auto_translate' ), 7 );
}
add_action( 'admin_bar_menu', array( $this, 'remove_customize_admin_bar' ), 41 ); // After WP_Admin_Bar::add_menus
/*
* Static front page and page for posts.
*
* Early instantiated to be able to correctly initialize language properties.
* Also loaded in customizer preview, directly reading the request as we act before WP.
*/
if ( 'page' === get_option( 'show_on_front' ) || ( isset( $_REQUEST['wp_customize'] ) && 'on' === $_REQUEST['wp_customize'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$this->static_pages = new PLL_Frontend_Static_Pages( $this );
}
$this->model->set_languages_ready();
}
/**
* Setups the language chooser based on options
*
* @since 1.2
*/
public function init() {
parent::init();
$this->links = new PLL_Frontend_Links( $this );
// Setup the language chooser
$c = array( 'Content', 'Url', 'Url', 'Domain' );
$class = 'PLL_Choose_Lang_' . $c[ $this->options['force_lang'] ];
$this->choose_lang = new $class( $this );
$this->choose_lang->init();
// Need to load nav menu class early to correctly define the locations in the customizer when the language is set from the content
$this->nav_menu = new PLL_Frontend_Nav_Menu( $this );
}
/**
* Setups filters and nav menus once the language has been defined
*
* @since 1.2
*
* @return void
*/
public function pll_language_defined() {
// Filters
$this->filters_links = new PLL_Frontend_Filters_Links( $this );
$this->filters = new PLL_Frontend_Filters( $this );
$this->filters_search = new PLL_Frontend_Filters_Search( $this );
$this->filters_widgets = new PLL_Frontend_Filters_Widgets( $this );
/*
* Redirects to canonical url before WordPress redirect_canonical
* but after Nextgen Gallery which hacks $_SERVER['REQUEST_URI'] !!!
* and restores it in 'template_redirect' with priority 1.
*/
$this->canonical = new PLL_Canonical( $this );
add_action( 'template_redirect', array( $this->canonical, 'check_canonical_url' ), 4 );
// Auto translate for Ajax
if ( ( ! defined( 'PLL_AUTO_TRANSLATE' ) || PLL_AUTO_TRANSLATE ) && wp_doing_ajax() ) {
$this->auto_translate();
}
}
/**
* When querying multiple taxonomies, makes sure that the language is not the queried object.
*
* @since 1.8
*
* @param WP_Query $query WP_Query object.
* @return void
*/
public function parse_tax_query( $query ) {
$pll_query = new PLL_Query( $query, $this->model );
$queried_taxonomies = $pll_query->get_queried_taxonomies();
if ( ! empty( $queried_taxonomies ) && 'language' == reset( $queried_taxonomies ) ) {
$query->tax_query->queried_terms['language'] = array_shift( $query->tax_query->queried_terms );
}
}
/**
* Modifies some query vars to "hide" that the language is a taxonomy and avoid conflicts.
*
* @since 1.2
*
* @param WP_Query $query WP_Query object.
* @return void
*/
public function parse_query( $query ) {
$qv = $query->query_vars;
$pll_query = new PLL_Query( $query, $this->model );
$taxonomies = $pll_query->get_queried_taxonomies();
// Allow filtering recent posts and secondary queries by the current language
if ( ! empty( $this->curlang ) ) {
$pll_query->filter_query( $this->curlang );
}
// Modifies query vars when the language is queried
if ( ! empty( $qv['lang'] ) || ( ! empty( $taxonomies ) && array( 'language' ) == array_values( $taxonomies ) ) ) {
// Do we query a custom taxonomy?
$taxonomies = array_diff( $taxonomies, array( 'language', 'category', 'post_tag' ) );
// Remove pages query when the language is set unless we do a search
// Take care not to break the single page, attachment and taxonomies queries!
if ( empty( $qv['post_type'] ) && ! $query->is_search && ! $query->is_singular && empty( $taxonomies ) && ! $query->is_category && ! $query->is_tag ) {
$query->set( 'post_type', 'post' );
}
// Unset the is_archive flag for language pages to prevent loading the archive template
// Keep archive flag for comment feed otherwise the language filter does not work
if ( empty( $taxonomies ) && ! $query->is_comment_feed && ! $query->is_post_type_archive && ! $query->is_date && ! $query->is_author && ! $query->is_category && ! $query->is_tag ) {
$query->is_archive = false;
}
// Unset the is_tax flag except if another custom tax is queried
if ( empty( $taxonomies ) && ( $query->is_category || $query->is_tag || $query->is_author || $query->is_post_type_archive || $query->is_date || $query->is_search || $query->is_feed ) ) {
$query->is_tax = false;
unset( $query->queried_object ); // FIXME useless?
}
}
}
/**
* Auto translate posts and terms ids
*
* @since 1.2
*
* @return void
*/
public function auto_translate() {
$this->auto_translate = new PLL_Frontend_Auto_Translate( $this );
}
/**
* Resets some variables when the blog is switched.
* Overrides the parent method.
*
* @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;
}
parent::switch_blog( $new_blog_id, $prev_blog_id );
// Need to check that some languages are defined when user is logged in, has several blogs, some without any languages.
if ( ! $this->is_active_on_current_site() || ! $this->model->has_languages() || ! did_action( 'pll_language_defined' ) ) {
return;
}
static $restore_curlang;
if ( empty( $restore_curlang ) ) {
$restore_curlang = $this->curlang->slug; // To always remember the current language through blogs.
}
$lang = $this->model->get_language( $restore_curlang );
$this->curlang = $lang ? $lang : $this->model->get_default_language();
if ( empty( $this->curlang ) ) {
return;
}
if ( isset( $this->static_pages ) ) {
$this->static_pages->init();
}
// Send the slug instead of the locale here to avoid conflicts with same locales.
$this->load_strings_translations( $this->curlang->slug );
}
/**
* Remove the customize admin bar on front-end when using a block theme.
*
* WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
* As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
* unless another plugin than Polylang interacts with the Customizer.
*
* @since 3.2
*
* @return void
*/
public function remove_customize_admin_bar() {
if ( ! $this->should_customize_menu_be_removed() ) {
return;
}
global $wp_admin_bar;
remove_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' ); // To avoid the script launch.
$wp_admin_bar->remove_menu( 'customize' );
}
}