add: adding comments to code files

This commit is contained in:
Patrick Stewart 2024-07-04 21:20:02 -07:00
parent 7a65e161f0
commit 54a2ef0dbe
3 changed files with 963 additions and 71 deletions

View file

@ -1,39 +1,269 @@
/* /*
* This file is part of the VieoFabric package. * This file is part of the Protevus Platform.
* This file is a port of the symfony Cookie.php class to Dart
* *
* (c) Patrick Stewart <patrick@example.com> * (C) Protevus <developers@protevus.com>
* (C) Fabien Potencier <fabien@symfony.com>
* *
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
import 'header_utils.dart';
import 'dart:math'; import 'dart:math';
import 'header_utils.dart';
/// Represents a cookie. /// Represents an HTTP cookie.
///
/// This class encapsulates all the attributes and behaviors of an HTTP cookie,
/// including its name, value, expiration, path, domain, security settings,
/// and other properties like SameSite and Partitioned.
///
/// It provides methods to create, modify, and inspect cookie properties,
/// as well as to convert cookies to their string representation for use
/// in HTTP headers.
///
/// The Cookie class supports various cookie attributes and security features,
/// allowing for fine-grained control over cookie behavior in web applications.
class Cookie { class Cookie {
/// Constant representing the 'None' value for the SameSite cookie attribute.
///
/// When set to 'none', the cookie will be sent with all cross-site requests,
/// including both cross-site reading and cross-site writing. This setting
/// requires the 'Secure' flag to be set in modern browsers.
///
/// Note: Using 'none' may have security implications and should be used
/// carefully, as it allows the cookie to be sent in all contexts.
static const String SAMESITE_NONE = 'none'; static const String SAMESITE_NONE = 'none';
/// Constant representing the 'Lax' value for the SameSite cookie attribute.
///
/// When set to 'lax', the cookie will be sent with top-level navigations and
/// will be sent along with GET requests initiated by third party websites.
/// This is less restrictive than 'Strict' but provides some protection against
/// CSRF attacks while allowing the cookie to be sent in more scenarios.
///
/// This is often used as a balance between security and functionality.
static const String SAMESITE_LAX = 'lax'; static const String SAMESITE_LAX = 'lax';
/// Constant representing the 'Strict' value for the SameSite cookie attribute.
///
/// When set to 'strict', the cookie will only be sent for same-site requests.
/// This is the most restrictive setting and provides the highest level of protection
/// against cross-site request forgery (CSRF) attacks.
///
/// With this setting, the cookie will not be sent with any cross-site requests,
/// including when a user follows a link from an external site. This can enhance
/// security but may impact functionality in some cases where cross-site cookie
/// access is required.
static const String SAMESITE_STRICT = 'strict'; static const String SAMESITE_STRICT = 'strict';
/// The name of the cookie.
///
/// This property represents the name of the cookie, which is used to identify
/// the cookie when it's sent between the server and the client. The name is
/// a required field for any cookie and must be set when the cookie is created.
///
/// The 'late' keyword indicates that this property will be initialized before
/// it's used, but not necessarily at the point of declaration.
late String name; late String name;
/// The value of the cookie.
///
/// This property represents the actual data stored in the cookie. It can be null
/// if the cookie is used for deletion (setting an expired cookie) or if it's a
/// flag-type cookie where the presence of the cookie itself is meaningful.
///
/// The value is typically a string, but it's declared as nullable (String?) to
/// allow for cases where a cookie might be set without a value or to represent
/// a not-yet-initialized state.
String? value; String? value;
/// The domain that the cookie is available to.
///
/// This property represents the domain attribute of the cookie. When set, it specifies
/// which hosts are allowed to receive the cookie. It's important for controlling
/// the scope of the cookie, especially in scenarios involving subdomains.
///
/// If null, the cookie will only be sent to the host that set the cookie.
///
/// Note: The domain must be a valid domain string. Setting this incorrectly can
/// lead to security vulnerabilities or the cookie not being sent as expected.
String? domain; String? domain;
/// The expiration time of the cookie.
///
/// This property represents the time at which the cookie should expire, stored as a Unix timestamp
/// (seconds since the Unix epoch). When this time is reached, the cookie is considered expired
/// and should be discarded by the client.
///
/// The 'late' keyword indicates that this property will be initialized before it's used,
/// but not necessarily at the point of declaration. This allows for flexible initialization
/// patterns, such as setting the expiration time in a constructor or method after the object
/// is created.
///
/// A value of 0 typically indicates that the cookie does not have a specific expiration time
/// and should be treated as a session cookie (expires when the browsing session ends).
late int expire; late int expire;
/// The path on the server where the cookie will be available.
///
/// This property represents the path attribute of the cookie. It specifies the
/// subset of URLs in a domain for which the cookie is valid. When a cookie has a
/// path set, it will only be sent to the server for requests to that path and
/// its subdirectories.
///
/// The 'late' keyword indicates that this property will be initialized before
/// it's used, but not necessarily at the point of declaration. Typically, it's
/// set in the constructor or a method that initializes the cookie.
///
/// If not explicitly set, the default path is usually '/'.
late String path; late String path;
/// Indicates whether the cookie should only be transmitted over a secure HTTPS connection.
///
/// This property is nullable:
/// - If set to true, the cookie will only be sent over secure connections.
/// - If set to false, the cookie can be sent over any connection.
/// - If null, the default secure setting (secureDefault) will be used.
///
/// The actual security behavior is determined by the [isSecure] method,
/// which considers both this property and the [secureDefault] value.
bool? secure; bool? secure;
/// Indicates whether the cookie should be accessible only through the HTTP protocol.
///
/// When set to true, the cookie is not accessible to client-side scripts (such as JavaScript),
/// which helps mitigate cross-site scripting (XSS) attacks.
///
/// The 'late' keyword indicates that this property will be initialized before it's used,
/// but not necessarily at the point of declaration.
late bool httpOnly; late bool httpOnly;
/// Indicates whether the cookie should be sent with no URL encoding.
///
/// When set to true, the cookie name and value will not be URL-encoded when the cookie
/// is converted to a string representation. This can be useful in situations where
/// the cookie value contains characters that don't need to be encoded or when working
/// with systems that expect raw cookie values.
///
/// The 'late' keyword indicates that this property will be initialized before it's used,
/// but not necessarily at the point of declaration.
late bool raw; late bool raw;
/// The SameSite attribute of the cookie.
///
/// This property represents the SameSite attribute for the cookie, which controls how the cookie is sent with cross-site requests.
/// It can have one of three values:
/// - 'strict': The cookie is only sent for same-site requests.
/// - 'lax': The cookie is sent for same-site requests and top-level navigation from external sites.
/// - 'none': The cookie is sent for all cross-site requests.
/// - null: If the SameSite attribute is not set.
///
/// The SameSite attribute helps protect against cross-site request forgery (CSRF) attacks.
String? sameSite; String? sameSite;
/// Indicates whether the cookie should be tied to the top-level site in cross-site context.
///
/// When set to true, the cookie will be partitioned, meaning it will be associated with
/// the top-level site in cross-site contexts. This can enhance privacy and security by
/// preventing tracking across different sites.
///
/// The 'late' keyword indicates that this property will be initialized before it's used,
/// but not necessarily at the point of declaration.
late bool partitioned; late bool partitioned;
/// Indicates the default value for the "secure" flag when it's not explicitly set.
///
/// This property determines whether cookies should be marked as secure by default
/// when the [secure] property is null. If set to true, cookies will be treated
/// as secure unless explicitly set otherwise.
///
/// The 'late' keyword indicates that this property will be initialized before
/// it's used, but not necessarily at the point of declaration.
late bool secureDefault; late bool secureDefault;
/// A string containing characters that are reserved and cannot be used in cookie names.
///
/// This constant defines a list of characters that are considered reserved in the context of cookies.
/// These characters have special meanings in cookie syntax and therefore cannot be used directly
/// in cookie names, especially when the 'raw' flag is set to true.
///
/// The reserved characters are:
/// - '=': Used to separate cookie name and value
/// - ',': Used to separate multiple cookies
/// - ';': Used to separate cookie attributes
/// - ' ': Space character
/// - '\t': Tab character
/// - '\r': Carriage return
/// - '\n': Line feed
/// - '\v': Vertical tab
/// - '\f': Form feed
///
/// This constant is used in validation checks to ensure cookie names do not contain these characters
/// when creating or manipulating cookies with the 'raw' option enabled.
static const String RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; static const String RESERVED_CHARS_LIST = "=,; \t\r\n\v\f";
/// A list of reserved characters in their original form.
///
/// This constant defines a list of characters that are considered reserved in the context of cookies.
/// These characters have special meanings in cookie syntax and therefore need to be encoded
/// when used in cookie names or values.
///
/// The reserved characters are:
/// - '=': Used to separate cookie name and value
/// - ',': Used to separate multiple cookies
/// - ';': Used to separate cookie attributes
/// - ' ': Space character
/// - '\t': Tab character
/// - '\r': Carriage return
/// - '\n': Line feed
/// - '\v': Vertical tab
/// - '\f': Form feed
///
/// This list is typically used in conjunction with RESERVED_CHARS_TO for encoding/decoding
/// cookie names and values to ensure proper handling of these special characters.
static const List<String> RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; static const List<String> RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"];
/// A list of URL-encoded representations of reserved characters.
///
/// This constant defines a list of URL-encoded versions of characters that are considered
/// reserved in the context of cookies. These encoded versions correspond to the characters
/// in RESERVED_CHARS_FROM.
///
/// The encoded characters are:
/// - '%3D': Encoded form of '=' (equals sign)
/// - '%2C': Encoded form of ',' (comma)
/// - '%3B': Encoded form of ';' (semicolon)
/// - '%20': Encoded form of ' ' (space)
/// - '%09': Encoded form of '\t' (tab)
/// - '%0D': Encoded form of '\r' (carriage return)
/// - '%0A': Encoded form of '\n' (line feed)
/// - '%0B': Encoded form of '\v' (vertical tab)
/// - '%0C': Encoded form of '\f' (form feed)
///
/// This list is typically used in conjunction with RESERVED_CHARS_FROM for encoding/decoding
/// cookie names and values to ensure proper handling of these special characters.
static const List<String> RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; static const List<String> RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C'];
/// Creates cookie from raw header string. /// Creates a Cookie object from a string representation of a cookie.
///
/// This static method parses a cookie string and creates a corresponding Cookie object.
///
/// Parameters:
/// - [cookie]: A string representation of the cookie.
/// - [decode]: A boolean flag indicating whether to decode the cookie name and value (default is false).
///
/// Returns:
/// A Cookie object created from the parsed cookie string.
///
/// The method performs the following steps:
/// 1. Initializes default values for cookie attributes.
/// 2. Splits the cookie string into parts.
/// 3. Extracts the cookie name and value.
/// 4. Parses additional cookie attributes.
/// 5. Handles the 'expires' and 'max-age' attributes.
/// 6. Creates and returns a new Cookie object with the parsed attributes.
static Cookie fromString(String cookie, {bool decode = false}) { static Cookie fromString(String cookie, {bool decode = false}) {
final data = <String, Object?>{ final data = <String, Object?>{
'expires': 0, 'expires': 0,
@ -75,11 +305,45 @@ static Cookie fromString(String cookie, {bool decode = false}) {
); );
} }
/// Factory constructor for creating a new cookie. /// Creates a new Cookie instance with the specified parameters.
///
/// Parameters:
/// - [name]: The name of the cookie (required).
/// - [value]: The value of the cookie (optional).
/// - [expire]: The expiration time of the cookie (default: 0).
/// - [path]: The path on the server where the cookie will be available (default: '/').
/// - [domain]: The domain that the cookie is available to (optional).
/// - [secure]: Whether the cookie should only be transmitted over secure HTTPS (optional).
/// - [httpOnly]: Whether the cookie should be accessible only through HTTP protocol (default: true).
/// - [raw]: Whether the cookie should use no URL encoding (default: false).
/// - [sameSite]: The SameSite attribute of the cookie (default: SAMESITE_LAX).
/// - [partitioned]: Whether the cookie should be tied to the top-level site in cross-site context (default: false).
///
/// Returns a new Cookie instance with the specified attributes.
factory Cookie.create(String name, {String? value, dynamic expire = 0, String? path = '/', String? domain, bool? secure, bool httpOnly = true, bool raw = false, String? sameSite = SAMESITE_LAX, bool partitioned = false}) { factory Cookie.create(String name, {String? value, dynamic expire = 0, String? path = '/', String? domain, bool? secure, bool httpOnly = true, bool raw = false, String? sameSite = SAMESITE_LAX, bool partitioned = false}) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Internal constructor for creating a Cookie instance.
///
/// This constructor initializes a Cookie object with the provided parameters.
/// It performs validation checks on the cookie name and sets default values for certain attributes.
///
/// Parameters:
/// - [name]: The name of the cookie.
/// - [value]: The value of the cookie.
/// - [expire]: The expiration time of the cookie (can be DateTime, int, or String).
/// - [path]: The path on the server where the cookie will be available (default is '/').
/// - [domain]: The domain that the cookie is available to.
/// - [secure]: Whether the cookie should only be transmitted over secure HTTPS.
/// - [httpOnly]: Whether the cookie should be accessible only through HTTP protocol.
/// - [raw]: Whether the cookie should use no URL encoding.
/// - [sameSite]: The SameSite attribute of the cookie.
/// - [partitioned]: Whether the cookie should be tied to the top-level site in cross-site context.
///
/// Throws:
/// - [ArgumentError] if the cookie name contains invalid characters when [raw] is true.
/// - [ArgumentError] if the cookie name is empty.
Cookie._internal(this.name, this.value, dynamic expire, String? path, this.domain, this.secure, this.httpOnly, this.raw, this.sameSite, this.partitioned) { Cookie._internal(this.name, this.value, dynamic expire, String? path, this.domain, this.secure, this.httpOnly, this.raw, this.sameSite, this.partitioned) {
if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) { if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) {
throw ArgumentError('The cookie name "$name" contains invalid characters.'); throw ArgumentError('The cookie name "$name" contains invalid characters.');
@ -95,21 +359,70 @@ static Cookie fromString(String cookie, {bool decode = false}) {
} }
/// Creates a cookie copy with a new value. /// Creates a cookie copy with a new value.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [value]. All other attributes remain unchanged.
///
/// Parameters:
/// - [value]: The new value to set for the cookie. Can be null to create a cookie without a value.
///
/// Returns:
/// A new Cookie instance with the updated value.
Cookie withValue(String? value) { Cookie withValue(String? value) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy with a new domain that the cookie is available to. /// Creates a cookie copy with a new domain that the cookie is available to.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [domain]. All other attributes remain unchanged.
///
/// Parameters:
/// - [domain]: The new domain to set for the cookie. Can be null to remove the domain restriction.
///
/// Returns:
/// A new Cookie instance with the updated domain.
Cookie withDomain(String? domain) { Cookie withDomain(String? domain) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy with a new time the cookie expires. /// Creates a cookie copy with a new time the cookie expires.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [expire] time. All other attributes remain unchanged.
///
/// Parameters:
/// - [expire]: The new expiration time for the cookie. Can be a DateTime, int (Unix timestamp),
/// or String (parseable date format).
///
/// Returns:
/// A new Cookie instance with the updated expiration time.
///
/// Throws:
/// - [ArgumentError] if the provided [expire] value is not a valid expiration time format.
Cookie withExpires(dynamic expire) { Cookie withExpires(dynamic expire) {
return Cookie._internal(name, value, _expiresTimestamp(expire), path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, _expiresTimestamp(expire), path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Converts expires formats to a unix timestamp. /// Converts various expiration time formats to a Unix timestamp.
///
/// This method takes a dynamic [expire] parameter and converts it to a Unix timestamp
/// (seconds since the Unix epoch). It supports the following input types:
///
/// - [DateTime]: Converts the DateTime to a Unix timestamp.
/// - [int]: Assumes the input is already a Unix timestamp and returns it as-is.
/// - [String]: Parses the string as a DateTime and converts it to a Unix timestamp.
///
/// If the input doesn't match any of these types, an [ArgumentError] is thrown.
///
/// Parameters:
/// - [expire]: The expiration time in one of the supported formats.
///
/// Returns:
/// An integer representing the expiration time as a Unix timestamp.
///
/// Throws:
/// - [ArgumentError] if the input format is not recognized or cannot be parsed.
static int _expiresTimestamp(dynamic expire) { static int _expiresTimestamp(dynamic expire) {
if (expire is DateTime) { if (expire is DateTime) {
return expire.millisecondsSinceEpoch ~/ 1000; return expire.millisecondsSinceEpoch ~/ 1000;
@ -122,22 +435,64 @@ static Cookie fromString(String cookie, {bool decode = false}) {
} }
} }
/// Creates a cookie copy with a new path on the server in which the cookie will be available on. /// Creates a cookie copy with a new path on the server where the cookie will be available.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [path]. All other attributes remain unchanged.
///
/// Parameters:
/// - [path]: The new path to set for the cookie. If an empty string is provided, it defaults to '/'.
///
/// Returns:
/// A new Cookie instance with the updated path.
Cookie withPath(String path) { Cookie withPath(String path) {
return Cookie._internal(name, value, expire, path.isEmpty ? '/' : path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path.isEmpty ? '/' : path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client. /// Creates a cookie copy that can only be transmitted over a secure HTTPS connection from the client.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [secure] flag. All other attributes remain unchanged.
///
/// Parameters:
/// - [secure]: A boolean value indicating whether the cookie should only be transmitted over HTTPS.
/// If true, the cookie will only be sent over secure connections.
///
/// Returns:
/// A new Cookie instance with the updated secure flag.
Cookie withSecure(bool secure) { Cookie withSecure(bool secure) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy that be accessible only through the HTTP protocol. /// Creates a cookie copy that can be accessible only through the HTTP protocol.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [httpOnly] flag. All other attributes remain unchanged.
///
/// Parameters:
/// - [httpOnly]: A boolean value indicating whether the cookie should be accessible only through
/// the HTTP protocol. If true, the cookie will not be accessible through client-side scripts.
///
/// Returns:
/// A new Cookie instance with the updated httpOnly flag.
Cookie withHttpOnly(bool httpOnly) { Cookie withHttpOnly(bool httpOnly) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy that uses no url encoding. /// Creates a cookie copy that uses no URL encoding.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [raw] flag. All other attributes remain unchanged.
///
/// Parameters:
/// - [raw]: A boolean value indicating whether the cookie should use no URL encoding.
/// If true, the cookie name and value will not be URL-encoded.
///
/// Returns:
/// A new Cookie instance with the updated raw flag.
///
/// Throws:
/// - [ArgumentError] if [raw] is set to true and the cookie name contains invalid characters.
Cookie withRaw(bool raw) { Cookie withRaw(bool raw) {
if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) { if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) {
throw ArgumentError('The cookie name "$name" contains invalid characters.'); throw ArgumentError('The cookie name "$name" contains invalid characters.');
@ -145,7 +500,23 @@ static Cookie fromString(String cookie, {bool decode = false}) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Creates a cookie copy with SameSite attribute. /// Creates a cookie copy with a new SameSite attribute.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [sameSite] value. All other attributes remain unchanged.
///
/// Parameters:
/// - [sameSite]: The new SameSite attribute value for the cookie. Valid values are:
/// - [SAMESITE_LAX]: Cookies are not sent on normal cross-site subrequests but are sent when a user navigates to the origin site.
/// - [SAMESITE_STRICT]: Cookies are only sent in a first-party context and not sent along with requests initiated by third party websites.
/// - [SAMESITE_NONE]: Cookies are sent in all contexts, i.e., in responses to both first-party and cross-origin requests.
/// - null: The SameSite attribute is not set.
///
/// Returns:
/// A new Cookie instance with the updated SameSite attribute.
///
/// Throws:
/// - [ArgumentError] if the provided [sameSite] value is not one of the valid options.
Cookie withSameSite(String? sameSite) { Cookie withSameSite(String? sameSite) {
final validSameSite = [SAMESITE_LAX, SAMESITE_STRICT, SAMESITE_NONE, null]; final validSameSite = [SAMESITE_LAX, SAMESITE_STRICT, SAMESITE_NONE, null];
if (!validSameSite.contains(sameSite?.toLowerCase())) { if (!validSameSite.contains(sameSite?.toLowerCase())) {
@ -155,11 +526,34 @@ static Cookie fromString(String cookie, {bool decode = false}) {
} }
/// Creates a cookie copy that is tied to the top-level site in cross-site context. /// Creates a cookie copy that is tied to the top-level site in cross-site context.
///
/// This method returns a new Cookie instance with the same attributes as the current cookie,
/// but with the provided [partitioned] flag. All other attributes remain unchanged.
///
/// Parameters:
/// - [partitioned]: A boolean value indicating whether the cookie should be tied to the top-level site
/// in cross-site context. If true, the cookie will be partitioned.
///
/// Returns:
/// A new Cookie instance with the updated partitioned flag.
Cookie withPartitioned(bool partitioned) { Cookie withPartitioned(bool partitioned) {
return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned);
} }
/// Returns the cookie as a string. /// Converts the cookie to its string representation.
///
/// This method generates a string that represents the cookie in the format used in HTTP headers.
/// It includes all the cookie's attributes such as name, value, expiration, path, domain, secure flag,
/// HTTP-only flag, SameSite attribute, and partitioned flag.
///
/// The method handles the following cases:
/// - If the cookie is raw, the name and value are not URL-encoded.
/// - If the value is null or empty, the cookie is treated as deleted with immediate expiration.
/// - If an expiration time is set, it's included in both 'expires' and 'Max-Age' attributes.
/// - All other attributes (path, domain, secure, httpOnly, sameSite, partitioned) are added if set.
///
/// Returns:
/// A string representation of the cookie suitable for use in an HTTP header.
@override @override
String toString() { String toString() {
final buffer = StringBuffer(); final buffer = StringBuffer();
@ -210,45 +604,129 @@ static Cookie fromString(String cookie, {bool decode = false}) {
} }
/// Gets the name of the cookie. /// Gets the name of the cookie.
///
/// Returns:
/// A string representing the name of the cookie.
String getName() => name; String getName() => name;
/// Gets the value of the cookie. /// Gets the value of the cookie.
///
/// Returns:
/// A string representing the value of the cookie, or null if the cookie has no value.
String? getValue() => value; String? getValue() => value;
/// Gets the domain that the cookie is available to. /// Gets the domain that the cookie is available to.
///
/// Returns:
/// A string representing the domain of the cookie, or null if no domain is set.
String? getDomain() => domain; String? getDomain() => domain;
/// Gets the time the cookie expires. /// Gets the expiration time of the cookie.
///
/// Returns:
/// An integer representing the expiration time of the cookie as a Unix timestamp.
/// If the cookie doesn't have an expiration time set, it returns 0.
int getExpiresTime() => expire; int getExpiresTime() => expire;
/// Gets the max-age attribute. /// Calculates and returns the Max-Age attribute value for the cookie.
///
/// This method computes the number of seconds until the cookie expires.
/// It does this by subtracting the current Unix timestamp from the cookie's
/// expiration timestamp. The result is always non-negative, with a minimum
/// value of 0.
///
/// Returns:
/// An integer representing the number of seconds until the cookie expires.
/// If the cookie has already expired, it returns 0.
int getMaxAge() { int getMaxAge() {
final maxAge = expire - (DateTime.now().millisecondsSinceEpoch ~/ 1000); final maxAge = expire - (DateTime.now().millisecondsSinceEpoch ~/ 1000);
return max(0, maxAge); return max(0, maxAge);
} }
/// Gets the path on the server in which the cookie will be available on. /// Gets the path on the server where the cookie will be available.
///
/// This method returns the path attribute of the cookie, which specifies the
/// subset of URLs in a domain for which the cookie is valid.
///
/// Returns:
/// A string representing the path of the cookie.
String getPath() => path; String getPath() => path;
/// Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. /// Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client.
///
/// This method returns the value of the 'secure' flag for the cookie. If the 'secure' flag
/// is explicitly set (either true or false), it returns that value. If 'secure' is null,
/// it falls back to the default secure setting (secureDefault).
///
/// Returns:
/// A boolean value: true if the cookie should only be sent over secure connections,
/// false otherwise.
bool isSecure() => secure ?? secureDefault; bool isSecure() => secure ?? secureDefault;
/// Checks whether the cookie will be made accessible only through the HTTP protocol. /// Checks whether the cookie is accessible only through the HTTP protocol.
///
/// This method returns the value of the 'httpOnly' flag for the cookie.
/// If true, the cookie is inaccessible to client-side scripts like JavaScript,
/// which helps mitigate cross-site scripting (XSS) attacks.
///
/// Returns:
/// A boolean value: true if the cookie is HTTP-only, false otherwise.
bool isHttpOnly() => httpOnly; bool isHttpOnly() => httpOnly;
/// Whether this cookie is about to be cleared. /// Checks if the cookie has been cleared or has expired.
///
/// This method determines whether the cookie is considered cleared by checking two conditions:
/// 1. The cookie has an expiration time set (expire != 0).
/// 2. The expiration time is in the past (earlier than the current time).
///
/// Returns:
/// A boolean value: true if the cookie has been cleared or has expired, false otherwise.
bool isCleared() => expire != 0 && expire < (DateTime.now().millisecondsSinceEpoch ~/ 1000); bool isCleared() => expire != 0 && expire < (DateTime.now().millisecondsSinceEpoch ~/ 1000);
/// Checks if the cookie value should be sent with no url encoding. /// Checks if the cookie value should be sent with no URL encoding.
///
/// This method returns the value of the 'raw' flag for the cookie.
/// If true, the cookie name and value will not be URL-encoded when the cookie is converted to a string.
///
/// Returns:
/// A boolean value: true if the cookie should be sent raw (without URL encoding), false otherwise.
bool isRaw() => raw; bool isRaw() => raw;
/// Checks whether the cookie should be tied to the top-level site in cross-site context. /// Checks whether the cookie should be tied to the top-level site in cross-site context.
///
/// This method returns the value of the 'partitioned' flag for the cookie.
/// If true, the cookie will be partitioned, meaning it will be tied to the top-level site
/// in cross-site contexts, which can help improve privacy and security.
///
/// Returns:
/// A boolean value: true if the cookie is partitioned, false otherwise.
bool isPartitioned() => partitioned; bool isPartitioned() => partitioned;
/// Gets the SameSite attribute of the cookie. /// Gets the SameSite attribute of the cookie.
///
/// This method returns the value of the SameSite attribute for the cookie.
/// The SameSite attribute is used to control how cookies are sent with cross-site requests.
///
/// Returns:
/// A string representing the SameSite attribute of the cookie, which can be:
/// - 'strict': The cookie is only sent for same-site requests.
/// - 'lax': The cookie is sent for same-site requests and top-level navigation from external sites.
/// - 'none': The cookie is sent for all cross-site requests.
/// - null: If the SameSite attribute is not set.
String? getSameSite() => sameSite; String? getSameSite() => sameSite;
/// Sets the default value of the "secure" flag when it is set to null. /// Sets the default value for the "secure" flag when it is not explicitly set.
///
/// This method allows you to specify a default value for the "secure" flag
/// that will be used when the secure property of the cookie is null.
///
/// Parameters:
/// - [defaultSecure]: A boolean value indicating whether cookies should be
/// secure by default. If true, cookies will be marked as secure by default
/// when the secure property is not explicitly set.
///
/// This setting affects the behavior of the [isSecure] method when the
/// secure property is null.
void setSecureDefault(bool defaultSecure) { void setSecureDefault(bool defaultSecure) {
secureDefault = defaultSecure; secureDefault = defaultSecure;
} }

View file

@ -1,27 +1,121 @@
/*
* This file is part of the Protevus Platform.
* This file is a port of the symfony HeaderBag.php class to Dart
*
* (C) Protevus <developers@protevus.com>
* (C) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:collection'; import 'dart:collection';
/// HeaderBag is a container for HTTP headers. /// HeaderBag is a class that manages HTTP headers.
/// ///
/// Author: Fabien Potencier <fabien@symfony.com> /// This class provides functionality to store, retrieve, and manipulate HTTP headers.
/// This file is part of the Symfony package. /// It supports operations such as adding, removing, and checking for the presence of headers,
/// as well as special handling for Cache-Control directives.
///
/// Key features:
/// - Case-insensitive header names
/// - Support for multiple values per header
/// - Special handling for Cache-Control headers
/// - Methods to add, remove, and check headers
/// - Implements Iterable for easy traversal of headers
///
/// Usage:
/// ```dart
/// var headers = HeaderBag();
/// headers.set('Content-Type', 'application/json');
/// headers.add({'Accept': ['text/html', 'application/xhtml+xml']});
/// print(headers.get('content-type')); // Prints: application/json
/// ```
///
/// This class is particularly useful for HTTP clients and servers that need to
/// manage complex header scenarios, including multiple header values and
/// Cache-Control directives.
class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> { class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
/// A constant string containing uppercase letters and underscore.
///
/// This constant is used for case-insensitive string operations,
/// particularly in header name formatting. It includes the underscore
/// character followed by all uppercase letters of the English alphabet.
///
/// The string is defined as:
/// - Underscore: '_'
/// - Uppercase letters: 'A' through 'Z'
///
/// This constant is typically used in conjunction with the 'lower' constant
/// for case conversion operations within the HeaderBag class.
static const String upper = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; static const String upper = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
/// A constant string containing lowercase letters and hyphen.
///
/// This constant is used for case-insensitive string operations,
/// particularly in header name formatting. It includes the hyphen
/// character followed by all lowercase letters of the English alphabet.
///
/// The string is defined as:
/// - Hyphen: '-'
/// - Lowercase letters: 'a' through 'z'
///
/// This constant is typically used in conjunction with the 'upper' constant
/// for case conversion operations within the HeaderBag class.
static const String lower = '-abcdefghijklmnopqrstuvwxyz'; static const String lower = '-abcdefghijklmnopqrstuvwxyz';
/// A map to store the headers. /// A map to store the headers.
///
/// This private field stores all the HTTP headers in the HeaderBag.
/// The keys of the map are header names (stored in lowercase),
/// and the values are lists of header values.
///
/// Using a list for values allows for multiple values per header,
/// which is common in HTTP headers (e.g., multiple Set-Cookie headers).
///
/// The use of nullable String (String?) in the list allows for the
/// possibility of null values, which might occur in some edge cases.
final Map<String, List<String?>> _headers = {}; final Map<String, List<String?>> _headers = {};
/// A map to store cache control directives. /// A map to store cache control directives.
///
/// This map holds key-value pairs representing Cache-Control directives.
/// Keys are directive names (e.g., "max-age", "no-cache"), and values are
/// the corresponding directive values or true for valueless directives.
///
/// This map is used internally to manage and manipulate Cache-Control
/// header information efficiently, allowing for easy addition, removal,
/// and retrieval of individual directives.
final Map<String, dynamic> _cacheControl = {}; final Map<String, dynamic> _cacheControl = {};
/// Constructor for HeaderBag /// Constructor for HeaderBag
///
/// Creates a new HeaderBag instance with the given headers.
///
/// @param headers An optional map of headers to initialize the HeaderBag with.
/// The map keys are header names, and the values are lists of
/// header values. If not provided, an empty map is used.
///
/// This constructor initializes the HeaderBag by setting each header in the
/// provided map using the `set` method, which ensures proper formatting and
/// handling of special headers like 'Cache-Control'.
HeaderBag([Map<String, List<String?>> headers = const {}]) { HeaderBag([Map<String, List<String?>> headers = const {}]) {
headers.forEach((key, values) { headers.forEach((key, values) {
set(key, values); set(key, values);
}); });
} }
/// Returns the headers as a string. /// Returns a string representation of the headers.
///
/// This method creates a formatted string of all headers in the HeaderBag.
/// The headers are sorted alphabetically by key, and each header is
/// presented on a new line with the header name capitalized appropriately.
/// The header names are right-padded to align all header values.
///
/// If the HeaderBag is empty, an empty string is returned.
///
/// @return A formatted string representation of all headers.
@override @override
String toString() { String toString() {
if (_headers.isEmpty) { if (_headers.isEmpty) {
@ -42,11 +136,17 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
return content.toString(); return content.toString();
} }
/// Returns the headers. /// Returns all headers or headers for a specific key.
/// ///
/// @param key The name of the headers to return or null to get them all /// If a [key] is provided, this method returns a map containing only that key
/// (in lowercase) and its associated list of values. If the key doesn't exist,
/// an empty list is returned for that key.
/// ///
/// @return A map of headers. /// If no [key] is provided, this method returns all headers in the HeaderBag.
///
/// @param key The optional key to retrieve specific headers.
/// @return A map of headers. If a key is provided, the map will contain only
/// that key-value pair. If no key is provided, all headers are returned.
Map<String, List<String?>> all([String? key]) { Map<String, List<String?>> all([String? key]) {
if (key != null) { if (key != null) {
return {key.toLowerCase(): _headers[key.toLowerCase()] ?? []}; return {key.toLowerCase(): _headers[key.toLowerCase()] ?? []};
@ -56,25 +156,55 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
/// Returns the parameter keys. /// Returns the parameter keys.
/// ///
/// @return A list of keys. /// This method retrieves all the keys from the internal _headers map
/// and returns them as a list of strings. The keys represent the names
/// of all the headers stored in this HeaderBag.
///
/// @return A list of strings containing all the header names.
List<String> keys() { List<String> keys() {
return _headers.keys.toList(); return _headers.keys.toList();
} }
/// Replaces the current HTTP headers by a new set. /// Replaces the current HTTP headers with a new set of headers.
///
/// This method first clears all existing headers in the HeaderBag,
/// then adds the new headers provided in the [headers] parameter.
///
/// @param headers A map of new headers to replace the existing ones.
/// The map keys are header names, and the values are
/// lists of header values.
void replace(Map<String, List<String?>> headers) { void replace(Map<String, List<String?>> headers) {
_headers.clear(); _headers.clear();
add(headers); add(headers);
} }
/// Adds new headers to the current HTTP headers set. /// Adds new headers to the current HTTP headers set.
///
/// This method takes a map of headers and adds them to the existing headers
/// in the HeaderBag. If a header with the same name already exists, its values
/// are appended to the existing values.
///
/// @param headers A map where keys are header names and values are lists of
/// header values to be added.
///
/// Each header in the input map is added using the `set` method, which handles
/// the details of appending values and updating special headers like 'Cache-Control'.
void add(Map<String, List<String?>> headers) { void add(Map<String, List<String?>> headers) {
headers.forEach((key, values) { headers.forEach((key, values) {
set(key, values); set(key, values);
}); });
} }
/// Returns the first header by name or the default one. /// Returns the first value of the specified HTTP header.
///
/// This method retrieves the first value of the header specified by [key].
/// If the header doesn't exist or has no values, it returns the [defaultValue].
///
/// @param key The name of the HTTP header to retrieve.
/// @param defaultValue An optional default value to return if the header
/// doesn't exist or has no values. Defaults to null if not specified.
/// @return The first value of the specified header, or the default value
/// if the header doesn't exist or has no values.
String? get(String key, [String? defaultValue]) { String? get(String key, [String? defaultValue]) {
var headers = all(key)[key.toLowerCase()]; var headers = all(key)[key.toLowerCase()];
if (headers == null || headers.isEmpty) { if (headers == null || headers.isEmpty) {
@ -85,8 +215,19 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
/// Sets a header by name. /// Sets a header by name.
/// ///
/// @param values The value or an array of values /// This method sets or adds a header to the HeaderBag. It can handle both
/// @param replace Whether to replace the actual value or not (true by default) /// single values and lists of values.
///
/// @param key The name of the header to set. This will be converted to lowercase.
/// @param values The value or list of values to set for the header.
/// @param replace Whether to replace the existing values (if any) or append to them.
/// Defaults to true.
///
/// If [replace] is true or the header doesn't exist, it will overwrite any existing
/// values. If [replace] is false and the header exists, it will append the new values.
///
/// For the 'cache-control' header, this method also updates the internal
/// cache control directives by parsing the new header value.
void set(String key, dynamic values, [bool replace = true]) { void set(String key, dynamic values, [bool replace = true]) {
key = key.toLowerCase(); key = key.toLowerCase();
List<String?> valueList; List<String?> valueList;
@ -111,17 +252,39 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
} }
} }
/// Returns true if the HTTP header is defined. /// Checks if a specific HTTP header is present in the HeaderBag.
///
/// This method determines whether a header with the given [key] exists
/// in the HeaderBag. The header name (key) is case-insensitive.
///
/// @param key The name of the HTTP header to check for.
/// @return true if the header exists, false otherwise.
bool hasHeader(String key) { bool hasHeader(String key) {
return _headers.containsKey(key.toLowerCase()); return _headers.containsKey(key.toLowerCase());
} }
/// Returns true if the given HTTP header contains the given value. /// Checks if a specific HTTP header contains a given value.
///
/// This method determines whether the header specified by [key] contains
/// the given [value]. The header name (key) is case-insensitive.
///
/// @param key The name of the HTTP header to check.
/// @param value The value to search for in the header.
/// @return true if the header contains the value, false otherwise.
/// If the header doesn't exist, this method returns false.
bool containsHeaderValue(String key, String value) { bool containsHeaderValue(String key, String value) {
return _headers[key.toLowerCase()]?.contains(value) ?? false; return _headers[key.toLowerCase()]?.contains(value) ?? false;
} }
/// Removes a header. /// Removes a header from the HeaderBag.
///
/// This method removes the header specified by [key] from the HeaderBag.
/// The key is case-insensitive and will be converted to lowercase before removal.
///
/// If the removed header is 'cache-control', this method also clears
/// the internal cache control directives.
///
/// @param key The name of the header to remove.
void remove(String key) { void remove(String key) {
key = key.toLowerCase(); key = key.toLowerCase();
_headers.remove(key); _headers.remove(key);
@ -132,6 +295,17 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
/// Returns the HTTP header value converted to a date. /// Returns the HTTP header value converted to a date.
/// ///
/// This method retrieves the value of the specified HTTP header and attempts
/// to parse it as a DateTime object. If the header doesn't exist or its value
/// is null, the method returns the provided default value.
///
/// @param key The name of the HTTP header to retrieve and parse as a date.
/// @param defaultValue An optional DateTime object to return if the header
/// doesn't exist or its value is null. Defaults to null if not specified.
/// @return A DateTime object representing the parsed header value, or the
/// default value if the header doesn't exist or its value is null.
/// @throws Exception if the header value cannot be parsed as a valid date.
///
/// Throws an exception when the HTTP header is not parseable. /// Throws an exception when the HTTP header is not parseable.
DateTime? getDate(String key, [DateTime? defaultValue]) { DateTime? getDate(String key, [DateTime? defaultValue]) {
var value = get(key); var value = get(key);
@ -147,36 +321,83 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
} }
/// Adds a custom Cache-Control directive. /// Adds a custom Cache-Control directive.
///
/// This method adds a new directive to the Cache-Control header or updates
/// an existing one. The directive is specified by [key], and an optional
/// [value] can be provided.
///
/// @param key The name of the Cache-Control directive to add or update.
/// @param value The value of the directive. Defaults to true if not specified.
///
/// After updating the internal cache control directives, this method
/// regenerates the Cache-Control header string and sets it using the `set` method.
void addCacheControlDirective(String key, [dynamic value = true]) { void addCacheControlDirective(String key, [dynamic value = true]) {
_cacheControl[key] = value; _cacheControl[key] = value;
set('Cache-Control', getCacheControlHeader()); set('Cache-Control', getCacheControlHeader());
} }
/// Returns true if the Cache-Control directive is defined. /// Checks if a specific Cache-Control directive is present.
///
/// This method determines whether a Cache-Control directive with the given [key]
/// exists in the internal cache control directives map.
///
/// @param key The name of the Cache-Control directive to check for.
/// @return true if the directive exists, false otherwise.
bool hasCacheControlDirective(String key) { bool hasCacheControlDirective(String key) {
return _cacheControl.containsKey(key); return _cacheControl.containsKey(key);
} }
/// Returns a Cache-Control directive value by name. /// Returns the value of a specific Cache-Control directive.
///
/// This method retrieves the value associated with the given Cache-Control
/// directive [key] from the internal cache control directives map.
///
/// @param key The name of the Cache-Control directive to retrieve.
/// @return The value of the specified Cache-Control directive, or null if
/// the directive doesn't exist. The return type is dynamic as
/// Cache-Control directive values can be of various types.
dynamic getCacheControlDirective(String key) { dynamic getCacheControlDirective(String key) {
return _cacheControl[key]; return _cacheControl[key];
} }
/// Removes a Cache-Control directive. /// Removes a Cache-Control directive.
///
/// This method removes the specified Cache-Control directive from the internal
/// cache control directives map and updates the Cache-Control header accordingly.
///
/// @param key The name of the Cache-Control directive to remove.
///
/// After removing the directive from the internal map, this method regenerates
/// the Cache-Control header string and sets it using the `set` method.
void removeCacheControlDirective(String key) { void removeCacheControlDirective(String key) {
_cacheControl.remove(key); _cacheControl.remove(key);
set('Cache-Control', getCacheControlHeader()); set('Cache-Control', getCacheControlHeader());
} }
/// Returns an iterator for headers. /// Returns an iterator for the headers.
/// ///
/// @return An iterator of MapEntry. /// This method provides an iterator that allows iteration over all headers
/// in the HeaderBag. Each iteration yields a MapEntry where the key is the
/// header name (as a String) and the value is a List of String? representing
/// the header values.
///
/// This implementation directly returns the iterator of the internal _headers
/// map entries, allowing for efficient iteration over all headers.
///
/// @return An Iterator<MapEntry<String, List<String?>>> for iterating over
/// all headers in the HeaderBag.
@override @override
Iterator<MapEntry<String, List<String?>>> get iterator { Iterator<MapEntry<String, List<String?>>> get iterator {
return _headers.entries.iterator; return _headers.entries.iterator;
} }
/// Returns the number of headers. /// Returns the number of headers.
///
/// This getter provides the count of unique headers in the HeaderBag.
/// It directly returns the length of the internal _headers map,
/// which represents the number of distinct header names stored.
///
/// @return An integer representing the number of headers in the HeaderBag.
@override @override
int get length { int get length {
return _headers.length; return _headers.length;
@ -184,15 +405,38 @@ class HeaderBag extends IterableBase<MapEntry<String, List<String?>>> {
/// Generates the Cache-Control header value. /// Generates the Cache-Control header value.
/// ///
/// @return A string representation of the Cache-Control header. /// This method creates a string representation of the Cache-Control header
/// based on the directives stored in the internal _cacheControl map.
///
/// The method performs the following steps:
/// 1. Creates a sorted copy of the _cacheControl map using a SplayTreeMap.
/// 2. Iterates through the entries of the sorted map.
/// 3. Formats each entry as "key=value".
/// 4. Joins all formatted entries with ", " as separator.
///
/// @return A string representation of the Cache-Control header, where
/// directives are sorted alphabetically by key and separated by commas.
/// For example: "max-age=300, must-revalidate, no-cache".
String getCacheControlHeader() { String getCacheControlHeader() {
var sortedCacheControl = SplayTreeMap<String, dynamic>.from(_cacheControl); var sortedCacheControl = SplayTreeMap<String, dynamic>.from(_cacheControl);
return sortedCacheControl.entries.map((e) => '${e.key}=${e.value}').join(', '); return sortedCacheControl.entries.map((e) => '${e.key}=${e.value}').join(', ');
} }
/// Parses a Cache-Control HTTP header. /// Parses a Cache-Control HTTP header string into a map of directives.
/// ///
/// @return A map of Cache-Control directives. /// This method takes a Cache-Control header value as a string and converts it
/// into a map where keys are directive names and values are directive values.
///
/// The method performs the following steps:
/// 1. Splits the header string by commas to separate individual directives.
/// 2. For each directive, splits by '=' to separate the name and value.
/// 3. Trims whitespace from directive names and values.
/// 4. If a directive has no value (no '='), it's set to true.
///
/// @param header A string containing the Cache-Control header value.
/// @return A Map<String, dynamic> where keys are directive names (String)
/// and values are either String (for directives with values) or
/// bool (true for directives without values).
Map<String, dynamic> _parseCacheControl(String header) { Map<String, dynamic> _parseCacheControl(String header) {
var parts = header.split(',').map((e) => e.split('=')).toList(); var parts = header.split(',').map((e) => e.split('=')).toList();
var map = <String, dynamic>{}; var map = <String, dynamic>{};

View file

@ -1,24 +1,62 @@
/*
* This file is part of the Protevus Platform.
* This file is a port of the symfony HeaderUtils.php class to Dart
*
* (C) Protevus <developers@protevus.com>
* (C) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:convert'; import 'dart:convert';
class HeaderUtils { class HeaderUtils {
/// A constant string representing the "attachment" disposition type.
///
/// This value is used in HTTP headers, particularly in the Content-Disposition
/// header, to indicate that the content is expected to be downloaded and saved
/// locally by the user agent, rather than being displayed inline in the browser.
static const String DISPOSITION_ATTACHMENT = 'attachment'; static const String DISPOSITION_ATTACHMENT = 'attachment';
/// A constant string representing the "inline" disposition type.
///
/// This value is used in HTTP headers, particularly in the Content-Disposition
/// header, to indicate that the content is expected to be displayed inline
/// in the browser, rather than being downloaded and saved locally.
static const String DISPOSITION_INLINE = 'inline'; static const String DISPOSITION_INLINE = 'inline';
// This class should not be instantiated. /// Private constructor to prevent instantiation of the HeaderUtils class.
///
/// This class is intended to be used as a utility class with static methods only.
/// The underscore before the constructor name makes it private to this library.
HeaderUtils._(); HeaderUtils._();
/// Splits an HTTP header by one or more separators. /// Splits an HTTP header by one or more separators.
/// ///
/// This method parses a given HTTP header string and splits it into parts
/// based on the provided separators. It handles quoted strings and tokens
/// according to HTTP header specifications.
///
/// Parameters:
/// - [header]: The HTTP header string to be split.
/// - [separators]: A string containing one or more separator characters.
///
/// Returns:
/// A List of Lists of Strings, where each inner List represents a part of
/// the header split by the separators.
///
/// Throws:
/// - [ArgumentError] if [separators] is empty.
///
/// Example: /// Example:
/// HeaderUtils.split('da, en-gb;q=0.8', ',;')
/// // => [['da'], ['en-gb', 'q=0.8']]
/// ///
/// HeaderUtils.split('da, en-gb;q=0.8', ',;') /// The method uses regular expressions to handle complex cases such as
/// // => [['da'], ['en-gb', 'q=0.8']] /// quoted strings and multiple separators. It preserves the structure of
/// /// the original header while splitting it into logical parts.
/// @param String separators List of characters to split on, ordered by
/// precedence, e.g. ',', ';=', or ',;='
///
/// @return List<List<String>> Nested array with as many levels as there are characters in
/// separators
static List<List<String>> split(String header, String separators) { static List<List<String>> split(String header, String separators) {
if (separators.isEmpty) { if (separators.isEmpty) {
throw ArgumentError('At least one separator must be specified.'); throw ArgumentError('At least one separator must be specified.');
@ -57,8 +95,16 @@ class HeaderUtils {
/// will be used as the values, or true if the nested array only contains one /// will be used as the values, or true if the nested array only contains one
/// element. Array keys are lowercased. /// element. Array keys are lowercased.
/// ///
/// Example: /// Parameters:
/// - [parts]: A List of Lists of Strings, where each inner List represents a part
/// to be combined into the associative array.
/// ///
/// Returns:
/// A Map<String, dynamic> where the keys are the lowercased first elements of each
/// inner List, and the values are either the second elements or true if there's no
/// second element.
///
/// Example:
/// HeaderUtils.combine([['foo', 'abc'], ['bar']]) /// HeaderUtils.combine([['foo', 'abc'], ['bar']])
/// // => {'foo': 'abc', 'bar': true} /// // => {'foo': 'abc', 'bar': true}
static Map<String, dynamic> combine(List<List<String>> parts) { static Map<String, dynamic> combine(List<List<String>> parts) {
@ -73,12 +119,21 @@ class HeaderUtils {
/// Joins an associative array into a string for use in an HTTP header. /// Joins an associative array into a string for use in an HTTP header.
/// ///
/// The key and value of each entry are joined with '=', and all entries /// This method takes a Map of key-value pairs and joins them into a single string,
/// are joined with the specified separator and an additional space (for /// suitable for use in an HTTP header. Each key-value pair is formatted as follows:
/// readability). Values are quoted if necessary. /// - If the value is `true`, only the key is included.
/// - Otherwise, the pair is formatted as "key=value", where the value is quoted if necessary.
///
/// The formatted pairs are then joined with the specified separator and an additional space.
///
/// Parameters:
/// - [assoc]: A Map<String, dynamic> containing the key-value pairs to be joined.
/// - [separator]: A String used to separate the formatted pairs in the output.
///
/// Returns:
/// A String representing the joined key-value pairs, suitable for use in an HTTP header.
/// ///
/// Example: /// Example:
///
/// HeaderUtils.headerToString({'foo': 'abc', 'bar': true, 'baz': 'a b c'}, ',') /// HeaderUtils.headerToString({'foo': 'abc', 'bar': true, 'baz': 'a b c'}, ',')
/// // => 'foo=abc, bar, baz="a b c"' /// // => 'foo=abc, bar, baz="a b c"'
static String headerToString(Map<String, dynamic> assoc, String separator) { static String headerToString(Map<String, dynamic> assoc, String separator) {
@ -93,11 +148,28 @@ class HeaderUtils {
return parts.join('$separator '); return parts.join('$separator ');
} }
/// Encodes a string as a quoted string, if necessary. /// Quotes a string for use in HTTP headers if necessary.
/// ///
/// If a string contains characters not allowed by the "token" construct in /// This method takes a string and determines whether it needs to be quoted
/// the HTTP specification, it is backslash-escaped and enclosed in quotes /// for use in HTTP headers. If quoting is necessary, it encloses the string
/// to match the "quoted-string" construct. /// in double quotes and escapes any existing double quotes within the string.
///
/// Parameters:
/// - [s]: The input string to be quoted if necessary.
///
/// Returns:
/// A String that is either:
/// - The original string if it doesn't need quoting
/// - The string enclosed in double quotes with internal quotes escaped
/// - An empty quoted string ('""') if the input is an empty string
///
/// Throws:
/// - [ArgumentError] if the input string is null.
///
/// Example:
/// quote('simple') // => 'simple'
/// quote('needs "quotes"') // => '"needs \"quotes\""'
/// quote('') // => '""'
static String quote(String? s) { static String quote(String? s) {
if (s == null) { if (s == null) {
throw ArgumentError('Input string cannot be null'); throw ArgumentError('Input string cannot be null');
@ -116,30 +188,79 @@ class HeaderUtils {
return s; return s;
} }
/// Determines if a string can be used unquoted in HTTP headers.
///
/// This method checks if the given string consists only of characters
/// that are allowed in unquoted header values according to HTTP specifications.
///
/// Parameters:
/// - [s]: The string to be checked.
///
/// Returns:
/// - `true` if the string can be used unquoted in HTTP headers.
/// - `false` if the string needs to be quoted for use in HTTP headers.
///
/// The allowed characters are:
/// - Alphanumeric characters (a-z, A-Z, 0-9)
/// - The following special characters: !#$%&'*+-\.^_`|~
///
/// This method is typically used internally by other header-related functions
/// to determine whether a value needs quoting before being included in an HTTP header.
static bool _isQuotingAllowed(String s) { static bool _isQuotingAllowed(String s) {
final pattern = RegExp('^[a-zA-Z0-9!#\$%&\'*+\\-\\.^_`|~]+\$'); final pattern = RegExp('^[a-zA-Z0-9!#\$%&\'*+\\-\\.^_`|~]+\$');
return pattern.hasMatch(s); return pattern.hasMatch(s);
} }
/// Decodes a quoted string. /// Removes quotes and unescapes characters in a string.
/// ///
/// If passed an unquoted string that matches the "token" construct (as /// This method processes a string that may have been quoted or contain
/// defined in the HTTP specification), it is passed through verbatim. /// escaped characters. It performs the following operations:
/// 1. Removes surrounding double quotes if present.
/// 2. Unescapes any escaped characters (i.e., removes the backslash).
///
/// Parameters:
/// - [s]: The input string to be unquoted and unescaped.
///
/// Returns:
/// A String with quotes removed and escaped characters processed.
///
/// Example:
/// unquote('"Hello \\"World\\""') // => 'Hello "World"'
/// unquote('No \\"quotes\\"') // => 'No "quotes"'
static String unquote(String s) { static String unquote(String s) {
return s.replaceAllMapped(RegExp(r'\\(.)|\"'), (match) => match[1] ?? ''); return s.replaceAllMapped(RegExp(r'\\(.)|\"'), (match) => match[1] ?? '');
} }
/// Generates an HTTP Content-Disposition field-value. /// Generates an HTTP Content-Disposition header value.
/// ///
/// @param String disposition One of "inline" or "attachment" /// This method creates a properly formatted Content-Disposition header value
/// @param String filename A unicode string /// based on the given disposition type and filename. It supports both ASCII
/// @param String filenameFallback A string containing only ASCII characters that /// and non-ASCII filenames, providing a fallback for older user agents.
/// is semantically equivalent to filename. If the filename is already ASCII,
/// it can be omitted, or just copied from filename
/// ///
/// @throws ArgumentError /// Parameters:
/// - [disposition]: The disposition type, must be either "attachment" or "inline".
/// - [filename]: The filename to be used in the Content-Disposition header.
/// - [filenameFallback]: An optional ASCII-only fallback filename for older user agents.
/// If not provided, it defaults to the same value as [filename].
///
/// Returns:
/// A String representing the formatted Content-Disposition header value.
///
/// Throws:
/// - [ArgumentError] if:
/// - The disposition is neither "attachment" nor "inline".
/// - The filename fallback contains non-ASCII characters.
/// - The filename fallback contains the "%" character.
/// - Either filename or fallback contains "/" or "\" characters.
/// ///
/// @see RFC 6266 /// @see RFC 6266
///
/// Example:
/// makeDisposition('attachment', 'example.pdf')
/// // => 'attachment; filename="example.pdf"'
///
/// makeDisposition('inline', 'résumé.pdf', 'resume.pdf')
/// // => 'inline; filename="resume.pdf"; filename*=utf-8\'\'r%C3%A9sum%C3%A9.pdf'
static String makeDisposition(String disposition, String filename, [String filenameFallback = '']) { static String makeDisposition(String disposition, String filename, [String filenameFallback = '']) {
if (![DISPOSITION_ATTACHMENT, DISPOSITION_INLINE].contains(disposition)) { if (![DISPOSITION_ATTACHMENT, DISPOSITION_INLINE].contains(disposition)) {
throw ArgumentError('The disposition must be either "$DISPOSITION_ATTACHMENT" or "$DISPOSITION_INLINE".'); throw ArgumentError('The disposition must be either "$DISPOSITION_ATTACHMENT" or "$DISPOSITION_INLINE".');
@ -168,6 +289,36 @@ static bool _isQuotingAllowed(String s) {
} }
/// Like parse_str(), but preserves dots in variable names. /// Like parse_str(), but preserves dots in variable names.
/// Parses a query string into a Map of key-value pairs.
///
/// This method takes a query string and converts it into a Map where the keys
/// are the query parameters and the values are their corresponding values.
///
/// Parameters:
/// - [query]: The query string to parse.
/// - [ignoreBrackets]: If true, treats square brackets as part of the parameter name.
/// Defaults to false.
/// - [separator]: The character used to separate key-value pairs in the query string.
/// Defaults to '&'.
///
/// Returns:
/// A Map<String, dynamic> where keys are the parameter names and values are the
/// corresponding parameter values.
///
/// If [ignoreBrackets] is false (default), the method handles parameters with square
/// brackets specially, decoding them from base64 and including the bracket content
/// in the resulting key.
///
/// Example:
/// parseQuery('foo=bar&baz=qux')
/// // => {'foo': 'bar', 'baz': 'qux'}
///
/// parseQuery('foo[]=bar&foo[]=baz', false, '&')
/// // => {'foo[]': 'bar', 'foo[]': 'baz'}
///
/// Note: This method includes some specific handling for the character '0' in keys
/// and values, truncating strings at this character. It also trims whitespace from
/// the left side of keys. This is like parse_str(), but preserves dots in variable names.
static Map<String, dynamic> parseQuery(String query, [bool ignoreBrackets = false, String separator = '&']) { static Map<String, dynamic> parseQuery(String query, [bool ignoreBrackets = false, String separator = '&']) {
final result = <String, dynamic>{}; final result = <String, dynamic>{};
@ -213,7 +364,26 @@ static Map<String, dynamic> parseQuery(String query, [bool ignoreBrackets = fals
return result; return result;
} }
/// Groups parts of a header string based on specified separators.
///
/// This recursive method processes a list of [RegExpMatch] objects, grouping them
/// based on the provided [separators]. It handles nested structures in header strings.
///
/// Parameters:
/// - [matches]: A list of [RegExpMatch] objects representing parts of the header.
/// - [separators]: A string containing characters used as separators.
/// - [first]: A boolean indicating if this is the first call in the recursion (default: true).
///
/// Returns:
/// A List of Lists of Strings, where each inner List represents a grouped part of the header.
///
/// The method works by:
/// 1. Splitting the parts based on the first separator in the [separators] string.
/// 2. Recursively processing subgroups if more separators are available.
/// 3. Handling special cases for the last separator and quoted strings.
///
/// This method is typically used internally by the [split] method to process complex
/// header structures with multiple levels of separators.
static List<List<String>> _groupParts(List<RegExpMatch> matches, String separators, [bool first = true]) { static List<List<String>> _groupParts(List<RegExpMatch> matches, String separators, [bool first = true]) {
final separator = separators[0]; final separator = separators[0];
separators = separators.substring(1); separators = separators.substring(1);