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

531 lines 17.6 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|null
16 */
17 public $curlang;
18
19 /**
20 * Language selected in the admin language filter.
21 *
22 * @var PLL_Language|null
23 */
24 public $filter_lang;
25
26 /**
27 * Preferred language to assign to new contents.
28 *
29 * @var PLL_Language|null
30 */
31 public $pref_lang;
32
33 /**
34 * @var PLL_Filters_Links|null
35 */
36 public $filters_links;
37
38 /**
39 * @var PLL_Admin_Links|null
40 */
41 public $links;
42
43 /**
44 * @var PLL_Admin_Notices|null
45 */
46 public $notices;
47
48 /**
49 * @var PLL_Admin_Static_Pages|null
50 */
51 public $static_pages;
52
53 /**
54 * @var PLL_Admin_Default_Term|null
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 add_action( 'admin_menu', array( $this, 'remove_customize_submenu' ) );
72
73 // Setup js scripts and css styles
74 add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ), 0 ); // High priority in case an ajax request is sent by an immediately invoked function
75
76 add_action( 'customize_controls_enqueue_scripts', array( $this, 'customize_controls_enqueue_scripts' ) );
77
78 // Early instantiated to be able to correctly initialize language properties.
79 $this->static_pages = new PLL_Admin_Static_Pages( $this );
80 $this->model->set_languages_ready();
81 }
82
83 /**
84 * Setups filters and action needed on all admin pages and on plugins page
85 * Loads the settings pages or the filters base on the request
86 *
87 * @since 1.2
88 */
89 public function init() {
90 parent::init();
91
92 $this->notices = new PLL_Admin_Notices( $this );
93
94 $this->default_term = new PLL_Admin_Default_Term( $this );
95 $this->default_term->add_hooks();
96
97 if ( ! $this->model->has_languages() ) {
98 return;
99 }
100
101 $this->links = new PLL_Admin_Links( $this ); // FIXME needed here ?
102 $this->filters_links = new PLL_Filters_Links( $this ); // FIXME needed here ?
103
104 // Filter admin language for users
105 // We must not call user info before WordPress defines user roles in wp-settings.php
106 add_action( 'setup_theme', array( $this, 'init_user' ) );
107 add_filter( 'request', array( $this, 'request' ) );
108
109 // Adds the languages in admin bar
110 add_action( 'admin_bar_menu', array( $this, 'admin_bar_menu' ), 100 ); // 100 determines the position
111 }
112
113 /**
114 * Adds the link to the languages panel in the WordPress admin menu
115 *
116 * @since 0.1
117 *
118 * @return void
119 */
120 public function add_menus() {
121 global $admin_page_hooks;
122
123 // Prepare the list of tabs
124 $tabs = array( 'lang' => __( 'Languages', 'polylang' ) );
125
126 // Only if at least one language has been created
127 if ( $this->model->has_languages() ) {
128 $tabs['strings'] = __( 'Translations', 'polylang' );
129 }
130
131 $tabs['settings'] = __( 'Settings', 'polylang' );
132
133 /**
134 * Filter the list of tabs in Polylang settings
135 *
136 * @since 1.5.1
137 *
138 * @param array $tabs list of tab names
139 */
140 $tabs = apply_filters( 'pll_settings_tabs', $tabs );
141
142 $parent = '';
143
144 foreach ( $tabs as $tab => $title ) {
145 $page = 'lang' === $tab ? 'mlang' : "mlang_$tab";
146 if ( empty( $parent ) ) {
147 $parent = $page;
148 add_menu_page( $title, __( 'Languages', 'polylang' ), 'manage_options', $page, '__return_null', 'dashicons-translation' );
149 $admin_page_hooks[ $page ] = 'languages'; // Hack to avoid the localization of the hook name. See: https://core.trac.wordpress.org/ticket/18857
150 }
151
152 add_submenu_page( $parent, $title, $title, 'manage_options', $page, array( $this, 'languages_page' ) );
153 }
154 }
155
156 /**
157 * Dummy method to display the 3 tabs pages: languages, strings translations, settings.
158 * Overwritten in `PLL_Settings`.
159 *
160 * @since 3.7
161 *
162 * @return void
163 */
164 public function languages_page() {}
165
166 /**
167 * Setup js scripts & css styles ( only on the relevant pages )
168 *
169 * @since 0.6
170 *
171 * @return void
172 */
173 public function admin_enqueue_scripts() {
174 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
175
176 wp_enqueue_script( 'pll_admin', plugins_url( "/js/build/admin{$suffix}.js", POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
177 $inline_script = sprintf( 'let pll_admin = %s;', wp_json_encode( array( 'ajax_filter' => $this->get_ajax_filter_data() ) ) );
178 wp_add_inline_script( 'pll_admin', $inline_script, 'before' );
179
180 $screen = get_current_screen();
181 if ( empty( $screen ) ) {
182 return;
183 }
184
185 /*
186 * For each script:
187 * 0 => the pages on which to load the script
188 * 1 => the scripts it needs to work
189 * 2 => true if loaded even if languages have not been defined yet, false otherwise
190 * 3 => true if loaded in footer
191 */
192 $scripts = array(
193 'user' => array( array( 'profile', 'user-edit' ), array( 'jquery' ), false, false ),
194 'widgets' => array( array( 'widgets' ), array( 'jquery' ), false, false ),
195 );
196
197 $block_screens = array( 'widgets', 'site-editor' );
198
199 if ( ! empty( $screen->post_type ) && $this->model->is_translated_post_type( $screen->post_type ) ) {
200 $scripts['post'] = array( array( 'edit', 'upload' ), array( 'jquery', 'wp-ajax-response' ), false, true );
201
202 // Classic editor.
203 if ( ! method_exists( $screen, 'is_block_editor' ) || ! $screen->is_block_editor() ) {
204 $scripts['classic-editor'] = array( array( 'post', 'media', 'async-upload' ), array( 'jquery', 'wp-ajax-response', 'post', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
205 }
206
207 // Block editor with legacy metabox in WP 5.0+.
208 $block_screens[] = 'post';
209 }
210
211 if ( $this->is_block_editor( $screen ) ) {
212 $scripts['block-editor'] = array( $block_screens, array( 'jquery', 'wp-ajax-response', 'wp-api-fetch', 'jquery-ui-dialog', 'wp-i18n' ), false, true );
213 }
214
215 if ( ! empty( $screen->taxonomy ) && $this->model->is_translated_taxonomy( $screen->taxonomy ) ) {
216 $scripts['term'] = array( array( 'edit-tags', 'term' ), array( 'jquery', 'wp-ajax-response', 'jquery-ui-autocomplete' ), false, true );
217 }
218
219 foreach ( $scripts as $script => $v ) {
220 if ( in_array( $screen->base, $v[0] ) && ( $v[2] || $this->model->has_languages() ) ) {
221 wp_enqueue_script( "pll_{$script}", plugins_url( "/js/build/{$script}{$suffix}.js", POLYLANG_ROOT_FILE ), $v[1], POLYLANG_VERSION, $v[3] );
222 if ( 'classic-editor' === $script || 'block-editor' === $script ) {
223 wp_set_script_translations( "pll_{$script}", 'polylang' );
224 }
225 }
226 }
227
228 wp_register_style( 'polylang_admin', plugins_url( "/css/build/admin{$suffix}.css", POLYLANG_ROOT_FILE ), array( 'wp-jquery-ui-dialog' ), POLYLANG_VERSION );
229 wp_enqueue_style( 'polylang_dialog', plugins_url( "/css/build/dialog{$suffix}.css", POLYLANG_ROOT_FILE ), array( 'polylang_admin' ), POLYLANG_VERSION );
230
231 $this->add_inline_scripts();
232 }
233
234 /**
235 * Tells whether or not the given screen is block editor kind.
236 * e.g. widget, site or post editor.
237 *
238 * @since 3.3
239 *
240 * @param WP_Screen $screen Screen object.
241 * @return bool True if the screen is a block editor, false otherwise.
242 */
243 protected function is_block_editor( $screen ) {
244 return method_exists( $screen, 'is_block_editor' ) && $screen->is_block_editor() && ! pll_use_block_editor_plugin();
245 }
246
247 /**
248 * Enqueue scripts to the WP Customizer.
249 *
250 * @since 2.4.0
251 *
252 * @return void
253 */
254 public function customize_controls_enqueue_scripts() {
255 if ( $this->model->has_languages() ) {
256 $suffix = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
257 wp_enqueue_script( 'pll_widgets', plugins_url( '/js/build/widgets' . $suffix . '.js', POLYLANG_ROOT_FILE ), array( 'jquery' ), POLYLANG_VERSION, true );
258 $this->add_inline_scripts();
259 }
260 }
261
262 /**
263 * Adds inline scripts to set the default language in JS
264 * and localizes scripts.
265 *
266 * @since 3.3
267 *
268 * @return void
269 */
270 private function add_inline_scripts() {
271 if ( wp_script_is( 'pll_block-editor', 'enqueued' ) ) {
272 $default_lang_script = 'const pllDefaultLanguage = "' . $this->options['default_lang'] . '";';
273 wp_add_inline_script(
274 'pll_block-editor',
275 $default_lang_script,
276 'before'
277 );
278 }
279 if ( wp_script_is( 'pll_widgets', 'enqueued' ) ) {
280 wp_localize_script(
281 'pll_widgets',
282 'pll_widgets',
283 array(
284 'flags' => wp_list_pluck( $this->model->get_languages_list(), 'flag', 'slug' ),
285 )
286 );
287 }
288 }
289
290 /**
291 * Returns the data to use with the AJAX filter.
292 * The final goal is to detect if an ajax request is made on admin or frontend.
293 *
294 * Takes care to various situations:
295 * - When the AJAX request has no `options.data` thanks to ScreenfeedFr.
296 * See: https://wordpress.org/support/topic/ajaxprefilter-may-not-work-as-expected.
297 * - When `options.data` is a JSON string.
298 * See: https://wordpress.org/support/topic/polylang-breaking-third-party-ajax-requests-on-admin-panels.
299 * - When `options.data` is an empty string (GET request with the method 'load').
300 * See: https://wordpress.org/support/topic/invalid-url-during-wordpress-new-dashboard-widget-operation.
301 *
302 * @since 3.7
303 *
304 * @return array
305 */
306 public function get_ajax_filter_data(): array {
307 global $post_ID, $tag_ID;
308
309 $params = array( 'pll_ajax_backend' => 1 );
310 if ( ! empty( $post_ID ) ) {
311 $params = array_merge( $params, array( 'pll_post_id' => (int) $post_ID ) );
312 }
313
314 if ( ! empty( $tag_ID ) ) {
315 $params = array_merge( $params, array( 'pll_term_id' => (int) $tag_ID ) );
316 }
317
318 /**
319 * Filters the list of parameters to add to the admin ajax request.
320 *
321 * @since 3.4.5
322 *
323 * @param array $params List of parameters to add to the admin ajax request.
324 */
325 return (array) apply_filters( 'pll_admin_ajax_params', $params );
326 }
327
328 /**
329 * Sets the admin current language, used to filter the content
330 *
331 * @since 2.0
332 *
333 * @return void
334 */
335 public function set_current_language() {
336 $this->curlang = $this->filter_lang;
337
338 // Edit Post
339 if ( isset( $_REQUEST['pll_post_id'] ) && $lang = $this->model->post->get_language( (int) $_REQUEST['pll_post_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
340 $this->curlang = $lang;
341 } 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
342 $this->curlang = $lang;
343 } 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
344 $this->curlang = empty( $_GET['new_lang'] ) ? $this->pref_lang : $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
345 }
346
347 // Edit Term
348 elseif ( isset( $_REQUEST['pll_term_id'] ) && $lang = $this->model->term->get_language( (int) $_REQUEST['pll_term_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
349 $this->curlang = $lang;
350 } 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
351 if ( isset( $_GET['tag_ID'] ) && $lang = $this->model->term->get_language( (int) $_GET['tag_ID'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
352 $this->curlang = $lang;
353 } elseif ( ! empty( $_GET['new_lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
354 $this->curlang = $this->model->get_language( sanitize_key( $_GET['new_lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
355 } elseif ( empty( $this->curlang ) ) {
356 $this->curlang = $this->pref_lang;
357 }
358 }
359
360 // Ajax
361 if ( wp_doing_ajax() && ! empty( $_REQUEST['lang'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
362 $this->curlang = $this->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
363 }
364
365 /**
366 * Filters the current language used by Polylang in the admin context.
367 *
368 * @since 3.2
369 *
370 * @param PLL_Language|false|null $curlang Instance of the current language.
371 * @param PLL_Admin_Base $polylang Instance of the main Polylang's object.
372 */
373 $this->curlang = apply_filters( 'pll_admin_current_language', $this->curlang, $this );
374
375 // Inform that the admin language has been set.
376 if ( $this->curlang instanceof PLL_Language ) {
377 /** This action is documented in frontend/choose-lang.php */
378 do_action( 'pll_language_defined', $this->curlang->slug, $this->curlang );
379 } else {
380 /** This action is documented in include/class-polylang.php */
381 do_action( 'pll_no_language_defined' ); // To load overridden textdomains.
382 }
383 }
384
385 /**
386 * Defines the backend language and the admin language filter based on user preferences
387 *
388 * @since 1.2.3
389 *
390 * @return void
391 */
392 public function init_user() {
393 // Language for admin language filter: may be empty
394 // $_GET['lang'] is numeric when editing a language, not when selecting a new language in the filter
395 // We intentionally don't use a nonce to update the language filter
396 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
397 update_user_meta( $user_id, 'pll_filter_content', ( $lang = $this->model->get_language( sanitize_key( $_GET['lang'] ) ) ) ? $lang->slug : '' ); // phpcs:ignore WordPress.Security.NonceVerification
398 }
399
400 $this->filter_lang = $this->model->get_language( get_user_meta( get_current_user_id(), 'pll_filter_content', true ) );
401
402 // Set preferred language for use when saving posts and terms: must not be empty
403 $this->pref_lang = empty( $this->filter_lang ) ? $this->model->get_default_language() : $this->filter_lang;
404
405 /**
406 * Filters the preferred language on admin side.
407 * The preferred language is used for example to determine the language of a new post.
408 *
409 * @since 1.2.3
410 *
411 * @param PLL_Language $pref_lang Preferred language.
412 */
413 $this->pref_lang = apply_filters( 'pll_admin_preferred_language', $this->pref_lang );
414
415 $this->set_current_language();
416 }
417
418 /**
419 * Avoids parsing a tax query when all languages are requested
420 * Fixes https://wordpress.org/support/topic/notice-undefined-offset-0-in-wp-includesqueryphp-on-line-3877 introduced in WP 4.1
421 *
422 * @see https://core.trac.wordpress.org/ticket/31246 the suggestion of @boonebgorges.
423 *
424 * @since 1.6.5
425 *
426 * @param array $qvars The array of requested query variables.
427 * @return array
428 */
429 public function request( $qvars ) {
430 if ( isset( $qvars['lang'] ) && 'all' === $qvars['lang'] ) {
431 unset( $qvars['lang'] );
432 }
433
434 return $qvars;
435 }
436
437 /**
438 * Adds the languages list in admin bar for the admin languages filter.
439 *
440 * @since 0.9
441 *
442 * @param WP_Admin_Bar $wp_admin_bar WP_Admin_Bar global object.
443 * @return void
444 */
445 public function admin_bar_menu( $wp_admin_bar ) {
446 $all_item = (object) array(
447 'slug' => 'all',
448 'name' => __( 'Show all languages', 'polylang' ),
449 'flag' => '<span class="ab-icon"></span>',
450 );
451
452 $selected = empty( $this->filter_lang ) ? $all_item : $this->filter_lang;
453
454 $title = sprintf(
455 '<span class="ab-label"%1$s><span class="screen-reader-text">%2$s</span>%3$s</span>',
456 $selected instanceof PLL_Language ? sprintf( ' lang="%s"', esc_attr( $selected->get_locale( 'display' ) ) ) : '',
457 __( 'Filters content by language', 'polylang' ),
458 esc_html( $selected->name )
459 );
460
461 /**
462 * Filters the admin languages filter submenu items
463 *
464 * @since 2.6
465 *
466 * @param array $items The admin languages filter submenu items.
467 */
468 $items = apply_filters( 'pll_admin_languages_filter', array_merge( array( $all_item ), $this->model->get_languages_list() ) );
469
470 $menu = array(
471 'id' => 'languages',
472 'title' => $selected->flag . $title,
473 'href' => esc_url( add_query_arg( 'lang', $selected->slug, remove_query_arg( 'paged' ) ) ),
474 'meta' => array(
475 'title' => __( 'Filters content by language', 'polylang' ),
476 ),
477 );
478
479 if ( 'all' !== $selected->slug ) {
480 $menu['meta']['class'] = 'pll-filtered-languages';
481 }
482
483 if ( ! empty( $items ) ) {
484 $wp_admin_bar->add_menu( $menu );
485 }
486
487 foreach ( $items as $lang ) {
488 if ( $selected->slug === $lang->slug ) {
489 continue;
490 }
491
492 $wp_admin_bar->add_menu(
493 array(
494 'parent' => 'languages',
495 'id' => $lang->slug,
496 'title' => $lang->flag . esc_html( $lang->name ),
497 'href' => esc_url( add_query_arg( 'lang', $lang->slug, remove_query_arg( 'paged' ) ) ),
498 'meta' => 'all' === $lang->slug ? array() : array( 'lang' => esc_attr( $lang->get_locale( 'display' ) ) ),
499 )
500 );
501 }
502 }
503
504 /**
505 * Remove the customize submenu when using a block theme.
506 *
507 * WordPress removes the Customizer menu if a block theme is activated and no other plugins interact with it.
508 * As Polylang interacts with the Customizer, we have to delete this menu ourselves in the case of a block theme,
509 * unless another plugin than Polylang interacts with the Customizer.
510 *
511 * @since 3.2
512 *
513 * @return void
514 */
515 public function remove_customize_submenu() {
516 if ( ! $this->should_customize_menu_be_removed() ) {
517 return;
518 }
519
520 global $submenu;
521
522 if ( ! empty( $submenu['themes.php'] ) ) {
523 foreach ( $submenu['themes.php'] as $submenu_item ) {
524 if ( 'customize' === $submenu_item[1] ) {
525 remove_submenu_page( 'themes.php', $submenu_item[2] );
526 }
527 }
528 }
529 }
530 }
531