clamp( (int) get_option( self::OPTION_DAYS, 0 ) ); } /** * Set the window, clamped to a range the pruner can act on. * * @param int $days Requested window. */ public function set_retention_days( int $days ): void { update_option( self::OPTION_DAYS, $this->clamp( $days ) ); } /** * Remove entries older than the window. * * @param DateTimeImmutable|null $now Reference time. * @param int|null $days One-off window; null uses the stored setting. * @return int Number of entries removed. */ public function prune( ?DateTimeImmutable $now = null, ?int $days = null ): int { list( $cutoff, $doomed ) = $this->plan( $now, $days ); if ( null === $cutoff || array() === $doomed ) { return 0; } // Anchor to the last removed row before it goes: verification resumes // from this hash instead of GENESIS. $anchor = $this->ledger->chain_hash_of( (int) end( $doomed ) ); foreach ( $doomed as $id ) { $this->snapshots->delete_by_ledger( (int) $id ); } $removed = $this->ledger->delete_ids( $doomed ); if ( null !== $anchor ) { update_option( LedgerWriter::OPTION_ANCHOR, $anchor ); } $this->record( $removed, $cutoff, $anchor ); return $removed; } /** * What a prune would remove, without removing it. * * @param DateTimeImmutable|null $now Reference time. * @param int|null $days One-off window; null uses the stored setting. * @return array{count: int, cutoff: string|null} */ public function preview( ?DateTimeImmutable $now = null, ?int $days = null ): array { list( $cutoff, $doomed ) = $this->plan( $now, $days ); return array( 'count' => count( $doomed ), 'cutoff' => null === $cutoff ? null : $cutoff->format( 'Y-m-d H:i:s' ), ); } /** * Work out the cutoff and the entries a prune would take. * * Shared by prune() and preview() so a dry run cannot describe something * different from what the real thing would do. * * @param DateTimeImmutable|null $now Reference time. * @param int|null $days One-off window; null uses the stored setting. * @return array{0: DateTimeImmutable|null, 1: list} */ private function plan( ?DateTimeImmutable $now, ?int $days ): array { $window = $this->clamp( $days ?? $this->retention_days() ); if ( 0 === $window ) { return array( null, array() ); } $cutoff = ( $now ?? new DateTimeImmutable( 'now' ) )->modify( sprintf( '-%d days', $window ) ); return array( $cutoff, $this->ledger->prunable_prefix( $cutoff, $this->pending->ledger_ids_awaiting_decision(), self::BATCH ), ); } /** * Write the prune's own ledger entry, chained onto the survivors. * * @param int $removed Rows removed. * @param DateTimeImmutable $cutoff Cutoff applied. * @param string|null $anchor Chain hash re-anchored to. */ private function record( int $removed, DateTimeImmutable $cutoff, ?string $anchor ): void { $this->writer->append( array( 'ts' => new DateTimeImmutable( 'now' ), // Retention is the site's own housekeeping, not an agent's // action; 0 is the system actor. 'agent_id' => 0, 'wp_user_id' => 0, 'source' => 'system', 'action_cat' => 'delete', 'entity_type' => 'ledger', 'status' => 'allowed', 'input' => array( 'pruned' => $removed, 'cutoff' => $cutoff->format( 'Y-m-d H:i:s' ), 'anchor' => $anchor, ), ) ); } /** * Keep the window inside a range the pruner can act on. * * @param int $days Requested window. */ private function clamp( int $days ): int { if ( $days < 0 ) { return 0; } return min( $days, self::MAX_DAYS ); } }