10, 'per_ip' => 30, 'per_site' => 300, 'uploads_per_ip' => 60, 'monthly_budget' => '10', 'estimate' => '0.07', ); /** * Request storage. * * @var Requests */ private $requests; /** * Upload counter. * * @var Throttle */ private $throttle; /** * Normalised settings. * * @var array */ private $config; /** * Returns the current Unix time. * * @var callable */ private $clock; /** * Creates the limits. * * @param Requests $requests Request storage. * @param Throttle $throttle Upload counter. * @param array $config From normalize(). * @param callable $clock Returns the current Unix time. */ public function __construct( Requests $requests, Throttle $throttle, array $config, callable $clock ) { $this->requests = $requests; $this->throttle = $throttle; $this->config = $config; $this->clock = $clock; } /** * Settings with defaults for anything missing or unreadable. * * @param mixed $config Saved or filtered settings. * @return array{per_visitor: int, per_ip: int, per_site: int, uploads_per_ip: int, monthly_budget_micros: int, estimate_micros: int} */ public static function normalize( $config ): array { $config = is_array( $config ) ? $config : array(); $count = static function ( string $key ) use ( $config ): int { $value = $config[ $key ] ?? self::DEFAULTS[ $key ]; return is_numeric( $value ) ? max( 0, (int) $value ) : (int) self::DEFAULTS[ $key ]; }; $money = static function ( string $key ) use ( $config ): int { $value = $config[ $key ] ?? self::DEFAULTS[ $key ]; return is_numeric( $value ) ? Money::to_micros( is_string( $value ) ? $value : (float) $value ) : Money::to_micros( self::DEFAULTS[ $key ] ); }; return array( 'per_visitor' => $count( 'per_visitor' ), 'per_ip' => $count( 'per_ip' ), 'per_site' => $count( 'per_site' ), 'uploads_per_ip' => $count( 'uploads_per_ip' ), 'monthly_budget_micros' => $money( 'monthly_budget' ), 'estimate_micros' => $money( 'estimate' ), ); } /** * Why a new try-on may not start, or null when it may. * * @param string $visitor Visitor hash. * @param string $ip IP hash, '' when unknown. */ public function tryon_problem( string $visitor, string $ip ): ?string { $now = $this->now(); $since = gmdate( 'Y-m-d H:i:s', $now - self::WINDOW ); if ( $this->reached( ( $this->config['per_visitor'] ?? 0 ), $this->requests->count_created_since( $since, 'visitor_hash', $visitor ) ) ) { return self::VISITOR; } if ( '' !== $ip && $this->reached( ( $this->config['per_ip'] ?? 0 ), $this->requests->count_created_since( $since, 'ip_hash', $ip ) ) ) { return self::IP; } if ( $this->reached( ( $this->config['per_site'] ?? 0 ), $this->requests->count_created_since( $since ) ) ) { return self::SITE; } $budget = $this->config['monthly_budget_micros'] ?? 0; if ( $budget > 0 ) { $spend = $this->requests->spend_since( self::month_start( $now ) ); $estimate = $this->config['estimate_micros'] ?? 0; if ( $spend['micros'] + ( $spend['unpriced'] + 1 ) * $estimate > $budget ) { return self::BUDGET; } } return null; } /** * Counts a photo upload; false when this IP uploaded too many in the window. * * @param string $ip IP hash, '' when unknown. */ public function allow_upload( string $ip ): bool { if ( '' === $ip ) { return true; } return $this->throttle->hit( 'fitzaro_up_' . substr( $ip, 0, 40 ), $this->config['uploads_per_ip'] ?? 0, self::WINDOW ); } /** * Start of the calendar month of $now, in UTC. * * @param int $now Unix time. */ public static function month_start( int $now ): string { return gmdate( 'Y-m-01 00:00:00', $now ); } /** * Whether a count reached a limit (0 = unlimited). * * @param int $limit Limit. * @param int $count Current count. */ private function reached( int $limit, int $count ): bool { return $limit > 0 && $count >= $limit; } /** * Current Unix time. */ private function now(): int { return (int) ( $this->clock )(); } }