ActivityLog::get_events( $args ), Database::get_table_name(), static function (): array { return array( 'retention_days' => ActivityLog::retention_days_or_default( get_option( 'ops_activity_log_retention_days', ActivityLog::DEFAULT_RETENTION_DAYS ) ), 'store_full_ips' => (bool) get_option( EventLogger::OPTION_STORE_FULL_IPS, false ), 'ip_source' => EventLogger::ip_source(), ); }, // The sixth parameter (who acted) since the login-alerts slice: // an alert is written during wp_login, before the current user is // set, and a "this wasn't me" report from a logged-out browser -- // both are the user's own events, exactly as their login is. // Optional, so a caller passing five is unchanged. static function ( string $event_type, string $summary, ?string $object_type = null, ?int $object_id = null, ?array $details = null, ?int $actor_user_id = null ): void { EventLogger::log( $event_type, $summary, $object_type, $object_id, $details, $actor_user_id ); } ); } /** * Register admin settings hooks after WordPress has initialized translations. * * Public because WordPress invokes this named action callback. */ public static function register_admin(): void { if ( self::$admin_registered ) { return; } self::$admin_registered = true; ( new SettingsPage() )->register(); // Network Admin → Activity Log (#8): super admins, network-active only. if ( Network\NetworkLog::is_active() && is_network_admin() ) { Admin\NetworkPage::register(); } } /** * Remove all plugin data: the activity log table and every option. * Called from uninstall.php (WP_UNINSTALL_PLUGIN context) — never from * deactivation, which must leave data intact for reactivation. * * On a network (#8) uninstall.php runs ONCE, on the main site, however * the plugin was activated: so every site's table and options are * removed site by site, then the network log's table and its network * option. A site the plugin never ran on has nothing to remove, and * each removal is a no-op there. Before #8 only the main site was * cleaned, leaving every other site's table behind. */ public static function uninstall(): void { if ( ! function_exists( 'is_multisite' ) || ! is_multisite() ) { self::uninstall_site(); return; } // Every site of every network (get_sites() reads all networks unless // given one): the network table is one per install. foreach ( get_sites( array( 'fields' => 'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); try { self::uninstall_site(); } finally { restore_current_blog(); } } Network\NetworkDatabase::drop_table(); } /** * One site's data: its table and its options. */ private static function uninstall_site(): void { Database::drop_table(); delete_option( 'ops_activity_log_retention_days' ); delete_option( 'ops_activity_log_retention_unlimited' ); delete_option( EventLogger::OPTION_STORE_FULL_IPS ); delete_option( EventLogger::OPTION_IP_SOURCE ); delete_option( Exclusions::OPTION_USERS ); delete_option( Exclusions::OPTION_ROLES ); delete_option( Alerts::OPTION_ENABLED ); delete_option( AiSummary::OPTION_AI_ENABLED ); delete_option( Sessions\SessionPolicy::OPTION ); delete_option( Importer::STATE_OPTION ); // Location and device (#7): the settings, the encrypted licence key, // and the database this plugin installed. A database the owner // pointed at elsewhere on the server is theirs and stays. delete_option( Context\RequestContext::OPTION_DEVICE ); delete_option( Context\GeoUpdater::OPTION_ENABLED ); delete_option( Context\GeoUpdater::OPTION_ACCOUNT ); delete_option( Context\GeoUpdater::OPTION_KEY ); delete_option( Context\GeoUpdater::OPTION_EDITION ); delete_option( Context\GeoUpdater::OPTION_STATUS ); delete_option( Context\GeoUpdater::OPTION_LAST_SUCCESS ); Context\GeoDatabase::remove_managed_dir(); delete_option( Context\GeoDatabase::OPTION_PATH ); // File integrity monitoring: its table and options. The scanned // files were only ever read. Integrity\IntegrityActions::uninstall(); // The chain's signed state and last verdict. Chain\ChainActions::uninstall(); } /** * Handle plugin deactivation. */ public static function on_deactivate(): void { // Clear every instance, including duplicates left by older deployments. wp_clear_scheduled_hook( 'ops_activity_log_daily_cron' ); // A running import's next batch (#21). Its events carry the source as // an argument, which wp_clear_scheduled_hook() would have to match; // wp_unschedule_hook() clears every one. The run's state is kept, so // reactivating and pressing Import carries on from its cursor. wp_unschedule_hook( ImportActions::CRON_HOOK ); // A running integrity scan's continuation. Its position is kept, so // "Run a batch now" after reactivating carries on from it. wp_unschedule_hook( Integrity\IntegrityActions::CRON_HOOK ); // A running verification's continuation; its position is kept. wp_unschedule_hook( Chain\ChainActions::CRON_HOOK ); } /** * Schedule daily cron job. */ private static function schedule_cron(): void { if ( ! wp_next_scheduled( 'ops_activity_log_daily_cron' ) ) { wp_schedule_event( time(), 'daily', 'ops_activity_log_daily_cron' ); } add_action( 'ops_activity_log_daily_cron', array( __CLASS__, 'run_daily_cron' ) ); } /** * Run daily cron tasks. */ public static function run_daily_cron(): void { // Purge old events based on retention setting. $retention_days = ActivityLog::retention_days_or_default( get_option( 'ops_activity_log_retention_days', ActivityLog::DEFAULT_RETENTION_DAYS ) ); ActivityLog::purge_old_events( $retention_days ); // Send the daily digest. EmailDigest honours the site's off-switch. EmailDigest::send_daily_digest(); // The optional weekly GeoLite2 download (#7). A no-op unless the // owner switched it on and gave it their own MaxMind credentials. Context\GeoUpdater::maybe_update(); // The daily file integrity scan, only when the owner switched it on. Integrity\IntegrityActions::daily(); // The daily verification of the log's chain. Chain\ChainActions::daily(); } }