blog_id ) || ! self::is_network_active() ) { return; } self::switched( (int) $site->blog_id, [ self::class, 'activate_site' ] ); } /** * Seeds keys, installs storage, schedules cron and asks for a rewrite flush. * * The per-site half of `activate()`: everything it touches is one site's. * * @return void */ private static function activate_site(): void { if ( '' === (string) Options::get( 'indexnow.key', '' ) ) { Options::set( 'indexnow.key', self::generate_indexnow_key() ); } if ( '' === (string) Options::get( 'security.expires', '' ) ) { Options::set( 'security.expires', gmdate( 'Y-m-d\TH:i:s\Z', time() + YEAR_IN_SECONDS ) ); } if ( class_exists( Citations\Store::class ) ) { Citations\Store::install(); } if ( ! wp_next_scheduled( 'acid_aeo_daily' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'acid_aeo_daily' ); } update_option( Upgrader::OPTION, ACID_AEO_VERSION, false ); update_option( 'acid_aeo_flush_needed', 1 ); } /** * Clears one site's scheduled work and rewrite rules. * * The order matters: `Endpoints\Router::remove_rewrite_rules()` takes the * plugin's rules out of the in-memory table, and only then is the flush * that writes the table back safe to run. * * @return void */ private static function deactivate_site(): void { wp_clear_scheduled_hook( 'acid_aeo_daily' ); wp_clear_scheduled_hook( 'acid_aeo_indexnow_flush' ); wp_clear_scheduled_hook( Modules\StaticFiles::CRON_HOOK ); Endpoints\Router::remove_rewrite_rules(); flush_rewrite_rules( false ); } /** * Runs one callback once per site on the network. * * `number => 0` asks for every site rather than the first hundred. * * @param callable $callback Per-site routine. * @return void */ private static function for_each_site( callable $callback ): void { $site_ids = get_sites( [ 'fields' => 'ids', 'number' => 0, ] ); foreach ( $site_ids as $site_id ) { self::switched( (int) $site_id, $callback ); } } /** * Runs one callback against one site and comes back. * * The `finally` is the point. `switch_to_blog()` pushes onto a stack that * only `restore_current_blog()` pops, so a callback that throws — a * database error during `dbDelta()`, a fatal in somebody's filter — would * otherwise leave every later query, and the rest of the request, pointed * at a site nobody asked for. Whatever happens in the body, the switch is * unwound. * * The two memos go on both hops because they belong to the site they were * read from: `Options` holds one site's settings, and `Cache` holds one * site's cache version and whether it has already been bumped. Carrying * either across a switch would have one site's answers read as another's. * * @param int $site_id Site to switch to. * @param callable $callback Routine to run there. * @return void */ private static function switched( int $site_id, callable $callback ): void { switch_to_blog( $site_id ); Options::reset(); Cache::reset(); try { call_user_func( $callback ); } finally { restore_current_blog(); Options::reset(); Cache::reset(); } } /** * Whether the plugin is activated for the whole network. * * @return bool True when it is network-active. */ private static function is_network_active(): bool { if ( ! is_multisite() ) { return false; } if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return is_plugin_active_for_network( plugin_basename( ACID_AEO_FILE ) ); } /** * Generates the IndexNow key. * * IndexNow accepts any 8 to 128 character key, so a hosting environment * without a working CSPRNG falls back to WordPress' own generator rather * than failing activation. * * @return string A 32 character key. */ private static function generate_indexnow_key(): string { try { return bin2hex( random_bytes( 16 ) ); } catch ( \Exception $e ) { return strtolower( wp_generate_password( 32, false ) ); } } }