PluginProbe
WP EXtra – One Click Optimize / trunk
WP EXtra – One Click Optimize vtrunk
8.7.1 8.6.8 8.7.0 trunk 5.9 8.0 8.5.0 8.5.4 8.5.5 8.6.0 8.6.1 8.6.2 8.6.3 8.6.5
wp-extra / src / Modules / Common / Permission.php

Permission.php in WP EXtra – One Click Optimize trunk, at src/Modules/Common/Permission.php

961 lines 38.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPEXtra\Modules\Common;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 use WPEXtra\Settings;
9 use WPEXtra\Helper;
10 use WPEXtra\Base;
11
12 class Permission extends Base {
13
14 public function __construct() {
15 parent::__construct();
16 add_action('admin_bar_menu', [$this, 'admin_bar_extra'], 150);
17 }
18
19 public function admin_bar_extra($meta = true) {
20 global $wp_admin_bar;
21 if (!is_user_logged_in() || !is_super_admin() || !is_admin_bar_showing()) {
22 return;
23 }
24 $wp_admin_bar->add_menu([
25 'id' => 'wp-extra',
26 'title' => __('WP EXtra', 'wp-extra'),
27 'href' => admin_url('admin.php?page=wp-extra'),
28 ]);
29 }
30
31 protected $features = [
32 'wp_toolbar',
33 'admin_site_link',
34 'wp_adminbar',
35 'wp_adminbar_auto',
36 'adminmenu_list',
37 'menu_chunk_saver',
38 'adminplugin_list',
39 'disable_widgets',
40 'scrolltotop',
41 'registration_date',
42 'last_login',
43 'application_passwords',
44 'profile_pw',
45 'profile_email',
46 'no_backend',
47 'adminmenu_extra',
48 ];
49
50 public function scrolltotop() {
51 if (is_admin()) {
52 add_action('admin_footer', [$this, 'render_scrolltotop']);
53 }
54 }
55
56 public function render_scrolltotop() {
57 $screen = get_current_screen();
58 if ($screen && !empty($screen->is_block_editor)) {
59 return;
60 }
61 ?>
62 <style>
63 #wpex-backtotop {
64 position: fixed;
65 right: 20px;
66 bottom: 20px;
67 z-index: 99999;
68 width: 40px;
69 height: 40px;
70 display: none;
71 align-items: center;
72 justify-content: center;
73 background: var(--wp-admin-theme-color, #2271b1);
74 color: #ffffff;
75 border-radius: 4px;
76 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
77 text-decoration: none;
78 cursor: pointer;
79 transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease;
80 outline: none;
81 border: none;
82 padding: 0;
83 margin: 0;
84 box-sizing: border-box;
85 }
86 .rtl #wpex-backtotop {
87 right: auto;
88 left: 20px;
89 }
90 #wpex-backtotop:hover {
91 background: var(--wp-admin-theme-color-darker-10, #135e96);
92 color: #ffffff;
93 transform: translateY(-2px);
94 box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
95 }
96 #wpex-backtotop:active {
97 transform: translateY(0);
98 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
99 }
100 #wpex-backtotop svg {
101 width: 20px;
102 height: 20px;
103 display: block;
104 fill: currentColor;
105 }
106 </style>
107 <a id="wpex-backtotop" href="#" title="<?php esc_attr_e('Scroll to top', 'wp-extra'); ?>" aria-label="<?php esc_attr_e('Scroll to top', 'wp-extra'); ?>">
108 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
109 <path d="M12 4.5l-7 7 1.4 1.4 4.6-4.6V19.5h2V8.3l4.6 4.6 1.4-1.4-7-7z"/>
110 </svg>
111 </a>
112 <script>
113 jQuery(function($) {
114 var $btn = $('#wpex-backtotop');
115 $(window).on('scroll', function() {
116 if ($(this).scrollTop() > 180) {
117 if (!$btn.is(':visible')) {
118 $btn.css('display', 'flex').hide().fadeIn(200);
119 }
120 } else {
121 if ($btn.is(':visible')) {
122 $btn.fadeOut(200);
123 }
124 }
125 });
126 $btn.on('click', function(e) {
127 e.preventDefault();
128 $('html, body').animate({ scrollTop: 0 }, 250);
129 });
130 });
131 </script>
132 <?php
133 }
134
135 public function registration_date() {
136 add_filter('manage_users_columns', [$this, 'registration_date_columns']);
137 add_filter('manage_users_custom_column', [$this, 'registration_date_custom_column'], 10, 3);
138 add_filter('manage_users_sortable_columns', [$this, 'registration_date_sortable_columns']);
139 }
140
141 public function registration_date_columns($columns) {
142 $columns['registration_date'] = __('User Registration Date', 'wp-extra');
143 return $columns;
144 }
145
146 public function registration_date_custom_column($row_output, $column_id_attr, $user_id) {
147 if ($column_id_attr === 'registration_date') {
148 $registered = get_the_author_meta('registered', $user_id);
149 return $registered ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($registered)) : '';
150 }
151 return $row_output;
152 }
153
154 public function registration_date_sortable_columns($columns) {
155 return wp_parse_args(['registration_date' => 'registered'], $columns);
156 }
157
158 public function last_login() {
159 add_filter('manage_users_columns', [$this, 'last_login_columns']);
160 add_filter('manage_users_custom_column', [$this, 'last_login_custom_column'], 10, 3);
161 add_filter('manage_users_sortable_columns', [$this, 'last_login_sortable_columns']);
162 add_action('wp_login', [$this, 'update_last_login'], 10, 2);
163 }
164
165 public function last_login_columns($columns) {
166 $columns['last_login'] = __('Last Login', 'wp-extra');
167 return $columns;
168 }
169
170 public function last_login_custom_column($row_output, $column_id_attr, $user_id) {
171 if ($column_id_attr === 'last_login') {
172 $last_login = get_user_meta($user_id, 'last_login', true);
173 if (!empty($last_login)) {
174 return sprintf(__('%s ago', 'wp-extra'), human_time_diff(strtotime($last_login), current_time('timestamp')));
175 }
176 return __('Never', 'wp-extra');
177 }
178 return $row_output;
179 }
180
181 public function update_last_login($login, $user = null) {
182 if (!$user) {
183 $user = get_user_by('login', $login);
184 }
185 if ($user) {
186 update_user_meta($user->ID, 'last_login', current_time('mysql'));
187 }
188 }
189
190 public function last_login_sortable_columns($columns) {
191 return wp_parse_args(['last_login' => 'lastlogin'], $columns);
192 }
193
194 public function wp_toolbar() {
195 add_action('admin_bar_menu', [$this, 'remove_nodes'], 999);
196 }
197
198 public function admin_site_link() {
199 add_action('admin_bar_menu', [$this, 'custom_site_name_link'], 99);
200 }
201
202 public function custom_site_name_link($wp_admin_bar) {
203 if (Helper::get_option('admin_site_link')) {
204 $node = $wp_admin_bar->get_node('site-name');
205 if ($node) {
206 $node->href = admin_url();
207 $wp_admin_bar->add_node((array) $node);
208 }
209 }
210 }
211
212 public function remove_nodes($wp_admin_bar) {
213 $toolbar_nodes = Helper::get_option('wp_toolbar');
214 if (is_array($toolbar_nodes)) {
215 foreach ($toolbar_nodes as $menu_id) {
216 $wp_admin_bar->remove_node($menu_id);
217 }
218 }
219 }
220
221 public function wp_adminbar() {
222 add_filter('show_admin_bar', [$this, 'filter_show_admin_bar'], 99);
223 }
224
225 public function filter_show_admin_bar($show) {
226 if (is_admin()) {
227 return $show;
228 }
229 return false;
230 }
231
232 public function wp_adminbar_auto() {
233 add_action('admin_enqueue_scripts', [$this, 'adminbar_auto_css']);
234 add_action('wp_enqueue_scripts', [$this, 'adminbar_auto_css']);
235 }
236
237 public function adminbar_auto_css() {
238 if (!is_admin_bar_showing()) {
239 return;
240 }
241 $css = '@media (min-width: 783px) {
242 html.wp-toolbar { padding-top: 0 !important; }
243 html { margin-top: 0 !important; }
244 body.admin-bar { margin-top: 0 !important; }
245 #wpadminbar {
246 top: 0 !important;
247 transform: translateY(-100%);
248 opacity: 0;
249 pointer-events: none;
250 transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1);
251 z-index: 999999 !important;
252 }
253 #wpadminbar::after {
254 content: "";
255 position: absolute;
256 top: 100%;
257 left: 0;
258 right: 0;
259 height: 12px;
260 pointer-events: auto;
261 }
262 #wpadminbar:hover,
263 #wpadminbar:focus-within {
264 transform: translateY(0);
265 opacity: 1;
266 pointer-events: auto;
267 }
268 }';
269 wp_add_inline_style('admin-bar', Helper::minifyCSS($css));
270 }
271
272 public function adminmenu_list() {
273 add_action('admin_menu', [$this, 'admin_menu_roles'], 9999);
274 }
275
276 public function admin_menu_roles() {
277 if (!is_admin()) {
278 return;
279 }
280 $admin_menus = (array) Helper::get_option('adminmenu_list', []);
281 if (isset($_GET['page']) && $_GET['page'] === 'wp-extra') {
282 return;
283 }
284 foreach ($admin_menus as $menu_page) {
285 remove_menu_page($menu_page);
286 }
287 }
288
289 public function adminplugin_list() {
290 add_filter('all_plugins', [$this, 'filter_hidden_plugins']);
291 }
292
293 public function filter_hidden_plugins($plugins) {
294 $hide_plugins = (array) Helper::get_option('adminplugin_list', []);
295 if (!empty($hide_plugins) && is_array($plugins)) {
296 foreach ($hide_plugins as $plugin_file) {
297 if (isset($plugins[$plugin_file])) {
298 unset($plugins[$plugin_file]);
299 }
300 }
301 }
302 return $plugins;
303 }
304
305 public function application_passwords() {
306 add_filter('wp_is_application_passwords_available', '__return_false');
307 }
308
309 public function profile_pw() {
310 add_filter('show_password_fields', [$this, 'filter_show_password_fields']);
311 }
312
313 public function filter_show_password_fields($show_password_fields) {
314 if (!current_user_can('administrator')) {
315 return false;
316 }
317 return $show_password_fields;
318 }
319
320 public function profile_email() {
321 add_action('personal_options_update', [$this, 'disableEmailChangeBack']);
322 add_action('edit_user_profile_update', [$this, 'disableEmailChangeBack']);
323 add_action('show_user_profile', [$this, 'disableEmailChangeBackFront']);
324 add_action('edit_user_profile', [$this, 'disableEmailChangeBackFront']);
325 }
326
327 public function disableEmailChangeBack($user_id) {
328 if (!current_user_can('administrator')) {
329 $user = get_user_by('ID', $user_id);
330 if ($user) {
331 $_POST['email'] = $user->user_email;
332 }
333 }
334 }
335
336 public function disableEmailChangeBackFront($user) {
337 if (!current_user_can('administrator')) {
338 ?>
339 <script>
340 document.addEventListener('DOMContentLoaded', function(){
341 var emailInput = document.getElementById('email');
342 if (emailInput) {
343 emailInput.setAttribute('disabled', 'disabled');
344 }
345 });
346 </script>
347 <?php
348 }
349 }
350
351 public function no_backend() {
352 add_action('admin_init', [$this, 'redirect_non_admin_user']);
353 }
354
355 public function redirect_non_admin_user() {
356 if (wp_doing_ajax() || wp_doing_cron() || (defined('REST_REQUEST') && REST_REQUEST)) {
357 return;
358 }
359
360 if (current_user_can('administrator')) {
361 return;
362 }
363
364 $user = wp_get_current_user();
365 $blocked_roles = (array) Helper::get_option('no_backend', []);
366
367 if (!empty($blocked_roles) && !empty($user->roles) && array_intersect($blocked_roles, (array)$user->roles)) {
368 wp_safe_redirect(home_url('/'));
369 exit;
370 }
371 }
372
373 public function adminmenu_extra() {
374 add_action('admin_menu', [$this, 'restrict_settings_access'], 999);
375 add_action('admin_init', [$this, 'block_settings_page_access']);
376 }
377
378 public function restrict_settings_access() {
379 $super_admins = (array) Helper::get_option('adminmenu_extra', []);
380 if (!empty($super_admins)) {
381 $current_user_id = (string) get_current_user_id();
382 if (!in_array($current_user_id, array_map('strval', $super_admins), true)) {
383 remove_menu_page('wp-extra');
384 remove_submenu_page('wp-extra', 'wp-extra');
385 remove_submenu_page('wp-extra', 'wpex-email-logs');
386 }
387 }
388 }
389
390 public function block_settings_page_access() {
391 $super_admins = (array) Helper::get_option('adminmenu_extra', []);
392 if (!empty($super_admins) && isset($_GET['page']) && in_array($_GET['page'], ['wp-extra', 'wpex-email-logs'], true)) {
393 $current_user_id = (string) get_current_user_id();
394 if (!in_array($current_user_id, array_map('strval', $super_admins), true)) {
395 wp_die(esc_html__('You do not have permission to access WP EXtra settings.', 'wp-extra'), 403);
396 }
397 }
398 }
399
400 public function menu_chunk_saver() {
401 add_action('admin_enqueue_scripts', [$this, 'menu_chunk_saver_scripts']);
402 add_filter('walker_nav_menu_start_el', [$this, 'handle_non_link_menu_items'], 10, 4);
403 add_action('wp_ajax_wpex_save_menu_chunk', [$this, 'ajax_save_menu_chunk']);
404 add_action('wp_ajax_kn_save_menu_chunk', [$this, 'ajax_save_menu_chunk']);
405 }
406
407 public function menu_chunk_saver_scripts($hook) {
408 if ('nav-menus.php' !== $hook) {
409 return;
410 }
411 add_action('admin_footer', [$this, 'render_menu_chunk_saver_script'], 99);
412 }
413
414 public function handle_non_link_menu_items($item_output, $item, $depth, $args) {
415 if (empty($item->url) || '#' === trim($item->url)) {
416 $item_output = str_replace('href="#"', 'href="javascript:void(0)" class="wpex-menu-no-link"', $item_output);
417 }
418 return $item_output;
419 }
420
421 public function ajax_save_menu_chunk() {
422 $nonce = $_POST['security'] ?? '';
423 if (!wp_verify_nonce($nonce, 'wpex_menu_chunk_nonce') && !wp_verify_nonce($nonce, 'kn_menu_chunk_nonce')) {
424 wp_send_json_error(['message' => __('Security check failed.', 'wp-extra')]);
425 }
426
427 if (!current_user_can('edit_theme_options')) {
428 wp_send_json_error(['message' => __('You do not have permission to edit menus.', 'wp-extra')]);
429 }
430
431 $menu_id = isset($_POST['menu_id']) ? intval($_POST['menu_id']) : 0;
432 $chunk_index = isset($_POST['chunk_index']) ? intval($_POST['chunk_index']) : 0;
433 $total_chunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 1;
434 $is_first_chunk = !empty($_POST['is_first_chunk']);
435 $is_last_chunk = !empty($_POST['is_last_chunk']);
436
437 if (!$menu_id || !is_nav_menu($menu_id)) {
438 wp_send_json_error(['message' => __('Invalid menu.', 'wp-extra')]);
439 }
440
441 if ($is_first_chunk) {
442 if (!empty($_POST['menu_name'])) {
443 $menu_name = sanitize_text_field(wp_unslash($_POST['menu_name']));
444 wp_update_nav_menu_object($menu_id, ['menu-name' => $menu_name]);
445 }
446
447 if (!empty($_POST['deleted_items']) && is_array($_POST['deleted_items'])) {
448 foreach ($_POST['deleted_items'] as $del_id) {
449 $del_id = intval($del_id);
450 if ($del_id > 0 && is_nav_menu_item($del_id)) {
451 wp_delete_post($del_id, true);
452 }
453 }
454 }
455
456 if (isset($_POST['menu_locations']) && is_array($_POST['menu_locations'])) {
457 $locations = get_nav_menu_locations();
458 foreach ($_POST['menu_locations'] as $loc => $checked) {
459 $loc = sanitize_key($loc);
460 if ($checked) {
461 $locations[$loc] = $menu_id;
462 } elseif (isset($locations[$loc]) && $locations[$loc] === $menu_id) {
463 unset($locations[$loc]);
464 }
465 }
466 set_theme_mod('nav_menu_locations', $locations);
467 }
468 }
469
470 $items = isset($_POST['items']) && is_array($_POST['items']) ? wp_unslash($_POST['items']) : [];
471 $id_mapping = [];
472
473 foreach ($items as $item_data) {
474 if (!is_array($item_data)) {
475 continue;
476 }
477
478 $db_id = isset($item_data['menu-item-db-id']) ? intval($item_data['menu-item-db-id']) : 0;
479 $temp_id = isset($item_data['_temp_id']) ? sanitize_text_field($item_data['_temp_id']) : '';
480
481 $args = [
482 'menu-item-object-id' => isset($item_data['menu-item-object-id']) ? sanitize_text_field($item_data['menu-item-object-id']) : '',
483 'menu-item-object' => isset($item_data['menu-item-object']) ? sanitize_text_field($item_data['menu-item-object']) : '',
484 'menu-item-parent-id' => isset($item_data['menu-item-parent-id']) ? intval($item_data['menu-item-parent-id']) : 0,
485 'menu-item-position' => isset($item_data['menu-item-position']) ? intval($item_data['menu-item-position']) : 0,
486 'menu-item-type' => isset($item_data['menu-item-type']) ? sanitize_text_field($item_data['menu-item-type']) : 'custom',
487 'menu-item-title' => isset($item_data['menu-item-title']) ? wp_kses_post($item_data['menu-item-title']) : '',
488 'menu-item-url' => isset($item_data['menu-item-url']) ? esc_url_raw($item_data['menu-item-url']) : '',
489 'menu-item-description' => isset($item_data['menu-item-description']) ? wp_kses_post($item_data['menu-item-description']) : '',
490 'menu-item-attr-title' => isset($item_data['menu-item-attr-title']) ? sanitize_text_field($item_data['menu-item-attr-title']) : '',
491 'menu-item-target' => isset($item_data['menu-item-target']) ? sanitize_text_field($item_data['menu-item-target']) : '',
492 'menu-item-classes' => isset($item_data['menu-item-classes']) ? sanitize_text_field($item_data['menu-item-classes']) : '',
493 'menu-item-xfn' => isset($item_data['menu-item-xfn']) ? sanitize_text_field($item_data['menu-item-xfn']) : '',
494 'menu-item-status' => 'publish',
495 ];
496
497 $new_id = wp_update_nav_menu_item($menu_id, $db_id > 0 ? $db_id : 0, $args);
498
499 if (!is_wp_error($new_id) && $new_id) {
500 if ($temp_id) {
501 $id_mapping[$temp_id] = $new_id;
502 }
503
504 $custom_meta_fields = [
505 'menu-item-design' => '_menu_item_design',
506 'menu-item-image' => '_menu_item_image',
507 'menu-item-icon' => '_menu_item_icon',
508 'menu-item-custom_label' => '_menu_item_custom_label',
509 'menu-item-custom_label_color' => '_menu_item_custom_label_color',
510 'menu-item-badge_color' => '_menu_item_badge_color',
511 ];
512
513 foreach ($custom_meta_fields as $post_key => $meta_key) {
514 if (isset($item_data[$post_key])) {
515 update_post_meta($new_id, $meta_key, sanitize_text_field($item_data[$post_key]));
516 }
517 }
518 }
519 }
520
521 wp_send_json_success([
522 'chunk_index' => $chunk_index,
523 'total_chunks' => $total_chunks,
524 'is_last' => $is_last_chunk,
525 'id_mapping' => $id_mapping,
526 ]);
527 }
528
529 public function render_menu_chunk_saver_script() {
530 ?>
531 <style>
532 @keyframes wpex-btn-spin {
533 0% { transform: rotate(0deg); }
534 100% { transform: rotate(360deg); }
535 }
536 .wpex-btn-spinner-icon {
537 display: inline-block;
538 width: 14px;
539 height: 14px;
540 border: 2px solid rgba(255, 255, 255, 0.4);
541 border-radius: 50%;
542 border-top-color: #ffffff;
543 animation: wpex-btn-spin 0.75s linear infinite;
544 vertical-align: middle;
545 margin-right: 6px;
546 }
547 .button.menu-save.is-saving {
548 display: inline-flex !important;
549 align-items: center !important;
550 justify-content: center !important;
551 pointer-events: none !important;
552 opacity: 0.9 !important;
553 cursor: not-allowed !important;
554 }
555 .item-controls .item-group-toggle {
556 display: inline-flex !important;
557 align-items: center;
558 justify-content: center;
559 gap: 4px;
560 height: 38px;
561 padding: 0 8px !important;
562 margin: 0 !important;
563 color: #64748b;
564 background: transparent !important;
565 border: none !important;
566 box-shadow: none !important;
567 cursor: pointer;
568 text-decoration: none;
569 outline: none;
570 transition: all 0.15s ease;
571 vertical-align: middle;
572 }
573 .item-controls .item-group-toggle:hover {
574 color: #2563eb !important;
575 background: #f1f5f9 !important;
576 }
577 .item-controls .item-group-toggle .dashicons {
578 font-size: 14px;
579 width: 14px;
580 height: 14px;
581 line-height: 14px;
582 transition: transform 0.2s ease;
583 }
584 .item-controls .item-group-toggle .wpex-group-badge {
585 font-size: 11px;
586 font-weight: 700;
587 background: #e2e8f0;
588 color: #475569;
589 padding: 1px 6px;
590 border-radius: 10px;
591 line-height: 1.2;
592 }
593 .item-controls .item-group-toggle.is-collapsed .wpex-group-badge {
594 background: #2563eb;
595 color: #ffffff;
596 }
597 li.menu-item.wpex-has-collapsed-children {
598 border-left: 3px solid #2563eb !important;
599 }
600 .wpex-menu-single-toggle-wrapper {
601 position: absolute !important;
602 left: 50% !important;
603 top: 50% !important;
604 transform: translate(-50%, -50%) !important;
605 }
606 .wpex-menu-single-toggle-wrapper .button {
607 display: inline-flex !important;
608 align-items: center !important;
609 justify-content: center !important;
610 gap: 4px !important;
611 font-size: 13px !important;
612 height: 30px !important;
613 padding: 0 12px !important;
614 line-height: 1 !important;
615 vertical-align: middle !important;
616 }
617 .wpex-menu-single-toggle-wrapper .button .dashicons {
618 display: inline-flex !important;
619 align-items: center !important;
620 justify-content: center !important;
621 font-size: 16px !important;
622 width: 16px !important;
623 height: 16px !important;
624 line-height: 16px !important;
625 margin: 0 !important;
626 padding: 0 !important;
627 vertical-align: middle !important;
628 }
629 </style>
630
631 <script type="text/javascript">
632 jQuery(document).ready(function ($) {
633 function getItemDepth($li) {
634 var cls = $li.attr('class') || '';
635 var match = cls.match(/menu-item-depth-(\d+)/);
636 return match ? parseInt(match[1], 10) : 0;
637 }
638
639 function getGroupChildren($parentLi) {
640 var parentDepth = getItemDepth($parentLi);
641 var $children = $();
642 var $next = $parentLi.next('li.menu-item');
643 if ($next.length && getItemDepth($next) > parentDepth) {
644 while ($next.length && getItemDepth($next) > parentDepth) {
645 $children = $children.add($next);
646 $next = $next.next('li.menu-item');
647 }
648 }
649 return $children;
650 }
651
652 function updateGroupToggles() {
653 $('#menu-to-edit > li.menu-item').each(function () {
654 var $item = $(this);
655 var depth = getItemDepth($item);
656 var $controls = $item.find('.item-controls').first();
657 var $editBtn = $controls.find('.item-edit');
658 var $next = $item.next('li.menu-item');
659
660 var hasChildren = $next.length > 0 && getItemDepth($next) > depth;
661 var $children = hasChildren ? getGroupChildren($item) : $();
662 var childCount = $children.length;
663 var $toggleBtn = $controls.find('.item-group-toggle');
664
665 if (hasChildren && childCount > 0) {
666 if (!$toggleBtn.length) {
667 $toggleBtn = $('<button type="button" class="button-link item-group-toggle" title="<?php esc_attr_e('Collapse / Expand', 'wp-extra'); ?>" aria-label="<?php esc_attr_e('Toggle group', 'wp-extra'); ?>">' +
668 '<span class="dashicons dashicons-arrow-down-alt2"></span>' +
669 '<span class="wpex-group-badge">' + childCount + '</span>' +
670 '</button>');
671
672 if ($editBtn.length) {
673 $editBtn.after($toggleBtn);
674 } else {
675 $controls.append($toggleBtn);
676 }
677 } else {
678 $toggleBtn.find('.wpex-group-badge').text(childCount);
679 }
680 } else {
681 if ($toggleBtn.length) {
682 $toggleBtn.remove();
683 }
684 $item.removeClass('wpex-has-collapsed-children');
685 }
686 });
687 }
688
689 if ($('#nav-menu-footer .major-publishing-actions').length && !$('.wpex-menu-single-toggle-wrapper').length) {
690 var singleToggleHtml = $('<div class="wpex-menu-single-toggle-wrapper">' +
691 '<button type="button" class="button wpex-toggle-all-groups" title="<?php esc_attr_e('Collapse / Expand', 'wp-extra'); ?>">' +
692 '<span class="dashicons dashicons-arrow-right-alt2"></span> <?php esc_html_e('Collapse', 'wp-extra'); ?>' +
693 '</button>' +
694 '</div>');
695
696 $('#nav-menu-footer .major-publishing-actions .publishing-action').before(singleToggleHtml);
697 }
698
699 $(document).on('click', '.wpex-toggle-all-groups', function (e) {
700 e.preventDefault();
701 var $btn = $(this);
702 var isAllCollapsed = $btn.hasClass('is-all-collapsed');
703
704 if (!isAllCollapsed) {
705 $('#menu-to-edit > li.menu-item').each(function () {
706 var $item = $(this);
707 var depth = getItemDepth($item);
708 var $toggle = $item.find('.item-group-toggle');
709 if (depth === 0 && $toggle.length && !$toggle.hasClass('is-collapsed')) {
710 $toggle.trigger('click');
711 }
712 });
713 $btn.addClass('is-all-collapsed');
714 $btn.html('<span class="dashicons dashicons-arrow-down-alt2"></span> <?php esc_html_e('Expand', 'wp-extra'); ?>');
715 } else {
716 $('#menu-to-edit > li.menu-item').each(function () {
717 var $item = $(this);
718 var depth = getItemDepth($item);
719 var $toggle = $item.find('.item-group-toggle');
720 if (depth === 0 && $toggle.length && $toggle.hasClass('is-collapsed')) {
721 $toggle.trigger('click');
722 }
723 });
724 $btn.removeClass('is-all-collapsed');
725 $btn.html('<span class="dashicons dashicons-arrow-right-alt2"></span> <?php esc_html_e('Collapse', 'wp-extra'); ?>');
726 }
727 });
728
729 $(document).on('click', '.item-group-toggle', function (e) {
730 e.preventDefault();
731 e.stopPropagation();
732
733 var $btn = $(this);
734 var $item = $btn.closest('li.menu-item');
735 var $children = getGroupChildren($item);
736 var isCollapsed = $btn.hasClass('is-collapsed');
737
738 if (isCollapsed) {
739 $children.show();
740 $btn.removeClass('is-collapsed');
741 $btn.find('.dashicons').removeClass('dashicons-arrow-right-alt2').addClass('dashicons-arrow-down-alt2');
742 $btn.attr('title', '<?php esc_attr_e('Collapse', 'wp-extra'); ?>');
743 $item.removeClass('wpex-has-collapsed-children');
744 } else {
745 $children.hide();
746 $btn.addClass('is-collapsed');
747 $btn.find('.dashicons').removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-right-alt2');
748 $btn.attr('title', '<?php esc_attr_e('Expand', 'wp-extra'); ?> (' + $children.length + ')');
749 $item.addClass('wpex-has-collapsed-children');
750 }
751 });
752
753 updateGroupToggles();
754
755 var debounceTimer = null;
756 function triggerUpdateToggles() {
757 clearTimeout(debounceTimer);
758 debounceTimer = setTimeout(updateGroupToggles, 100);
759 }
760
761 var menuTreeObserver = new MutationObserver(function () {
762 triggerUpdateToggles();
763 });
764 var menuToEditEl = document.getElementById('menu-to-edit');
765 if (menuToEditEl) {
766 menuTreeObserver.observe(menuToEditEl, { childList: true, subtree: true });
767 }
768
769 $('#menu-to-edit').on('sortstop', function () {
770 triggerUpdateToggles();
771 });
772
773 var $urlInput = $('#custom-menu-item-url');
774 if ($urlInput.length) {
775 $urlInput.attr('type', 'text').removeAttr('required');
776 if ($urlInput.val() === 'https://' || $urlInput.val() === 'http://') {
777 $urlInput.val('').attr('placeholder', '# (leave empty for text-only item)');
778 }
779
780 $('#submit-custommenuitem').on('click mousedown', function () {
781 var val = $.trim($urlInput.val());
782 if (val === '' || val === 'https://' || val === 'http://') {
783 $urlInput.val('#');
784 }
785 });
786 }
787
788 var $form = $('#update-nav-menu');
789 if (!$form.length) return;
790
791 var CHUNK_SIZE = 30;
792
793 $form.on('submit', function (e) {
794 $('#menu-to-edit > li.menu-item:hidden').show();
795
796 var $menuItems = $('#menu-to-edit > li.menu-item');
797 var totalItems = $menuItems.length;
798
799 e.preventDefault();
800
801 var menuId = $('#menu').val() || 0;
802 if (!menuId || menuId === '0') {
803 $form.off('submit').submit();
804 return;
805 }
806
807 var $submitInputs = $('#save_menu_header, #save_menu_footer, .menu-save');
808 $('.publishing-action .spinner, #nav-menu-header .spinner').addClass('is-active');
809
810 $submitInputs.each(function () {
811 var $btn = $(this);
812 var loadingText = '<span class="wpex-btn-spinner-icon"></span> <?php esc_html_e('Saving...', 'wp-extra'); ?>';
813
814 if ($btn.is('input')) {
815 var $newBtn = $('<button type="submit" class="' + $btn.attr('class') + ' is-saving" id="' + $btn.attr('id') + '" disabled>' + loadingText + '</button>');
816 $btn.replaceWith($newBtn);
817 } else {
818 $btn.addClass('is-saving').prop('disabled', true).html(loadingText);
819 }
820 });
821
822 var allItemsData = [];
823 $menuItems.each(function (index) {
824 var $item = $(this);
825 var itemObj = {};
826
827 itemObj['menu-item-position'] = index + 1;
828
829 var itemId = $item.find('.menu-item-data-db-id').val();
830 itemObj['menu-item-db-id'] = itemId || 0;
831 itemObj['_temp_id'] = $item.attr('id') || ('item_' + index);
832
833 $item.find(':input').each(function () {
834 var name = this.name;
835 if (!name || name.indexOf('menu-item-') === -1) return;
836
837 var cleanKey = name.replace(/\[[^\]]*\]/g, '');
838 if (this.type === 'checkbox' || this.type === 'radio') {
839 if (this.checked) itemObj[cleanKey] = $(this).val();
840 } else {
841 itemObj[cleanKey] = $(this).val();
842 }
843 });
844
845 allItemsData.push(itemObj);
846 });
847
848 var chunks = [];
849 for (var i = 0; i < allItemsData.length; i += CHUNK_SIZE) {
850 chunks.push(allItemsData.slice(i, i + CHUNK_SIZE));
851 }
852 if (chunks.length === 0) {
853 chunks.push([]);
854 }
855
856 var totalChunks = chunks.length;
857 var currentChunk = 0;
858
859 var deletedItems = [];
860 $('input[name^="delete-menu-item"]').each(function () {
861 deletedItems.push($(this).val());
862 });
863
864 var menuLocations = {};
865 $('.menu-theme-locations :checkbox').each(function () {
866 var locName = $(this).attr('name').replace('menu-locations[', '').replace(']', '');
867 menuLocations[locName] = $(this).is(':checked') ? 1 : 0;
868 });
869
870 var menuName = $('#menu-name').val();
871
872 function sendNextChunk() {
873 var isFirst = (currentChunk === 0);
874 var isLast = (currentChunk === totalChunks - 1);
875
876 var postData = {
877 action: 'wpex_save_menu_chunk',
878 security: '<?php echo esc_js(wp_create_nonce('wpex_menu_chunk_nonce')); ?>',
879 menu_id: menuId,
880 chunk_index: currentChunk,
881 total_chunks: totalChunks,
882 is_first_chunk: isFirst ? 1 : 0,
883 is_last_chunk: isLast ? 1 : 0,
884 items: chunks[currentChunk]
885 };
886
887 if (isFirst) {
888 postData.menu_name = menuName;
889 postData.deleted_items = deletedItems;
890 postData.menu_locations = menuLocations;
891 }
892
893 $.ajax({
894 url: ajaxurl,
895 type: 'POST',
896 data: postData,
897 dataType: 'json',
898 success: function (response) {
899 if (response.success) {
900 currentChunk++;
901 if (currentChunk < totalChunks) {
902 sendNextChunk();
903 } else {
904 $('.menu-save').html('✓ <?php esc_html_e('Saved!', 'wp-extra'); ?>');
905 $('.publishing-action .spinner, #nav-menu-header .spinner').removeClass('is-active');
906 setTimeout(function () {
907 window.location.reload();
908 }, 400);
909 }
910 } else {
911 alert('Error saving menu: ' + (response.data ? response.data.message : 'Unknown error'));
912 window.location.reload();
913 }
914 },
915 error: function (xhr, status, error) {
916 alert('Server connection error: ' + error);
917 window.location.reload();
918 }
919 });
920 }
921
922 sendNextChunk();
923 });
924 });
925 </script>
926 <?php
927 }
928
929 public function disable_widgets() {
930 add_action('widgets_init', [$this, 'unregister_all_default_widgets'], 99);
931 }
932
933 public function unregister_all_default_widgets() {
934 $default_widgets = [
935 'WP_Widget_Pages',
936 'WP_Widget_Calendar',
937 'WP_Widget_Archives',
938 'WP_Widget_Links',
939 'WP_Widget_Media_Audio',
940 'WP_Widget_Media_Image',
941 'WP_Widget_Media_Video',
942 'WP_Widget_Media_Gallery',
943 'WP_Widget_Custom_HTML',
944 'WP_Widget_Meta',
945 'WP_Widget_Search',
946 'WP_Widget_Text',
947 'WP_Widget_Categories',
948 'WP_Widget_Recent_Posts',
949 'WP_Widget_Recent_Comments',
950 'WP_Widget_RSS',
951 'WP_Widget_Tag_Cloud',
952 'WP_Nav_Menu_Widget',
953 'WP_Widget_Block',
954 ];
955
956 foreach ($default_widgets as $widget) {
957 unregister_widget($widget);
958 }
959 }
960
961 }