'404ops-activity-log', 'tab' => 'integrity', self::RESULT_ARG => $result, ), admin_url( 'admin.php' ) ) ); exit; } /** * Carry out one posted operation. * * @param array $request The posted fields, unslashed. * @return string A RESULTS code. */ public static function perform( array $request ): string { $op = isset( $request['op'] ) && is_string( $request['op'] ) ? sanitize_key( $request['op'] ) : ''; $target = isset( $request['target'] ) && is_scalar( $request['target'] ) ? substr( (string) $request['target'], 0, 200 ) : ''; $nonce = isset( $request['_wpnonce'] ) && is_string( $request['_wpnonce'] ) ? sanitize_text_field( $request['_wpnonce'] ) : ''; $ops = array( self::OP_START, self::OP_BATCH, self::OP_CANCEL, self::OP_ACCEPT, self::OP_ACCEPT_UNIT, self::OP_SAVE ); if ( ! in_array( $op, $ops, true ) || ! self::available() ) { return 'bad_request'; } if ( false === wp_verify_nonce( $nonce, self::nonce_action( $op, $target ) ) ) { return 'bad_nonce'; } if ( ! current_user_can( self::capability() ) ) { return 'forbidden'; } switch ( $op ) { case self::OP_START: // Starting does no scanning: the cron continuation, or "Run // a batch now", does -- so the press returns at once. $result = Scanner::for_site()->start( 'manual' ); if ( 'started' === $result ) { self::schedule(); } return $result; case self::OP_BATCH: if ( ! Scanner::is_running() ) { return 'not_running'; } return self::advance( Scanner::for_site(), self::STEP_SECONDS ) ? 'batch' : 'finished'; case self::OP_CANCEL: wp_clear_scheduled_hook( self::CRON_HOOK ); return Scanner::cancel() ? 'cancelled' : 'not_running'; case self::OP_ACCEPT: return Review::accept( (int) $target, Roots::from_wordpress() ); case self::OP_ACCEPT_UNIT: return Review::accept_unit( $target ); default: return self::save( $request ); } } /** * Run a scan for a while; true while it is still running. * * @param Scanner $scanner The scanner. * @param float $seconds Budget. */ public static function advance( Scanner $scanner, float $seconds ): bool { return Scanner::RUNNING === $scanner->run( $seconds )['status']; } /** * @param array $request The posted fields. */ private static function save( array $request ): string { $was = self::auto_enabled(); $now = isset( $request['auto'] ); // '1'/'0', never a bare false (OPERATIONS.md: update_option() of // false on an absent option writes nothing). update_option( self::AUTO_OPTION, $now ? '1' : '0', false ); if ( $was !== $now ) { EventLogger::log( Scanner::EVENT_SETTINGS, $now ? 'Daily file integrity scan switched on' : 'Daily file integrity scan switched off', 'integrity', null, array( 'daily_scan' => $now ) ); } return 'saved'; } /** Whether the owner switched the daily scan on. */ public static function auto_enabled(): bool { return '1' === (string) get_option( self::AUTO_OPTION, '0' ); } /** * The daily cron's part: start a scan when the owner asked for daily * scans and none is running. */ public static function daily(): void { // The switch first: it is off on almost every site, and reading it // costs nothing the daily cron does not already pay. if ( ! self::auto_enabled() || ! self::available() ) { return; } if ( 'started' === Scanner::for_site()->start( 'schedule' ) ) { self::schedule(); } } /** WP-Cron continuation: run for a while, and come back while there is more. */ public static function cron(): void { if ( ! self::available() ) { return; } if ( self::advance( Scanner::for_site(), self::CRON_SECONDS ) ) { self::schedule(); } } private static function schedule(): void { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_single_event( time(), self::CRON_HOOK ); } } /** Remove everything this feature stored. Uninstall only. */ public static function uninstall(): void { Store::drop_table(); foreach ( Scanner::OPTIONS as $option ) { delete_option( $option ); } delete_option( self::AUTO_OPTION ); } }