PluginProbe
Polylang / 3.0.2
Polylang v3.0.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.0.2, at admin/admin-base.php

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