From 54a2ef0dbe2c12014302726f8ada66f478ad2b19 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Thu, 4 Jul 2024 21:20:02 -0700 Subject: [PATCH] add: adding comments to code files --- packages/http/lib/src/foundation/cookie.dart | 518 +++++++++++++++++- .../http/lib/src/foundation/header_bag.dart | 288 +++++++++- .../http/lib/src/foundation/header_utils.dart | 228 +++++++- 3 files changed, 963 insertions(+), 71 deletions(-) diff --git a/packages/http/lib/src/foundation/cookie.dart b/packages/http/lib/src/foundation/cookie.dart index f932429..db0203d 100644 --- a/packages/http/lib/src/foundation/cookie.dart +++ b/packages/http/lib/src/foundation/cookie.dart @@ -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 + * (C) Protevus + * (C) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import 'header_utils.dart'; 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 { + +/// 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'; + +/// 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'; + +/// 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'; +/// 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; + +/// 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; + +/// 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; + +/// 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; + +/// 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; + +/// 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; + +/// 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; +/// 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; + +/// 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; + +/// 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; + +/// 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; +/// 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"; + +/// 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 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 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}) { final data = { '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}) { 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) { if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) { 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. + /// + /// 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) { 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. + /// + /// 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) { return Cookie._internal(name, value, expire, path, domain, secure, httpOnly, raw, sameSite, partitioned); } /// 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) { 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) { if (expire is DateTime) { 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) { 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) { 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) { 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) { if (raw && name.contains(RegExp(r'[' + RESERVED_CHARS_LIST + r']'))) { 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); } - /// 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) { final validSameSite = [SAMESITE_LAX, SAMESITE_STRICT, SAMESITE_NONE, null]; 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. + /// + /// 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) { 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 String toString() { final buffer = StringBuffer(); @@ -210,45 +604,129 @@ static Cookie fromString(String cookie, {bool decode = false}) { } /// Gets the name of the cookie. + /// + /// Returns: + /// A string representing the name of the cookie. String getName() => name; /// 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; /// 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; - /// 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; - /// 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() { final maxAge = expire - (DateTime.now().millisecondsSinceEpoch ~/ 1000); 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; /// 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; - /// 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; - /// 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); - /// 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; /// 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; /// 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; - /// 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) { secureDefault = defaultSecure; } diff --git a/packages/http/lib/src/foundation/header_bag.dart b/packages/http/lib/src/foundation/header_bag.dart index 2ebbafc..78632a7 100644 --- a/packages/http/lib/src/foundation/header_bag.dart +++ b/packages/http/lib/src/foundation/header_bag.dart @@ -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 + * (C) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:collection'; -/// HeaderBag is a container for HTTP headers. +/// HeaderBag is a class that manages HTTP headers. /// -/// Author: Fabien Potencier -/// This file is part of the Symfony package. +/// This class provides functionality to store, retrieve, and manipulate HTTP headers. +/// 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>> { + + /// 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'; + + /// 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'; /// 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> _headers = {}; /// 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 _cacheControl = {}; /// 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> headers = const {}]) { headers.forEach((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 String toString() { if (_headers.isEmpty) { @@ -42,11 +136,17 @@ class HeaderBag extends IterableBase>> { 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> all([String? key]) { if (key != null) { return {key.toLowerCase(): _headers[key.toLowerCase()] ?? []}; @@ -56,25 +156,55 @@ class HeaderBag extends IterableBase>> { /// 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 keys() { 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> headers) { _headers.clear(); add(headers); } /// 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> headers) { headers.forEach((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]) { var headers = all(key)[key.toLowerCase()]; if (headers == null || headers.isEmpty) { @@ -85,8 +215,19 @@ class HeaderBag extends IterableBase>> { /// Sets a header by name. /// - /// @param values The value or an array of values - /// @param replace Whether to replace the actual value or not (true by default) + /// This method sets or adds a header to the HeaderBag. It can handle both + /// 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]) { key = key.toLowerCase(); List valueList; @@ -111,17 +252,39 @@ class HeaderBag extends IterableBase>> { } } - /// 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) { 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) { 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) { key = key.toLowerCase(); _headers.remove(key); @@ -132,6 +295,17 @@ class HeaderBag extends IterableBase>> { /// 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. DateTime? getDate(String key, [DateTime? defaultValue]) { var value = get(key); @@ -147,36 +321,83 @@ class HeaderBag extends IterableBase>> { } /// 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]) { _cacheControl[key] = value; 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) { 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) { return _cacheControl[key]; } /// 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) { _cacheControl.remove(key); 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>> for iterating over + /// all headers in the HeaderBag. @override Iterator>> get iterator { return _headers.entries.iterator; } /// 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 int get length { return _headers.length; @@ -184,15 +405,38 @@ class HeaderBag extends IterableBase>> { /// 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() { var sortedCacheControl = SplayTreeMap.from(_cacheControl); 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 where keys are directive names (String) + /// and values are either String (for directives with values) or + /// bool (true for directives without values). Map _parseCacheControl(String header) { var parts = header.split(',').map((e) => e.split('=')).toList(); var map = {}; diff --git a/packages/http/lib/src/foundation/header_utils.dart b/packages/http/lib/src/foundation/header_utils.dart index 2bec390..4cca83d 100644 --- a/packages/http/lib/src/foundation/header_utils.dart +++ b/packages/http/lib/src/foundation/header_utils.dart @@ -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 + * (C) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + import 'dart:convert'; 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'; + + /// 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'; - // 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._(); /// 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: + /// HeaderUtils.split('da, en-gb;q=0.8', ',;') + /// // => [['da'], ['en-gb', 'q=0.8']] /// - /// HeaderUtils.split('da, en-gb;q=0.8', ',;') - /// // => [['da'], ['en-gb', 'q=0.8']] - /// - /// @param String separators List of characters to split on, ordered by - /// precedence, e.g. ',', ';=', or ',;=' - /// - /// @return List> Nested array with as many levels as there are characters in - /// separators + /// The method uses regular expressions to handle complex cases such as + /// quoted strings and multiple separators. It preserves the structure of + /// the original header while splitting it into logical parts. static List> split(String header, String separators) { if (separators.isEmpty) { 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 /// 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 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']]) /// // => {'foo': 'abc', 'bar': true} static Map combine(List> parts) { @@ -73,12 +119,21 @@ class HeaderUtils { /// 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 - /// are joined with the specified separator and an additional space (for - /// readability). Values are quoted if necessary. + /// This method takes a Map of key-value pairs and joins them into a single string, + /// suitable for use in an HTTP header. Each key-value pair is formatted as follows: + /// - 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 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: - /// /// HeaderUtils.headerToString({'foo': 'abc', 'bar': true, 'baz': 'a b c'}, ',') /// // => 'foo=abc, bar, baz="a b c"' static String headerToString(Map assoc, String separator) { @@ -93,11 +148,28 @@ class HeaderUtils { 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 - /// the HTTP specification, it is backslash-escaped and enclosed in quotes - /// to match the "quoted-string" construct. + /// This method takes a string and determines whether it needs to be quoted + /// for use in HTTP headers. If quoting is necessary, it encloses the string + /// 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) { if (s == null) { throw ArgumentError('Input string cannot be null'); @@ -116,30 +188,79 @@ class HeaderUtils { 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) { final pattern = RegExp('^[a-zA-Z0-9!#\$%&\'*+\\-\\.^_`|~]+\$'); 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 - /// defined in the HTTP specification), it is passed through verbatim. + /// This method processes a string that may have been quoted or contain + /// 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) { 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" - /// @param String filename A unicode string - /// @param String filenameFallback A string containing only ASCII characters that - /// is semantically equivalent to filename. If the filename is already ASCII, - /// it can be omitted, or just copied from filename + /// This method creates a properly formatted Content-Disposition header value + /// based on the given disposition type and filename. It supports both ASCII + /// and non-ASCII filenames, providing a fallback for older user agents. /// - /// @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 + /// + /// 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 = '']) { if (![DISPOSITION_ATTACHMENT, DISPOSITION_INLINE].contains(disposition)) { 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. +/// 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 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 parseQuery(String query, [bool ignoreBrackets = false, String separator = '&']) { final result = {}; @@ -213,7 +364,26 @@ static Map parseQuery(String query, [bool ignoreBrackets = fals 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> _groupParts(List matches, String separators, [bool first = true]) { final separator = separators[0]; separators = separators.substring(1);