-
-
Notifications
You must be signed in to change notification settings - Fork 56
Expand file tree
/
Copy pathplugin-polylang.php
More file actions
407 lines (350 loc) · 14.9 KB
/
plugin-polylang.php
File metadata and controls
407 lines (350 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<?php
/**
* @package The_SEO_Framework\Compat\Plugin\PolyLang
* @subpackage The_SEO_Framework\Compatibility
*/
namespace The_SEO_Framework;
\defined( 'THE_SEO_FRAMEWORK_PRESENT' ) or die;
use The_SEO_Framework\{
Helper\Query,
Meta\URI,
};
\add_action( 'the_seo_framework_sitemap_header', __NAMESPACE__ . '\\_polylang_set_sitemap_language' );
\add_filter( 'the_seo_framework_sitemap_endpoint_list', __NAMESPACE__ . '\\_polylang_register_sitemap_languages', 20 );
\add_filter( 'the_seo_framework_sitemap_hpt_query_args', __NAMESPACE__ . '\\_polylang_sitemap_append_non_translatables' );
\add_filter( 'the_seo_framework_sitemap_nhpt_query_args', __NAMESPACE__ . '\\_polylang_sitemap_append_non_translatables' );
\add_filter( 'the_seo_framework_title_from_custom_field', __NAMESPACE__ . '\\pll__' );
\add_filter( 'the_seo_framework_title_from_generation', __NAMESPACE__ . '\\pll__' );
\add_filter( 'the_seo_framework_custom_field_description', __NAMESPACE__ . '\\pll__' );
\add_filter( 'the_seo_framework_generated_description', __NAMESPACE__ . '\\pll__' );
\add_filter( 'the_seo_framework_front_init', __NAMESPACE__ . '\\_hijack_polylang_home_url' );
\add_filter( 'pll_home_url_white_list', __NAMESPACE__ . '\\_polylang_allow_tsf_home_url' );
\add_filter( 'pll_home_url_allow_list', __NAMESPACE__ . '\\_polylang_allow_tsf_home_url' );
\add_action( 'the_seo_framework_cleared_sitemap_transients', __NAMESPACE__ . '\\_polylang_flush_sitemap' );
\add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\\_defunct_badly_coded_polylang_script', 11 );
\add_filter( 'the_seo_framework_seo_column_keys_order', __NAMESPACE__ . '\\_polylang_seo_column_keys_order' );
/**
* Registeres more sitemaps for the robots.txt to parse.
*
* This has no other intended effect. But default permalinks may react more tsf_sitemap query values,
* specifically ?tsf_sitemap=_base_polylang_es&lang=es" (assumed, untested).
*
* @hook the_seo_framework_sitemap_endpoint_list 20
* @since 5.0.5
* @param array[] $list {
* A list of sitemap endpoints keyed by ID.
*
* @type string|false $lock_id Optional. The cache key to use for locking. Defaults to index 'id'.
* Set to false to disable locking.
* @type string|false $cache_id Optional. The cache key to use for storing. Defaults to index 'id'.
* Set to false to disable caching.
* @type string $endpoint The expected "pretty" endpoint, meant for administrative display.
* @type string $epregex The endpoint regex, following the home path regex.
* @type callable $callback The callback for the sitemap output.
* @type bool $robots Whether the endpoint should be mentioned in the robots.txt file.
* }
* @return array[]
*/
function _polylang_register_sitemap_languages( $list ) {
if ( empty( $list['base'] ) )
return $list;
if ( ! Helper\Compatibility::can_i_use( [
'functions' => [
'pll_languages_list',
'pll_default_language',
],
] ) ) return $list;
// Do most work outside of a loop. We have two loops because of this.
// We fall back to -1 because null/false match with '0'
switch ( \get_option( 'polylang' )['force_lang'] ?? -1 ) {
case 0: // The language is set from content.
foreach (
array_diff(
\pll_languages_list( [ 'hide_empty' => 1 ] ),
[ \pll_default_language() ],
)
as $language
) {
$list[ "_base_polylang_$language" ] = [
'endpoint' => URI\Utils::append_query_to_url(
$list['base']['endpoint'],
"lang=$language",
),
] + $list['base'];
}
break;
case 1: // The language is set from the directory name in pretty permalinks.
foreach (
array_diff(
\pll_languages_list( [ 'hide_empty' => 1 ] ),
[ \pll_default_language() ],
)
as $language
) {
// Get the language-specific home URL to determine the correct sitemap path
$lang_home_url = \function_exists( 'pll_home_url' ) ? \pll_home_url( $language ) : '';
if ( $lang_home_url ) {
// Parse the language-specific home URL to get the path
$lang_parsed = parse_url( $lang_home_url );
$lang_path = $lang_parsed['path'] ?? '';
// Remove the site's base path to get the language-specific part
$site_parsed = parse_url( \home_url() );
$site_path = rtrim( $site_parsed['path'] ?? '', '/' );
// Get the relative path for this language
if ( $site_path && str_starts_with( $lang_path, $site_path ) ) {
$relative_path = substr( $lang_path, \strlen( $site_path ) );
} else {
$relative_path = $lang_path;
}
$relative_path = trim( $relative_path, '/' );
// Build the endpoint with the correct language path
$endpoint = $relative_path ? "$relative_path/{$list['base']['endpoint']}" : $list['base']['endpoint'];
} else {
// Fallback to the original method if pll_home_url is not available
$endpoint = "$language/{$list['base']['endpoint']}";
}
$list[ "_base_polylang_$language" ] = [
'endpoint' => $endpoint,
] + $list['base'];
}
}
return $list;
}
/**
* Sets the correct Polylang query language for the sitemap based on the 'lang' GET parameter.
*
* When the user supplies a correct 'lang' query parameter, they can overwrite our testing for force_lang settings.
* This is a fallback solution because we get endless support requests for Polylang, and we wish that plugin would be
* rewritten from scratch.
*
* @hook the_seo_framework_sitemap_header 10
* @since 4.1.2
* @access private
*/
function _polylang_set_sitemap_language() {
if ( ! \function_exists( 'PLL' ) || ! ( \PLL() instanceof \PLL_Frontend ) ) return;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Arbitrary input expected.
$lang = $_GET['lang'] ?? '';
// Language codes are user-definable: copy Polylang's filtering.
// The preg_match's source: \PLL_Admin_Model::validate_lang();
if ( ! \is_string( $lang ) || ! \strlen( $lang ) || ! preg_match( '#^[a-z_-]+$#', $lang ) ) {
switch ( \get_option( 'polylang' )['force_lang'] ?? -1 ) {
case 0:
// Polylang determines language sporadically from content: can't be trusted. Overwrite.
$lang = \function_exists( 'pll_default_language' ) ? \pll_default_language() : $lang;
break;
default:
// Polylang can differentiate languages by (sub)domain/directory name early. No need to interfere. Cancel.
return;
}
}
// This will default to the default language when $lang is invalid or unregistered. This is fine.
$new_lang = \PLL()->model->get_language( $lang );
if ( $new_lang ) {
\PLL()->curlang = $new_lang;
\did_action( 'pll_language_defined' ) or \do_action( 'pll_language_defined' );
}
}
/**
* Appends nontranslatable post types to the sitemap query arguments.
* Only appends when the default sitemap language is displayed.
*
* TODO Should we fix this? If user unassigns a post type as translatable, previously "translated" posts are still
* found "translated" by this query. This query, however, is forwarded to WP_Query, which Polylang can filter.
* It wouldn't surprise me if they added another black/white list for that. So, my investigation stops here.
*
* @hook the_seo_framework_sitemap_hpt_query_args 10
* @hook the_seo_framework_sitemap_nhpt_query_args 10
* @since 4.1.2
* @since 4.2.0 Now relies on the term_id, instead of mixing term_taxonomy_id and term_id.
* This is unlike Polylang, which relies on term_taxonomy_id somewhat consistently; however,
* in this case we can use term_id since we're specifying the taxonomy directly.
* WordPress 4.4.0 and later also rectifies term_id/term_taxonomy_id stratification, which is
* why we couldn't find an issue whilst introducing this filter.
* @access private
*
* @param array $args The query arguments.
* @return array The augmented query arguments.
*/
function _polylang_sitemap_append_non_translatables( $args ) {
if ( ! Helper\Compatibility::can_i_use( [
'functions' => [
'PLL',
'pll_languages_list',
'pll_default_language',
],
] ) ) return $args;
if ( ! ( \PLL() instanceof \PLL_Frontend ) ) return $args;
$default_lang = \pll_default_language( \OBJECT );
if ( ! isset( $default_lang->slug, $default_lang->term_id ) ) return $args;
if ( ( \PLL()->curlang->slug ?? null ) === $default_lang->slug ) {
$args['lang'] = ''; // Select all lang, so that Polylang doesn't affect the query below with an AND (we need OR).
$args['tax_query'] = [
'relation' => 'OR',
[
'taxonomy' => 'language',
'terms' => \pll_languages_list( [ 'fields' => 'term_id' ] ),
'operator' => 'NOT IN',
],
[
'taxonomy' => 'language',
'terms' => $default_lang->term_id,
'operator' => 'IN',
],
];
}
return $args;
}
/**
* Enables string translation support on titles and descriptions.
*
* @hook the_seo_framework_title_from_custom_field 10
* @hook the_seo_framework_title_from_generation 10
* @hook the_seo_framework_generated_description 10
* @hook the_seo_framework_custom_field_description 10
* @since 3.1.0
* @access private
*
* @param string $string The title or description
* @return string
*/
function pll__( $string ) {
if ( \function_exists( 'PLL' ) && \function_exists( 'pll__' ) )
if ( \PLL() instanceof \PLL_Frontend )
return \pll__( $string );
return $string;
}
/**
* Deletes all sitemap transients, instead of just one.
*
* We didn't implement this in our default APIs because we want to trigger WP hooks.
* Executing database queries directly bypass those. So, we do this afterward.
*
* @hook the_seo_framework_cleared_sitemap_transients 10
* @since 4.0.5
* @since 5.0.0 Removed clearing once-per-request restriction.
* @global \wpdb $wpdb
* @access private
*/
function _polylang_flush_sitemap() {
global $wpdb;
$transient_prefix = Sitemap\Cache::get_transient_prefix();
$wpdb->query( $wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
$wpdb->esc_like( "_transient_$transient_prefix" ) . '%',
) );
// We didn't use a wildcard after "_transient_" to reduce scans.
// A second query is faster on saturated sites.
$wpdb->query( $wpdb->prepare(
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
$wpdb->esc_like( "_transient_timeout_$transient_prefix" ) . '%',
) );
}
/**
* Polylang breaks the admin interface quick-edit and terms-addition functionality.
* This hack seeks to remove their broken code, letting WordPress take over
* correctly once more with full forward and backward compatibility, as we proposed.
*
* Practically, this applies the proposed fix of <https://github.com/polylang/polylang/issues/928#issuecomment-1040062844>.
*
* @hook admin_enqueue_scripts 11
* @see https://github.com/polylang/polylang/issues/928
* @since 5.0.0
*/
function _defunct_badly_coded_polylang_script() {
// Find last ajaxSuccess handler.
// Since this code runs directly after Polylang, it should grab theirs.
$remove_ajax_success = <<<'JS'
jQuery( () => {
const handler = jQuery._data( document, 'events' )?.ajaxSuccess?.pop().handler;
handler && jQuery( document ).off( 'ajaxSuccess', handler );
} );
JS;
// Remove PLL term handler on ajaxSuccess. It is redundant, achieves nothing,
// creates redundant secondary requests, and breaks all plugins but Yoast SEO.
\wp_add_inline_script( 'pll_term', $remove_ajax_success );
// Remove PLL post handler on ajaxSuccess. It is redundant, achieves nothing,
// creates redundant secondary requests, and breaks all plugins but Yoast SEO.
\wp_add_inline_script( 'pll_post', $remove_ajax_success );
}
/**
* Polylang breaks the home URL by not always augmenting the home URL.
* This hack lets Polylang's home_url filter think it's ready to start augmenting URLs, as we proposed,
* instead of engaging far too late.
*
* Practically, this applies the proposed fix of <https://github.com/polylang/polylang/issues/1422#issuecomment-1970620222>.
*
* Polylang also tests for a debug backtrace. They skip the first two callbacks because they are redundant.
* We add another callback in this trace: but it is at position 0. So, the second trace is now tested
* in Polylang's method (it being `apply_filters()`). This is of no functional impact but on the performance,
* since that function is not in the allow/block lists.
*
* @hook the_seo_framework_front_init 10
* @see https://github.com/polylang/polylang/issues/1422
* @see https://github.com/sybrew/the-seo-framework/issues/665
* @since 5.0.5
*/
function _hijack_polylang_home_url() {
if ( ! \function_exists( 'PLL' ) || ! ( \PLL() instanceof \PLL_Frontend ) ) return;
$default_cb = [ \PLL()->filters_links ?? null, 'home_url' ];
// If not false, this will imply method `home_url()` exists and is public.
$priority = $default_cb[0] ? \has_filter( 'home_url', $default_cb ) : false;
if ( false === $priority ) return;
\remove_filter( 'home_url', $default_cb, $priority );
\add_filter(
'home_url',
function ( ...$args ) use ( $default_cb ) {
global $wp_actions;
// Polylang runs as intended at template_redirect or later. Don't trick when pll_language_defined didn't run.
if ( isset( $wp_actions['template_redirect'] ) || ! isset( $wp_actions['pll_language_defined'] ) )
return \call_user_func_array( $default_cb, $args );
// Trick Polylang.
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- it's called a hijack for a reason.
$wp_actions['template_redirect'] = 1;
$url = \call_user_func_array( $default_cb, $args );
// Undo trick.
unset( $wp_actions['template_redirect'] );
return $url;
},
$priority,
4, // forward all the args.
);
}
/**
* Polylang breaks the home URL by not always augmenting the home URL.
* This filter adds TSF as correctly interpreting the home URL, so it can be
* agnostic about the home URL request.
*
* @hook pll_home_url_white_list 10 - I didn't pick this name.
* @hook pll_home_url_allow_list 10 - some day this will probably be instated.
* @since 5.0.5
* @param string[][] $allow_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.
* @return string[]
*/
function _polylang_allow_tsf_home_url( $allow_list ) {
$allow_list[] = [ 'file' => \THE_SEO_FRAMEWORK_DIR_PATH ];
return $allow_list;
}
/**
* Polylang and TSF race to prepend their column keys on terms to 'posts'.
*
* This filter forces TSF to be put before the language selection of Polylang
* by prioritizing their column keys to what TSF will prepend itself to.
*
* @since 5.1.0
*
* @param string[] $order_keys The column keys order.
* @return string[] The column keys order.
*/
function _polylang_seo_column_keys_order( $order_keys ) {
if ( ! \function_exists( 'PLL' ) || ! ( \PLL() instanceof \PLL_Admin ) )
return $order_keys;
$language_keys = array_map(
fn( $language ) => "language_{$language->slug}",
\PLL()->model->get_languages_list(),
);
array_unshift( $order_keys, ...$language_keys );
return $order_keys;
}