PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
mainwp / class / class-mainwp-unhooks-helper.php

class-mainwp-unhooks-helper.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.1, at class/class-mainwp-unhooks-helper.php

707 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP Unhook Helper
4 *
5 * Utilities to remove 3rd-party actions/filters by name, class, namespace, or file path.
6 *
7 * @package MainWP/Dashboard
8 */
9
10 namespace MainWP\Dashboard;
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Class MainWP_Unhooks_Helper
19 *
20 * @package MainWP\Dashboard
21 */
22 class MainWP_Unhooks_Helper { // phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace -- NOSONAR.
23
24 /**
25 * Private static variable to hold the single instance of the class.
26 *
27 * @var mixed Default null
28 */
29 private static $instance = null;
30
31 /**
32 * Private static variable to hold the single instance of the class.
33 *
34 * @var mixed Default null
35 */
36 private $active_plugins = null;
37
38 /**
39 * Method get_class_name()
40 *
41 * Get Class Name.
42 *
43 * @return object
44 */
45 public static function get_class_name() {
46 return __CLASS__;
47 }
48
49 /**
50 * Method instance()
51 *
52 * @return self
53 */
54 public static function instance() {
55 if ( null === static::$instance ) {
56 static::$instance = new self();
57 }
58 return static::$instance;
59 }
60
61
62 /**
63 * Constructor.
64 *
65 * Run each time the class is called.
66 */
67 public function __construct() {
68 add_action(
69 'plugins_loaded',
70 array( $this, 'hook_remove_unwanted_hooks' ),
71 0
72 );
73
74 add_action(
75 'mainwp_shutdown',
76 array( $this, 'hook_unhook_shutdown' ),
77 10
78 );
79 }
80
81 /**
82 * Remove unwanted hooks.
83 */
84 public function hook_remove_unwanted_hooks() {
85
86 if ( ! static::is_enabled_unhooks() ) {
87 return;
88 }
89
90 if ( ! static::is_request_unhooks() ) {
91 add_action(
92 'current_screen',
93 array( $this, 'hook_remove_unwanted_hooks_default_pages' ),
94 );
95 return;
96 }
97
98 /**
99 * Hook remove unwanted hooks.
100 *
101 * @since 5.5.
102 */
103 $remove = apply_filters( 'mainwp_unhooks_remove_unwanted_hooks', true, 'unwanted_hooks' ); //phpcs:ignore -- ok.
104
105 if ( ! $remove ) {
106 return;
107 }
108
109 MainWP_Logger::instance()->log_events( 'unhooks', '[hook_remove_unwanted_hooks] :: [page=' . ( isset( $_GET['page'] ) ? esc_html( wp_unslash( $_GET['page'] ) ) : '' ) . ']' ); //phpcs:ignore -- ok.
110 $this->remove_slow_hooks();
111 }
112
113 /**
114 * Default mainwp pages to remove hooks.
115 */
116 public function hook_remove_unwanted_hooks_default_pages() {
117
118 if ( ! MainWP_System::is_mainwp_pages() ) {
119 return;
120 }
121
122 if ( static::is_request_unhooks() ) { // requested unhooks.
123 return;
124 }
125
126 $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore -- ok.
127
128 if ( $this->is_excluded_pages( $page ) ) {
129 return;
130 }
131
132 /**
133 * Hook default mainwp pages to remove unwanted hooks.
134 *
135 * @since 5.5.
136 */
137 $remove = apply_filters( 'mainwp_unhooks_remove_unwanted_hooks', true, 'default_pages' ); //phpcs:ignore -- ok.
138
139 if ( $remove ) {
140 MainWP_Logger::instance()->log_events( 'unhooks', '[hook_remove_unwanted_hooks_default_pages] :: [page=' . $page . ']' ); //phpcs:ignore -- ok.
141 $this->remove_slow_hooks();
142 }
143 }
144
145
146 /**
147 * Method is_excluded_pages().
148 *
149 * @param string $page Excluded page check.
150 *
151 * @return bool Excluded page or not
152 */
153 public function is_excluded_pages( $page = '' ) {
154 if ( empty( $page ) || in_array( $page, (array) $this->get_default_excluded_pages() ) ) {
155 return true;
156 }
157 return false;
158 }
159
160
161 /**
162 * Method is_enabled_unhooks().
163 *
164 * @return bool Enabled or not.
165 */
166 public static function is_enabled_unhooks() {
167
168 static $enabled;
169
170 if ( null === $enabled ) {
171 /**
172 * Enable remove unwanted hooks or not.
173 *
174 * Default: false.
175 *
176 * @since 5.5.
177 */
178 $enabled = apply_filters( 'mainwp_unhooks_is_enabled', false ); //phpcs:ignore -- ok.
179 }
180
181 return $enabled ? true : false;
182 }
183
184 /**
185 * Hook shutdown.
186 */
187 public function hook_unhook_shutdown() {
188
189 if ( static::is_enabled_unhooks() && ( ! $this->is_excluded_pages() || static::is_request_unhooks() ) ) {
190 $rt = MainWP_Execution_Helper::get_run_time();
191 MainWP_Logger::instance()->log_events( 'unhooks', '[Execution time=' . $rt . '] (seconds)', MainWP_Logger::INFO ); // phpcs:ignore -- ok.
192 MainWP_Logger::instance()->log_events( 'execution-time', '[Unhooks] :: [runtime=' . $rt . '] (seconds)', MainWP_Logger::INFO ); // phpcs:ignore -- ok.
193 }
194 }
195
196 /**
197 * Verify unhooks request.
198 *
199 * @return bool Unhooks or not.
200 */
201 public static function is_request_unhooks() {
202 if ( ! isset( $_GET['_uhnonce'] ) || ! wp_verify_nonce( (string) $_GET['_uhnonce'] , 'unhooks_nonce' ) ) { // phpcs:ignore -- ok.
203 return false;
204 }
205 return true;
206 }
207
208 /**
209 * Method get_unhooks_active_plugins().
210 *
211 * @return array Active non mainwp plugins.
212 */
213 public function get_unhooks_active_plugins() {
214
215 if ( null === $this->active_plugins ) {
216 $this->active_plugins = array();
217 $mainwp_str = 'mainwp';
218 $plugins_files = get_option( 'active_plugins', array() );
219 if ( is_array( $plugins_files ) ) {
220 foreach ( $plugins_files as $indx => $plugin_file ) {
221 if ( false === stripos( $plugin_file, $mainwp_str ) ) {
222 $this->active_plugins[ $indx ] = $plugin_file;
223 }
224 }
225 }
226 /**
227 * Get unhooks active plugins files.
228 *
229 * @since 5.5.
230 */
231 $this->active_plugins = apply_filters( 'mainwp_unhooks_active_plugins_files', $this->active_plugins );
232 }
233
234 return $this->active_plugins;
235 }
236
237
238
239 /**
240 * Get excluded pages.
241 */
242 public function get_default_excluded_pages() {
243
244 /**
245 * Hook get default excluded pages.
246 *
247 * @since 5.5.
248 */
249 return apply_filters(
250 'mainwp_unhooks_default_excluded_pages',
251 array(
252 'ActionLogs',
253 )
254 );
255 }
256
257
258 /**
259 * Remove slow hooks.
260 */
261 public function remove_slow_hooks() { // phpcs:ignore -- NOSONAR -complex.
262
263 $remove_desired_hooks = $this->unhooks_list();
264
265 $unhooks_types = $this->get_unhooks_types();
266
267 if ( is_array( $unhooks_types ) ) {
268 foreach ( $unhooks_types as $type => $values ) {
269 switch ( $type ) {
270 case 'all_exclude_mainwp':
271 case 'files_names':
272 $files = array();
273 if ( 'files_names' === $type ) {
274 $files = $values;
275 } elseif ( 'all_exclude_mainwp' === $type ) {
276 if ( true === $values ) {
277 $files = $this->get_unhooks_active_plugins();
278 }
279 }
280 if ( is_array( $files ) && ! empty( $files ) ) {
281 foreach ( $remove_desired_hooks as $hook ) {
282 $this->remove_by_file( $hook, $files );
283 }
284 }
285 break;
286 case 'plugin_names':
287 $names = $values;
288 if ( is_array( $names ) && ! empty( $names ) ) {
289 foreach ( $remove_desired_hooks as $hook ) {
290 $this->remove_by_name( $hook, $names );
291 }
292 }
293 break;
294 case 'class_names':
295 $cls_names = $values;
296 if ( is_array( $cls_names ) && ! empty( $cls_names ) ) {
297 foreach ( $remove_desired_hooks as $hook ) {
298 $this->remove_by_class( $hook, $cls_names );
299 }
300 }
301 break;
302 case 'all_specific':
303 $hooks = $values;
304 if ( is_array( $hooks ) ) {
305 foreach ( $hooks as $hook ) {
306 $this->remove_all( $hook );
307 }
308 }
309 break;
310 default:
311 // Nothing.
312 break;
313 }
314 }
315 }
316 }
317
318 /**
319 * Get un-hooks list.
320 *
321 * @param string $remove Which hooks list.
322 *
323 * @return array
324 */
325 public function unhooks_list( $remove = 'main' ) {
326
327 $main_hooks = array(
328 'admin_init',
329 'init',
330 'plugins_loaded',
331 );
332
333 $other_hooks = array(
334 'admin_footer',
335 'admin_head',
336 'admin_menu',
337 'plugin_loaded',
338 'wp_head',
339 'wp_loaded',
340 'wp_footer',
341 'admin_bar_init',
342 'admin_bar_menu',
343 'admin_enqueue_scripts',
344 'admin_notices',
345 'admin_print_footer_scripts',
346 'admin_print_scripts',
347 'admin_print_styles',
348 'after_setup_theme',
349 'all_admin_notices',
350 'current_screen',
351 'get_header',
352 'load_textdomain',
353 'muplugins_loaded',
354 'pre_get_posts',
355 'setup_theme',
356 'wp',
357 'wp_default_scripts',
358 'wp_default_styles',
359 'wp_enqueue_scripts',
360 'wp_print_footer_scripts',
361 'wp_print_scripts',
362 'wp_print_styles',
363 'wp_print_scripts',
364 );
365
366 /**
367 * Hook remove hooks list.
368 *
369 * @since 5.5.
370 */
371 return apply_filters(
372 'mainwp_unhooks_list',
373 'main' === $remove ? $main_hooks : array_unique( array_merge( $main_hooks, $other_hooks ) )
374 );
375 }
376
377 /**
378 * Get un-hooks types.
379 *
380 * @return array
381 */
382 public function get_unhooks_types() {
383
384 /**
385 * Hook get unhooks types.
386 *
387 * @since 5.5.
388 */
389 return apply_filters(
390 'mainwp_unhooks_get_types',
391 array(
392 'all_exclude_mainwp' => true,
393 'files_names' => array(),
394 'plugin_names' => array(),
395 'class_names' => array(),
396 'all_specific' => false,
397 )
398 );
399 }
400
401
402 /**
403 * Add url params to unhook.
404 *
405 * @param string $url Url to enable un-hooks.
406 *
407 * @return string Url with unhook nonce.
408 */
409 public static function add_unhooks_params( $url ) {
410
411 if ( ! static::is_enabled_unhooks() ) {
412 return $url;
413 }
414
415 $url_unhook = $url . '&_uhnonce=' . wp_create_nonce( 'unhooks_nonce' );
416 /**
417 * Hook add unhooks url params.
418 *
419 * @since 5.5.
420 */
421 return apply_filters( 'mainwp_unhooks_url_params', $url_unhook, $url ); // default disable.
422 }
423
424 /**
425 * Remove callbacks on a hook where the callable's function/method name matches a pattern
426 *
427 * @param mixed $hook The hook.
428 * @param mixed $names Array of classes names.
429 * @return int
430 */
431 private function remove_by_name( $hook, $cls_names = array() ) { // phpcs:ignore -- NOSONAR -complex.
432 global $wp_filter;
433 $removed = 0;
434 $failed = 0;
435
436 if ( empty( $wp_filter[ $hook ] ) ) {
437 return 0;
438 }
439
440 $hook_obj = $wp_filter[ $hook ];
441 $callbacks = $hook_obj->callbacks ?? array();
442
443 foreach ( $callbacks as $priority => $list ) {
444 foreach ( $list as $entry ) {
445 $callable = $entry['function'] ?? null;
446 $name = $this->callable_label( $callable );
447 if ( ! empty( $name ) ) {
448 $found = false;
449 foreach ( $cls_names as $find_name ) {
450 if ( false !== stripos( $name, $find_name ) ) {
451 $found = true;
452 break;
453 }
454 }
455
456 if ( $found ) {
457 $success = remove_action( $hook, $callable, (int) $priority );
458 if ( $success ) {
459 ++$removed;
460 } else {
461 ++$failed;
462 }
463 }
464 }
465 }
466 }
467 return $removed;
468 }
469
470 /**
471 * Remove callbacks whose underlying file path matches a regex (good for targeting a plugin).
472 *
473 * Note: This is HEAVY function.
474 *
475 * @param mixed $hook The hook.
476 * @param mixed $path_files The files path to check hooks.
477 * @return int
478 */
479 private function remove_by_file( $hook, $path_files ) { // phpcs:ignore -- NOSONAR -complex.
480
481 global $wp_filter;
482
483 if ( empty( $wp_filter[ $hook ] ) ) {
484 return 0;
485 }
486
487 $removed = 0;
488 $failed = 0;
489
490 $hook_obj = $wp_filter[ $hook ];
491 $callbacks = $hook_obj->callbacks ?? array();
492
493 $plugins_dir = 'wp-content/plugins';
494
495 if ( defined( 'WP_PLUGIN_DIR' ) ) {
496 $plugins_dir = str_replace( ABSPATH, '', WP_PLUGIN_DIR );
497 }
498
499 $lists = array();
500 $lists[ $hook ] = array();
501 foreach ( $callbacks as $priority => $list ) {
502 foreach ( $list as $entry ) {
503 $callable = $entry['function'] ?? null;
504 $file = $this->callable_file( $callable );
505 if ( ! empty( $file ) ) {
506 $found = false;
507 foreach ( $path_files as $dir_file ) {
508 $path = '/' . $plugins_dir . '/' . MainWP_Utility::get_dir_slug( $dir_file ) . '/';
509 if ( false !== stripos( $file, $path ) ) {
510 $found = true;
511 break;
512 }
513 }
514 if ( $found ) {
515 $success = remove_action( $hook, $callable, (int) $priority );
516 if ( $success ) {
517 ++$removed;
518 $lists[ $hook ][] = $file;
519 } else {
520 ++$failed;
521 }
522 }
523 }
524 unset( $callable );
525 }
526 }
527
528 if ( $removed || $failed ) {
529 MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by files] :: [hook=' . $hook . '] :: [removed=' . $removed . '] :: [failed=' . $failed . ']' ); //phpcs:ignore -- ok.
530 MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by files] :: [lists=' . print_r($lists, true ) . ']' ); //phpcs:ignore -- ok.
531 }
532
533 return $removed;
534 }
535
536 /**
537 * Remove callbacks where the callable belongs to a class/namespace regex.
538 *
539 * @param mixed $hook The hook.
540 * @param mixed $classes_names The classes names.
541 * @return int
542 */
543 private function remove_by_class( $hook, $classes_names ) { // phpcs:ignore -- NOSONAR -complex.
544 global $wp_filter;
545 $removed = 0;
546 $failed = 0;
547
548 if ( empty( $wp_filter[ $hook ] ) ) {
549 return 0;
550 }
551
552 $hook_obj = $wp_filter[ $hook ];
553 $callbacks = $hook_obj->callbacks ?? array();
554
555 foreach ( $callbacks as $priority => $list ) {
556 foreach ( $list as $entry ) {
557 $callable = $entry['function'] ?? null;
558
559 if ( is_array( $callable ) ) {
560 $obj_or_class = $callable[0];
561 $class = is_object( $obj_or_class ) ? get_class( $obj_or_class ) : $obj_or_class;
562 if ( ! empty( $class ) ) {
563 $found = false;
564
565 foreach ( $classes_names as $find_class ) {
566 if ( false !== stripos( $class, $find_class ) ) {
567 $found = true;
568 break;
569 }
570 }
571
572 if ( $found ) {
573 $success = remove_action( $hook, $callable, (int) $priority );
574 if ( $success ) {
575 ++$removed;
576 } else {
577 ++$failed;
578 }
579 }
580 }
581 }
582 }
583 }
584 if ( $removed || $failed ) {
585 MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by class :: [hook=' . $hook . '] :: [removed=' . $removed . '] :: [failed=' . $failed . ']' ); //phpcs:ignore -- ok.
586 }
587
588 return $removed;
589 }
590
591 /**
592 * Remove everything on a hook (optionally only at a given priority).
593 * Works for actions & filters.
594 *
595 * @param mixed $hook The hook.
596 * @param mixed $priority Hook priority.
597 * @return void
598 */
599 private function remove_all( $hook, $priority = false ) {
600 if ( false === $priority ) {
601 remove_all_actions( $hook ); // wrapper for remove_all_filters.
602 } else {
603 remove_all_filters( $hook, (int) $priority );
604 }
605 MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks all :: [hook=' . $hook . '] :: [priority=' . (false !== $priority ? (int)$priority : 'false') . ']' ); //phpcs:ignore -- ok.
606 }
607
608 /**
609 * Human label for any callable.
610 *
611 * @param mixed $callback The callable.
612 * @return string
613 */
614 private function callable_label( $callback ) {
615 if ( is_string( $callback ) ) {
616 return $callback;
617 }
618 if ( is_array( $callback ) ) {
619 $obj_or_class = $callback[0];
620 $meth = $callback[1] ?? '';
621 $class = is_object( $obj_or_class ) ? get_class( $obj_or_class ) : (string) $obj_or_class;
622 return $class ? "{$class}::{$meth}" : $meth;
623 }
624 if ( $callback instanceof \Closure ) {
625 return 'Closure';
626 }
627 return null;
628 }
629
630 /**
631 * Returns the source filename for a callable, or null if unknown.
632 *
633 * @param callable $callback
634 * @return string|null
635 */
636 private function callable_file( $callback ) { // phpcs:ignore -- NOSONAR -complex.
637
638 static $cache = array();
639
640 // Build a stable cache key for all callable shapes.
641 if ( $callback instanceof \Closure ) {
642 $key = 'closure:' . spl_object_id( $callback );
643 } elseif ( is_string( $callback ) ) {
644 $key = 'string:' . $callback; // "function" or "Class::method".
645 } elseif ( is_array( $callback ) ) {
646 $key = 'array:' . ( is_object( $callback[0] ) ? get_class( $callback[0] ) . '#' . spl_object_id( $callback[0] ) : $callback[0] ) . '::' . ( $callback[1] ?? '' );
647 } elseif ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) {
648 $key = 'invokable:' . get_class( $callback ) . '#' . spl_object_id( $callback );
649 } else {
650 return null;
651 }
652
653 if ( array_key_exists( $key, $cache ) ) {
654 return $cache[ $key ];
655 }
656
657 try {
658 if ( $callback instanceof \Closure ) {
659 $ref = new \ReflectionFunction( $callback );
660 $cache[ $key ] = $ref->getFileName();
661 $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null;
662 return $cache[ $key ];
663 }
664
665 if ( is_string( $callback ) ) {
666 // function or "Class::method".
667 if ( strpos( $callback, '::' ) !== false ) {
668 [$class, $method] = explode( '::', $callback, 2 );
669 $ref = new \ReflectionMethod( $class, $method );
670 $cache[ $key ] = $ref->getFileName();
671 $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null;
672 return $cache[ $key ];
673 } else {
674 // userland function.
675 if ( ! function_exists( $callback ) ) {
676 $cache[ $key ] = null;
677 return $cache[ $key ];
678 }
679 $ref = new \ReflectionFunction( $callback );
680 $cache[ $key ] = $ref->getFileName();
681 $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null;
682 return $cache[ $key ];
683 }
684 }
685
686 if ( is_array( $callback ) ) {
687 [$objOrClass, $method] = array( $callback[0], $callback[1] ?? '' );
688 $ref = new \ReflectionMethod( $objOrClass, $method );
689 $cache[ $key ] = $ref->getFileName();
690 $cache[ $key ] = $cache[ $key ] ?: null;
691 return $cache[ $key ];
692 }
693
694 if ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) {
695 $ref = new \ReflectionMethod( $callback, '__invoke' );
696 $cache[ $key ] = $ref->getFileName();
697 $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null;
698 return $cache[ $key ];
699 }
700 } catch ( \Throwable $e ) {
701 // swallow and fall through.
702 }
703 $cache[ $key ] = null;
704 return $cache[ $key ];
705 }
706 }
707