PluginProbe
Polylang / 3.1.2
Polylang v3.1.2
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / admin / admin-base.php

admin-base.php in Polylang 3.1.2, at admin/admin-base.php

501 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Polylang
4 */
5
6 /**
7 * Setup features available on all admin pages.
8 *
9 * @since 1.8
10 */
11 abstract class PLL_Admin_Base extends PLL_Base {
12 /**
13 * Current language (used to filter the content).
14 *
15 * @var PLL_Language
16 */
17 public $curlang;
18
19 /**
20 * Language selected in the admin language filter.
21 *
22 * @var PLL_Language
23 */
24 public $filter_lang;
25
26 /**
27 * Preferred language to assign to new contents.
28 *
29 * @var PLL_Language
30 */
31 public $pref_lang;
32
33 /**
34 * @var PLL_Filters_Links
35 */
36 public $filters_links;
37
38 /**
39 * @var PLL_Admin_Links
40 */
41 public $links;
42
43 /**
44 * @var PLL_Admin_Notices
45 */
46 public $notices;
47
48 /**
49 * @var PLL_Admin_Static_Pages
50 */
51 public $static_pages;
52
53 /**
54 * @var PLL_Admin_Default_Term
55 */
56 public $default_term;
57
58 /**
59 * Setups actions needed on all admin pages.
60 *
61 * @since 1.8
62 *
63 * @param PLL_Links_Model $links_model Reference to the links model.
64 */
65 public function __construct( &$links_model ) {
66 parent::__construct( $links_model );
67
68 // Adds the link to the languages panel in the WordPress admin menu
69 add_action( 'admin_menu', array( $this, 'add_menus' ) );
70
71 // Setup js scripts and css styles
72 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
73 add_action( 'admin_print_footer_scripts', array( $this, 'admin_print_footer_scripts' ), 0 ); // High priority in case an ajax request is sent by an immediately invoked function
74
75 add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
76 }
77
78 /**
79 * Setups filters and action needed on all admin pages and on plugins page
80 * Loads the settings pages or the filters base on the request
81 *
82 * @since 1.2
83 */
84 public function init() {
85 parent::init();
86
87 $this->notices = new PLL_Admin_Notices( $this );
88
89 if ( ! $this->model->get_languages_list() ) {
90 return;
91 }
92
93 $this->links = new PLL_Admin_Links( $this ); // FIXME needed here ?
94 $this->static_pages = new PLL_Admin_Static_Pages( $this ); // FIXME needed here ?
95 $this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ?
96 $this->default_term = new PLL_Admin_Default_Term( $this );
97 $this->default_term->add_hooks();
98
99 // Filter admin language for users
100 // We must not call user info before WordPress defines user roles in wp-settings.php
101 add_action( 'setup_theme', array( $this, 'init_user' ) );
102 add_filter( 'request', array( $this, 'request' ) );
103
104 // Adds the languages in admin bar
105 add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 ); // 100 determines the position
106 }
107
108 /**
109 * Adds the link to the languages panel in the WordPress admin menu
110 *
111 * @since 0.1
112 *
113 * @return void
114 */
115 public function add_menus() {
116 global $admin_page_hooks;
117
118 // Prepare the list of tabs
119 $tabs = array( 'lang' => __( 'Languages', 'polylang' ) );
120
121 // Only if at least one language has been created
122 if ( $this->model->get_languages_list() ) {
123 $tabs['strings'] = __( 'Strings translations', 'polylang' );
124 }
125
126 $tabs['settings'] = __( 'Settings', 'polylang' );
127
128 /**
129 * Filter the list of tabs in Polylang settings
130 *
131 * @since 1.5.1
132 *
133 * @param array $tabs list of tab names
134 */
135 $tabs = apply_filters( 'pll_settings_tabs', $tabs );
136
137 $parent = '';
138
139 foreach ( $tabs as $tab => $title ) {
140 $page = 'lang' === $tab ? 'mlang' : "mlang_$tab";
141 if ( empty( $parent ) ) {
142 $parent = $page;
143 add_menu_page( $title, __( 'Languages', 'polylang' ), 'manage_options', $page, '__return_null', 'dashicons-translation' );
144 $admin_page_hooks[ $page ] = 'languages'; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
145 }
146
147 add_submenu_page( $parent, $title, $title, 'manage_options', $page, array( $this, 'languages_page' ) );
148 }
149 }
150
151 /**
152 * Setup js scripts & css styles ( only on the relevant pages )
153 *
154 * @since 0.6
155 *
156 * @return void
157 */
158 public function admin_enqueue_scripts() {
159 $screen = get_current_screen();
160 if ( empty( $screen ) ) {
161 return;
162 }
163
164 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
165
166 /*
167 * For each script:
168 * 0 => the pages on which to load the script
169 * 1 => the scripts it needs to work
170 * 2 => 1 if loaded even if languages have not been defined yet, 0 otherwise
171 * 3 => 1 if loaded in footer
172 */
173 $scripts = array(
174 'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), 0, 0 ),
175 'widgets' => array( array( 'widgets' ), array( 'jquery' ), 0, 0 ),
176 );
177
178 if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
179 $scripts['post'] = array( array( 'edit', 'upload' ), array( 'jquery', 'wp-ajax-response' ), 0, 1 );
180
181 // Classic editor.
182 if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
183 $scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
184 }
185
186 // Block editor with legacy metabox in WP 5.0+.
187 if ( method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin() ) {
188 $scripts['block-editor'] = array( array( 'post' ), array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), 0, 1 );
189 }
190 }
191
192 if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
193 $scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), 0, 1 );
194 }
195
196 foreach ( $scripts as $script => $v ) {
197 if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->get_languages_list() ) ) {
198 wp_enqueue_script( 'pll_' . $script, plugins_url( '/js/build/' . $script . $suffix . '.js', POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
199 if ( 'classic-editor' === $script || 'block-editor' === $script ) {
200 wp_set_script_translations( 'pll_' . $script, 'polylang' );
201 }
202 }
203 }
204
205 wp_register_style( 'polylang_admin', plugins_url( '/css/build/admin' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
206 wp_enqueue_style( 'polylang_dialog', plugins_url( '/css/build/dialog' . $suffix . '.css', POLYLANG_ROOT_FILE ), array( 'polylang_admin' ), POLYLANG_VERSION );
207
208 $this->localize_scripts();
209 }
210
211 /**
212 * Enqueue scripts to the WP Customizer.
213 *
214 * @since 2.4.0
215 *
216 * @return void
217 */
218 public function customize_controls_enqueue_scripts() {
219 if ( $this->model->get_languages_list() ) {
220 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
221 wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
222 $this->localize_scripts();
223 }
224 }
225
226 /**
227 * Localize scripts.
228 *
229 * @since 2.4.0
230 *
231 * @return void
232 */
233 public function localize_scripts() {
234 if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
235 wp_localize_script(
236 'pll_widgets',
237 'pll_widgets',
238 array(
239 'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
240 )
241 );
242 }
243 }
244
245 /**
246 * Sets pll_ajax_backend on all backend ajax request
247 * The final goal is to detect if an ajax request is made on admin or frontend
248 *
249 * Takes care to various situations:
250 * when the ajax request has no options.data thanks to ScreenfeedFr
251 * see: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected
252 * when options.data is a json string
253 * see: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels
254 * when options.data is an empty string (GET request with the method 'load')
255 * see: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation
256 *
257 * @since 1.4
258 *
259 * @return void
260 */
261 public function admin_print_footer_scripts() {
262 global $post_ID, $tag_ID;
263
264 $params = array( 'pll_ajax_backend' => 1 );
265 if ( ! empty( $post_ID ) ) {
266 $params = array_merge( $params, array( 'pll_post_id' => (int) $post_ID ) );
267 }
268
269 if ( ! empty( $tag_ID ) ) {
270 $params = array_merge( $params, array( 'pll_term_id' => (int) $tag_ID ) );
271 }
272
273 $str = http_build_query( $params );
274 $arr = wp_json_encode( $params );
275 ?>
276 <script type="text/javascript">
277 if (typeof jQuery != 'undefined') {
278 jQuery(
279 function( $ ){
280 $.ajaxPrefilter( function ( options, originalOptions, jqXHR ) {
281 if ( -1 != options.url.indexOf( ajaxurl ) || -1 != ajaxurl.indexOf( options.url ) ) {
282
283 function addPolylangParametersAsString() {
284 if ( 'undefined' === typeof options.data || '' === options.data.trim() ) {
285 // Only Polylang data need to be send. So it could be as a simple query string.
286 options.data = '<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
287 } else {
288 /*
289 * In some cases data could be a JSON string like in third party plugins.
290 * So we need not to break their process by adding polylang parameters as valid JSON datas.
291 */
292 try {
293 options.data = JSON.stringify( Object.assign( JSON.parse( options.data ), <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> ) );
294 } catch( exception ) {
295 // Add Polylang data to the existing query string.
296 options.data = options.data + '&<?php echo $str; // phpcs:ignore WordPress.Security.EscapeOutput ?>';
297 }
298 }
299 }
300
301 /*
302 * options.processData set to true is the default jQuery process where the data is converted in a query string by using jQuery.param().
303 * This step is done before applying filters. Thus here the options.data is already a string in this case.
304 * @See https://github.com/jquery/jquery/blob/3.5.1/src/ajax.js#L563-L569 jQuery ajax function.
305 * It is the most case WordPress send ajax request this way however third party plugins or themes could be send JSON string.
306 * Use JSON format is recommended in jQuery.param() documentation to be able to send complex data structures.
307 * @See https://api.jquery.com/jquery.param/ jQuery param function.
308 */
309 if ( options.processData ) {
310 addPolylangParametersAsString();
311 } else {
312 /*
313 * If options.processData is set to false data could be undefined or pass as a string.
314 * So data as to be processed as if options.processData is set to true.
315 */
316 if ( 'undefined' === typeof options.data || 'string' === typeof options.data ) {
317 addPolylangParametersAsString();
318 } else {
319 // Otherwise options.data is probably an object.
320 options.data = Object.assign( options.data || {} , <?php echo $arr; // phpcs:ignore WordPress.Security.EscapeOutput ?> );
321 }
322 }
323 }
324 });
325 }
326 );
327 }
328 </script>
329 <?php
330 }
331
332 /**
333 * Sets the admin current language, used to filter the content
334 *
335 * @since 2.0
336 *
337 * @return void
338 */
339 public function set_current_language() {
340 $this->curlang = $this->filter_lang;
341
342 // Edit Post
343 if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
344 $this->curlang = $lang;
345 } elseif ( 'post.php' === $GLOBALS['pagenow'] && isset( $_GET['post'] ) && $this->model->is_translated_post_type( get_post_type( (int) $_GET['post'] ) ) && $lang = $this->model->post->get_language( (int) $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
346 $this->curlang = $lang;
347 } elseif ( 'post-new.php' === $GLOBALS['pagenow'] && ( empty( $_GET['post_type'] ) || $this->model->is_translated_post_type( sanitize_key( $_GET['post_type'] ) ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
348 $this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
349 }
350
351 // Edit Term
352 elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
353 $this->curlang = $lang;
354 } elseif ( in_array( $GLOBALS['pagenow'], array( 'edit-tags.php', 'term.php' ) ) && isset( $_GET['taxonomy'] ) && $this->model->is_translated_taxonomy( sanitize_key( $_GET['taxonomy'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification
355 if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
356 $this->curlang = $lang;
357 } elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
358 $this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
359 } elseif ( empty( $this->curlang ) ) {
360 $this->curlang = $this->pref_lang;
361 }
362 }
363
364 // Ajax
365 if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
366 $this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
367 }
368
369 // Inform that the admin language has been set.
370 if ( $this->curlang ) {
371 /** This action is documented in frontend/choose-lang.php */
372 do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
373 } else {
374 /** This action is documented in include/class-polylang.php */
375 do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
376 }
377 }
378
379 /**
380 * Defines the backend language and the admin language filter based on user preferences
381 *
382 * @since 1.2.3
383 *
384 * @return void
385 */
386 public function init_user() {
387 // Language for admin language filter: may be empty
388 // $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter
389 // We intentionally don't use a nonce to update the language filter
390 if ( ! wp_doing_ajax() && ! empty( $_GET['lang'] ) && ! is_numeric( sanitize_key( $_GET['lang'] ) ) && current_user_can( 'edit_user', $user_id = get_current_user_id() ) ) { // phpcs:ignore WordPress.Security.NonceVerification
391 update_user_meta( $user_id, 'pll_filter_content', ( $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ) ) ? $lang->slug : '' ); // phpcs:ignore WordPress.Security.NonceVerification
392 }
393
394 $this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
395
396 // Set preferred language for use when saving posts and terms: must not be empty
397 $this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_language( $this->options['default_lang'] ) : $this->filter_lang;
398
399 /**
400 * Filters the preferred language on admin side.
401 * The preferred language is used for example to determine the language of a new post.
402 *
403 * @since 1.2.3
404 *
405 * @param PLL_Language $pref_lang Preferred language.
406 */
407 $this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
408
409 $this->set_current_language();
410
411 // Plugin i18n, only needed for backend.
412 load_plugin_textdomain( 'polylang' );
413 }
414
415 /**
416 * Avoids parsing a tax query when all languages are requested
417 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
418 *
419 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
420 *
421 * @since 1.6.5
422 *
423 * @param array $qvars
424 * @return array
425 */
426 public function request( $qvars ) {
427 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
428 unset( $qvars['lang'] );
429 }
430
431 return $qvars;
432 }
433
434 /**
435 * Adds the languages list in admin bar for the admin languages filter.
436 *
437 * @since 0.9
438 *
439 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
440 * @return void
441 */
442 public function admin_bar_menu( $wp_admin_bar ) {
443 $all_item = (object) array(
444 'slug' => 'all',
445 'name' => __( 'Show all languages', 'polylang' ),
446 'flag' => '<span class="ab-icon"></span>',
447 );
448
449 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
450
451 $title = sprintf(
452 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
453 'all' === $selected->slug ? '' : sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ),
454 __( 'Filters content by language', 'polylang' ),
455 esc_html( $selected->name )
456 );
457
458 /**
459 * Filters the admin languages filter submenu items
460 *
461 * @since 2.6
462 *
463 * @param array $items The admin languages filter submenu items.
464 */
465 $items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
466
467 $menu = array(
468 'id' => 'languages',
469 'title' => $selected->flag . $title,
470 'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
471 'meta' => array(
472 'title' => __( 'Filters content by language', 'polylang' ),
473 ),
474 );
475
476 if ( 'all' !== $selected->slug ) {
477 $menu['meta']['class'] = 'pll-filtered-languages';
478 }
479
480 if ( ! empty( $items ) ) {
481 $wp_admin_bar->add_menu( $menu );
482 }
483
484 foreach ( $items as $lang ) {
485 if ( $selected->slug === $lang->slug ) {
486 continue;
487 }
488
489 $wp_admin_bar->add_menu(
490 array(
491 'parent' => 'languages',
492 'id' => $lang->slug,
493 'title' => $lang->flag . esc_html( $lang->name ),
494 'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
495 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
496 )
497 );
498 }
499 }
500 }
501