log_events( 'unhooks', '[hook_remove_unwanted_hooks] :: [page=' . ( isset( $_GET['page'] ) ? esc_html( wp_unslash( $_GET['page'] ) ) : '' ) . ']' ); //phpcs:ignore -- ok. $this->remove_slow_hooks(); } /** * Default mainwp pages to remove hooks. */ public function hook_remove_unwanted_hooks_default_pages() { if ( ! MainWP_System::is_mainwp_pages() ) { return; } if ( static::is_request_unhooks() ) { // requested unhooks. return; } $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore -- ok. if ( $this->is_excluded_pages( $page ) ) { return; } /** * Hook default mainwp pages to remove unwanted hooks. * * @since 5.5. */ $remove = apply_filters( 'mainwp_unhooks_remove_unwanted_hooks', true, 'default_pages' ); //phpcs:ignore -- ok. if ( $remove ) { MainWP_Logger::instance()->log_events( 'unhooks', '[hook_remove_unwanted_hooks_default_pages] :: [page=' . $page . ']' ); //phpcs:ignore -- ok. $this->remove_slow_hooks(); } } /** * Method is_excluded_pages(). * * @param string $page Excluded page check. * * @return bool Excluded page or not */ public function is_excluded_pages( $page = '' ) { if ( empty( $page ) || in_array( $page, (array) $this->get_default_excluded_pages() ) ) { return true; } return false; } /** * Method is_enabled_unhooks(). * * @return bool Enabled or not. */ public static function is_enabled_unhooks() { static $enabled; if ( null === $enabled ) { /** * Enable remove unwanted hooks or not. * * Default: false. * * @since 5.5. */ $enabled = apply_filters( 'mainwp_unhooks_is_enabled', false ); //phpcs:ignore -- ok. } return $enabled ? true : false; } /** * Hook shutdown. */ public function hook_unhook_shutdown() { if ( static::is_enabled_unhooks() && ( ! $this->is_excluded_pages() || static::is_request_unhooks() ) ) { $rt = MainWP_Execution_Helper::get_run_time(); MainWP_Logger::instance()->log_events( 'unhooks', '[Execution time=' . $rt . '] (seconds)', MainWP_Logger::INFO ); // phpcs:ignore -- ok. MainWP_Logger::instance()->log_events( 'execution-time', '[Unhooks] :: [runtime=' . $rt . '] (seconds)', MainWP_Logger::INFO ); // phpcs:ignore -- ok. } } /** * Verify unhooks request. * * @return bool Unhooks or not. */ public static function is_request_unhooks() { if ( ! isset( $_GET['_uhnonce'] ) || ! wp_verify_nonce( (string) $_GET['_uhnonce'] , 'unhooks_nonce' ) ) { // phpcs:ignore -- ok. return false; } return true; } /** * Method get_unhooks_active_plugins(). * * @return array Active non mainwp plugins. */ public function get_unhooks_active_plugins() { if ( null === $this->active_plugins ) { $this->active_plugins = array(); $mainwp_str = 'mainwp'; $plugins_files = get_option( 'active_plugins', array() ); if ( is_array( $plugins_files ) ) { foreach ( $plugins_files as $indx => $plugin_file ) { if ( false === stripos( $plugin_file, $mainwp_str ) ) { $this->active_plugins[ $indx ] = $plugin_file; } } } /** * Get unhooks active plugins files. * * @since 5.5. */ $this->active_plugins = apply_filters( 'mainwp_unhooks_active_plugins_files', $this->active_plugins ); } return $this->active_plugins; } /** * Get excluded pages. */ public function get_default_excluded_pages() { /** * Hook get default excluded pages. * * @since 5.5. */ return apply_filters( 'mainwp_unhooks_default_excluded_pages', array( 'ActionLogs', ) ); } /** * Remove slow hooks. */ public function remove_slow_hooks() { // phpcs:ignore -- NOSONAR -complex. $remove_desired_hooks = $this->unhooks_list(); $unhooks_types = $this->get_unhooks_types(); if ( is_array( $unhooks_types ) ) { foreach ( $unhooks_types as $type => $values ) { switch ( $type ) { case 'all_exclude_mainwp': case 'files_names': $files = array(); if ( 'files_names' === $type ) { $files = $values; } elseif ( 'all_exclude_mainwp' === $type ) { if ( true === $values ) { $files = $this->get_unhooks_active_plugins(); } } if ( is_array( $files ) && ! empty( $files ) ) { foreach ( $remove_desired_hooks as $hook ) { $this->remove_by_file( $hook, $files ); } } break; case 'plugin_names': $names = $values; if ( is_array( $names ) && ! empty( $names ) ) { foreach ( $remove_desired_hooks as $hook ) { $this->remove_by_name( $hook, $names ); } } break; case 'class_names': $cls_names = $values; if ( is_array( $cls_names ) && ! empty( $cls_names ) ) { foreach ( $remove_desired_hooks as $hook ) { $this->remove_by_class( $hook, $cls_names ); } } break; case 'all_specific': $hooks = $values; if ( is_array( $hooks ) ) { foreach ( $hooks as $hook ) { $this->remove_all( $hook ); } } break; default: // Nothing. break; } } } } /** * Get un-hooks list. * * @param string $remove Which hooks list. * * @return array */ public function unhooks_list( $remove = 'main' ) { $main_hooks = array( 'admin_init', 'init', 'plugins_loaded', ); $other_hooks = array( 'admin_footer', 'admin_head', 'admin_menu', 'plugin_loaded', 'wp_head', 'wp_loaded', 'wp_footer', 'admin_bar_init', 'admin_bar_menu', 'admin_enqueue_scripts', 'admin_notices', 'admin_print_footer_scripts', 'admin_print_scripts', 'admin_print_styles', 'after_setup_theme', 'all_admin_notices', 'current_screen', 'get_header', 'load_textdomain', 'muplugins_loaded', 'pre_get_posts', 'setup_theme', 'wp', 'wp_default_scripts', 'wp_default_styles', 'wp_enqueue_scripts', 'wp_print_footer_scripts', 'wp_print_scripts', 'wp_print_styles', 'wp_print_scripts', ); /** * Hook remove hooks list. * * @since 5.5. */ return apply_filters( 'mainwp_unhooks_list', 'main' === $remove ? $main_hooks : array_unique( array_merge( $main_hooks, $other_hooks ) ) ); } /** * Get un-hooks types. * * @return array */ public function get_unhooks_types() { /** * Hook get unhooks types. * * @since 5.5. */ return apply_filters( 'mainwp_unhooks_get_types', array( 'all_exclude_mainwp' => true, 'files_names' => array(), 'plugin_names' => array(), 'class_names' => array(), 'all_specific' => false, ) ); } /** * Add url params to unhook. * * @param string $url Url to enable un-hooks. * * @return string Url with unhook nonce. */ public static function add_unhooks_params( $url ) { if ( ! static::is_enabled_unhooks() ) { return $url; } $url_unhook = $url . '&_uhnonce=' . wp_create_nonce( 'unhooks_nonce' ); /** * Hook add unhooks url params. * * @since 5.5. */ return apply_filters( 'mainwp_unhooks_url_params', $url_unhook, $url ); // default disable. } /** * Remove callbacks on a hook where the callable's function/method name matches a pattern * * @param mixed $hook The hook. * @param mixed $names Array of classes names. * @return int */ private function remove_by_name( $hook, $cls_names = array() ) { // phpcs:ignore -- NOSONAR -complex. global $wp_filter; $removed = 0; $failed = 0; if ( empty( $wp_filter[ $hook ] ) ) { return 0; } $hook_obj = $wp_filter[ $hook ]; $callbacks = $hook_obj->callbacks ?? array(); foreach ( $callbacks as $priority => $list ) { foreach ( $list as $entry ) { $callable = $entry['function'] ?? null; $name = $this->callable_label( $callable ); if ( ! empty( $name ) ) { $found = false; foreach ( $cls_names as $find_name ) { if ( false !== stripos( $name, $find_name ) ) { $found = true; break; } } if ( $found ) { $success = remove_action( $hook, $callable, (int) $priority ); if ( $success ) { ++$removed; } else { ++$failed; } } } } } return $removed; } /** * Remove callbacks whose underlying file path matches a regex (good for targeting a plugin). * * Note: This is HEAVY function. * * @param mixed $hook The hook. * @param mixed $path_files The files path to check hooks. * @return int */ private function remove_by_file( $hook, $path_files ) { // phpcs:ignore -- NOSONAR -complex. global $wp_filter; if ( empty( $wp_filter[ $hook ] ) ) { return 0; } $removed = 0; $failed = 0; $hook_obj = $wp_filter[ $hook ]; $callbacks = $hook_obj->callbacks ?? array(); $plugins_dir = 'wp-content/plugins'; if ( defined( 'WP_PLUGIN_DIR' ) ) { $plugins_dir = str_replace( ABSPATH, '', WP_PLUGIN_DIR ); } $lists = array(); $lists[ $hook ] = array(); foreach ( $callbacks as $priority => $list ) { foreach ( $list as $entry ) { $callable = $entry['function'] ?? null; $file = $this->callable_file( $callable ); if ( ! empty( $file ) ) { $found = false; foreach ( $path_files as $dir_file ) { $path = '/' . $plugins_dir . '/' . MainWP_Utility::get_dir_slug( $dir_file ) . '/'; if ( false !== stripos( $file, $path ) ) { $found = true; break; } } if ( $found ) { $success = remove_action( $hook, $callable, (int) $priority ); if ( $success ) { ++$removed; $lists[ $hook ][] = $file; } else { ++$failed; } } } unset( $callable ); } } if ( $removed || $failed ) { MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by files] :: [hook=' . $hook . '] :: [removed=' . $removed . '] :: [failed=' . $failed . ']' ); //phpcs:ignore -- ok. MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by files] :: [lists=' . print_r($lists, true ) . ']' ); //phpcs:ignore -- ok. } return $removed; } /** * Remove callbacks where the callable belongs to a class/namespace regex. * * @param mixed $hook The hook. * @param mixed $classes_names The classes names. * @return int */ private function remove_by_class( $hook, $classes_names ) { // phpcs:ignore -- NOSONAR -complex. global $wp_filter; $removed = 0; $failed = 0; if ( empty( $wp_filter[ $hook ] ) ) { return 0; } $hook_obj = $wp_filter[ $hook ]; $callbacks = $hook_obj->callbacks ?? array(); foreach ( $callbacks as $priority => $list ) { foreach ( $list as $entry ) { $callable = $entry['function'] ?? null; if ( is_array( $callable ) ) { $obj_or_class = $callable[0]; $class = is_object( $obj_or_class ) ? get_class( $obj_or_class ) : $obj_or_class; if ( ! empty( $class ) ) { $found = false; foreach ( $classes_names as $find_class ) { if ( false !== stripos( $class, $find_class ) ) { $found = true; break; } } if ( $found ) { $success = remove_action( $hook, $callable, (int) $priority ); if ( $success ) { ++$removed; } else { ++$failed; } } } } } } if ( $removed || $failed ) { MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks by class :: [hook=' . $hook . '] :: [removed=' . $removed . '] :: [failed=' . $failed . ']' ); //phpcs:ignore -- ok. } return $removed; } /** * Remove everything on a hook (optionally only at a given priority). * Works for actions & filters. * * @param mixed $hook The hook. * @param mixed $priority Hook priority. * @return void */ private function remove_all( $hook, $priority = false ) { if ( false === $priority ) { remove_all_actions( $hook ); // wrapper for remove_all_filters. } else { remove_all_filters( $hook, (int) $priority ); } MainWP_Logger::instance()->log_events( 'unhooks', '[Unhooks all :: [hook=' . $hook . '] :: [priority=' . (false !== $priority ? (int)$priority : 'false') . ']' ); //phpcs:ignore -- ok. } /** * Human label for any callable. * * @param mixed $callback The callable. * @return string */ private function callable_label( $callback ) { if ( is_string( $callback ) ) { return $callback; } if ( is_array( $callback ) ) { $obj_or_class = $callback[0]; $meth = $callback[1] ?? ''; $class = is_object( $obj_or_class ) ? get_class( $obj_or_class ) : (string) $obj_or_class; return $class ? "{$class}::{$meth}" : $meth; } if ( $callback instanceof \Closure ) { return 'Closure'; } return null; } /** * Returns the source filename for a callable, or null if unknown. * * @param callable $callback * @return string|null */ private function callable_file( $callback ) { // phpcs:ignore -- NOSONAR -complex. static $cache = array(); // Build a stable cache key for all callable shapes. if ( $callback instanceof \Closure ) { $key = 'closure:' . spl_object_id( $callback ); } elseif ( is_string( $callback ) ) { $key = 'string:' . $callback; // "function" or "Class::method". } elseif ( is_array( $callback ) ) { $key = 'array:' . ( is_object( $callback[0] ) ? get_class( $callback[0] ) . '#' . spl_object_id( $callback[0] ) : $callback[0] ) . '::' . ( $callback[1] ?? '' ); } elseif ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { $key = 'invokable:' . get_class( $callback ) . '#' . spl_object_id( $callback ); } else { return null; } if ( array_key_exists( $key, $cache ) ) { return $cache[ $key ]; } try { if ( $callback instanceof \Closure ) { $ref = new \ReflectionFunction( $callback ); $cache[ $key ] = $ref->getFileName(); $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null; return $cache[ $key ]; } if ( is_string( $callback ) ) { // function or "Class::method". if ( strpos( $callback, '::' ) !== false ) { [$class, $method] = explode( '::', $callback, 2 ); $ref = new \ReflectionMethod( $class, $method ); $cache[ $key ] = $ref->getFileName(); $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null; return $cache[ $key ]; } else { // userland function. if ( ! function_exists( $callback ) ) { $cache[ $key ] = null; return $cache[ $key ]; } $ref = new \ReflectionFunction( $callback ); $cache[ $key ] = $ref->getFileName(); $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null; return $cache[ $key ]; } } if ( is_array( $callback ) ) { [$objOrClass, $method] = array( $callback[0], $callback[1] ?? '' ); $ref = new \ReflectionMethod( $objOrClass, $method ); $cache[ $key ] = $ref->getFileName(); $cache[ $key ] = $cache[ $key ] ?: null; return $cache[ $key ]; } if ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { $ref = new \ReflectionMethod( $callback, '__invoke' ); $cache[ $key ] = $ref->getFileName(); $cache[ $key ] = $cache[ $key ] ? $cache[ $key ] : null; return $cache[ $key ]; } } catch ( \Throwable $e ) { // swallow and fall through. } $cache[ $key ] = null; return $cache[ $key ]; } }