db = $db ?? $GLOBALS['wpdb']; $this->table = $table ?? new TokenTable( $this->db ); } /** * Issuing a token and storing its digest. * * Expired rows are purged first, so the table needs no scheduled cleanup. * * @return string|null The token to hand to the client, or null when it could not be stored. */ public function issue(): ?string { $this->purge_expired(); $token = TokenFactory::create(); $inserted = $this->db->insert( $this->table->name(), array( 'token_hash' => TokenFactory::hash( $token ), 'expires_at' => Dates::utc_in( Config::TOKEN_TTL ), ), array( '%s', '%s' ) ); return false === $inserted ? null : $token; } /** * Checking whether a token is currently valid. * * @param string $token Token as received from the client. * * @return bool Whether the token exists and has not expired. */ public function is_valid( string $token ): bool { if ( ! TokenFactory::has_valid_shape( $token ) ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Authentication lookup against a custom table; caching a credential check would defeat expiry. $found = $this->db->get_var( $this->db->prepare( 'SELECT id FROM %i WHERE token_hash = %s AND expires_at > %s LIMIT 1', $this->table->name(), TokenFactory::hash( $token ), Dates::now_utc() ) ); return null !== $found; } /** * Deleting tokens whose expiry has passed. * * @return int Number of rows removed. */ public function purge_expired(): int { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Housekeeping on a custom table; there is no core API and nothing to cache. $removed = $this->db->query( $this->db->prepare( 'DELETE FROM %i WHERE expires_at <= %s', $this->table->name(), Dates::now_utc() ) ); return false === $removed ? 0 : (int) $removed; } /** * Revoking a single token. * * @param string $token Token to revoke. * * @return bool Whether a row was removed. */ public function revoke( string $token ): bool { if ( ! TokenFactory::has_valid_shape( $token ) ) { return false; } $removed = $this->db->delete( $this->table->name(), array( 'token_hash' => TokenFactory::hash( $token ) ), array( '%s' ) ); return is_int( $removed ) && $removed > 0; } /** * Revoking every token. * * @return int Number of rows removed. */ public function revoke_all(): int { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk revocation on a custom table; no core API, nothing to cache. $removed = $this->db->query( $this->db->prepare( 'DELETE FROM %i', $this->table->name() ) ); return false === $removed ? 0 : (int) $removed; } }