upgrader = $upgrader; $this->scan_cron = $scan_cron; } /** * Hook the lifecycle callbacks. * * @param string $plugin_file Absolute path of the main plugin file. */ public function register( string $plugin_file ): void { $this->basename = plugin_basename( $plugin_file ); $this->register_file( $plugin_file ); // Deactivating the sibling package unschedules the events this package needs, and its own activation may not have run. add_action( 'init', array( $this, 'ensure_scheduled' ) ); // After core has created the new site's own tables and options (priority 100). add_action( 'wp_initialize_site', array( $this, 'activate_new_site' ), 900 ); add_filter( 'wpmu_drop_tables', array( $this, 'add_tables_to_drop' ), 10, 2 ); } /** * Hook the lifecycle of one package's main file: the sibling package's file too, because only one of them boots. * * Its uninstall hook is registered here, ahead of the SDK's, which only exists once an administrator deactivated * the plugin in wp-admin. {@see Uninstaller::handle_uninstall()} hands the SDK its part of the job back. * * @param string $plugin_file Absolute path of a main plugin file. */ public function register_file( string $plugin_file ): void { register_activation_hook( $plugin_file, function ( bool $network_wide = false ) use ( $plugin_file ): void { $this->activate( $network_wide ); register_uninstall_hook( $plugin_file, array( Uninstaller::class, 'handle_uninstall' ) ); } ); register_deactivation_hook( $plugin_file, array( $this, 'deactivate' ) ); // After the SDK's own deactivation hook has put its uninstall hook in the same slot. add_action( 'deactivated_plugin', static function ( string $plugin ) use ( $plugin_file ): void { if ( plugin_basename( $plugin_file ) === $plugin ) { register_uninstall_hook( $plugin_file, array( Uninstaller::class, 'handle_uninstall' ) ); } }, 99 ); } /** * Bring the prune event back when it is missing: deactivating one package of the plugin while the other stays active * removes it. An earlier version's recurring scan event goes. Never throws on a request. */ public function ensure_scheduled(): void { try { $this->schedule_prune(); $this->scan_cron->remove_recurring(); } catch ( RuntimeException $e ) { wp_trigger_error( __METHOD__, $e->getMessage() ); } } /** * Install on the current site, or on every site when network-activated. * * @param bool $network_wide Whether the plugin is being network-activated. */ public function activate( bool $network_wide = false ): void { $this->for_each_site( $network_wide, array( $this, 'activate_site' ) ); } /** * Unschedule the plugin's events on the current site, or on every site when network-deactivated. * Data stays: only uninstalling may remove it. * * @param bool $network_wide Whether the plugin is being network-deactivated. */ public function deactivate( bool $network_wide = false ): void { $this->for_each_site( $network_wide, array( $this, 'deactivate_site' ) ); } /** * Install on a freshly created site when the plugin is network-active. * * @param WP_Site $new_site The new site. */ public function activate_new_site( WP_Site $new_site ): void { if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( ! is_plugin_active_for_network( $this->basename ) ) { return; } switch_to_blog( (int) $new_site->blog_id ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog -- Installing a site's own tables. try { $this->activate_site(); } finally { restore_current_blog(); } } /** * Have core drop our tables together with a deleted site's. * * @param array $tables Tables core is about to drop. * @param int $site_id Site being deleted. * @return array */ public function add_tables_to_drop( array $tables, int $site_id ): array { global $wpdb; $prefix = $wpdb->get_blog_prefix( $site_id ); foreach ( array_merge( Schema::tables( $prefix ), Schema::extension_tables( $prefix ) ) as $table ) { $tables[ $table ] = $table; } return $tables; } /** * Install on the current site. * * @throws RuntimeException When the tables or the prune event cannot be created. */ private function activate_site(): void { $this->upgrader->run(); $this->schedule_prune(); $this->scan_cron->remove_recurring(); } /** * Schedule the daily prune on the current site unless it is. * * @throws RuntimeException When the event cannot be created. */ private function schedule_prune(): void { if ( false !== wp_next_scheduled( self::PRUNE_HOOK ) ) { return; } $scheduled = wp_schedule_event( time(), 'daily', self::PRUNE_HOOK, array(), true ); if ( is_wp_error( $scheduled ) ) { throw new RuntimeException( esc_html( $scheduled->get_error_message() ) ); } } /** * Unschedule on the current site. */ private function deactivate_site(): void { wp_unschedule_hook( self::PRUNE_HOOK ); $this->scan_cron->unschedule(); self::unschedule_extension_events(); } /** * Unschedule the cron events add-on code scheduled on the current site: every hook named * `consentfold_ext_*`. They are found by name, so no add-on code has to be loaded. */ public static function unschedule_extension_events(): void { $crons = _get_cron_array(); foreach ( is_array( $crons ) ? $crons : array() as $events ) { foreach ( array_keys( $events ) as $hook ) { if ( str_starts_with( (string) $hook, 'consentfold_ext_' ) ) { wp_unschedule_hook( (string) $hook ); } } } } /** * Run a callback on the current site, or on each site of the network. * * @param bool $network_wide Whether to visit every site (multisite only). * @param callable $callback Called with the site switched in. */ private function for_each_site( bool $network_wide, callable $callback ): void { if ( ! $network_wide || ! is_multisite() ) { $callback(); return; } foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog -- Network-wide install touches every site. try { $callback(); } finally { restore_current_blog(); } } } }