add: adding comments to code files

This commit is contained in:
Patrick Stewart 2024-07-04 22:04:16 -07:00
parent 96296b7b8c
commit 3b00a82cfd

View file

@ -1,31 +1,122 @@
/* /*
* This file is part of the Symfony package. * This file is part of the Protevus Platform.
* This file is a port of the symfony ResponseHeaderBag.php class to Dart
* *
* (c) Fabien Potencier <fabien@symfony.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_bag.dart';
import 'cookie.dart'; import 'cookie.dart';
import 'header_utils.dart'; import 'header_utils.dart';
import 'header_bag.dart';
/// ResponseHeaderBag is a container for Response HTTP headers. /// ResponseHeaderBag is a container for HTTP response headers.
/// ///
/// Author: Fabien Potencier <fabien@symfony.com> /// This class extends HeaderBag and provides additional functionality specific to
/// handling response headers. It includes methods for managing cookies, cache control,
/// and other HTTP response-specific headers.
///
/// Key features:
/// - Manages cookies with support for different domains and paths
/// - Handles cache control headers and directives
/// - Preserves case-sensitive header names
/// - Provides methods for setting, getting, and removing headers
/// - Implements special handling for certain headers like 'Set-Cookie' and 'Cache-Control'
///
/// The class uses several data structures to manage headers efficiently:
/// - [computedCacheControl]: Stores parsed cache control directives
/// - [cookies]: A nested map structure for storing cookies by domain, path, and name
/// - [headerNames]: Preserves the original case of header names
///
/// It also provides constants for cookie formats and content disposition types.
///
/// This class is designed to be used in HTTP response handling, particularly
/// in web frameworks and server-side applications.
class ResponseHeaderBag extends HeaderBag { class ResponseHeaderBag extends HeaderBag {
/// A constant string representing the 'flat' format for cookies.
///
/// This constant is used in methods that deal with cookie formatting,
/// particularly in the `getCookies` method, to specify that cookies
/// should be returned in a flat list structure.
static const String COOKIES_FLAT = 'flat'; static const String COOKIES_FLAT = 'flat';
/// A constant string representing the 'array' format for cookies.
///
/// This constant is used in methods that deal with cookie formatting,
/// particularly in the `getCookies` method, to specify that cookies
/// should be returned in an array structure.
static const String COOKIES_ARRAY = 'array'; static const String COOKIES_ARRAY = 'array';
/// A constant string representing the 'attachment' disposition type for headers.
///
/// This constant is typically used when setting the Content-Disposition header
/// to indicate that the content should be downloaded as an attachment rather
/// than 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 for headers.
///
/// This constant is typically used when setting the Content-Disposition header
/// to indicate that the content should be displayed inline in the browser
/// rather than downloaded as an attachment.
static const String DISPOSITION_INLINE = 'inline'; static const String DISPOSITION_INLINE = 'inline';
/// A map that stores computed cache control directives.
///
/// This map is used to cache the parsed values of the Cache-Control header.
/// The keys are the directive names (e.g., 'max-age', 'public', 'private'),
/// and the values are the corresponding directive values.
///
/// This cache is updated whenever the Cache-Control header is set or modified,
/// and it's used to provide quick access to cache control directives without
/// having to re-parse the header string each time.
Map<String, String> computedCacheControl = {}; Map<String, String> computedCacheControl = {};
/// A nested map structure representing cookies.
///
/// The structure is as follows:
/// - The outermost map's key is the domain (String).
/// - The middle map's key is the path (String).
/// - The innermost map's key is the cookie name (String).
/// - The value of the innermost map is the Cookie object.
///
/// This structure allows for efficient storage and retrieval of cookies
/// based on their domain, path, and name.
Map<String, Map<String, Map<String, Cookie>>> cookies = {}; Map<String, Map<String, Map<String, Cookie>>> cookies = {};
/// A map that stores the original case-sensitive names of headers.
///
/// This map is used to preserve the original capitalization of header names
/// when they are set or retrieved. The keys are the lowercase versions of
/// the header names, and the values are the original case-sensitive names.
///
/// For example, if a header "Content-Type" is set, this map would contain
/// an entry with key "content-type" and value "Content-Type".
///
/// This allows the class to maintain case-insensitive header lookup while
/// still being able to return headers with their original capitalization.
Map<String, String> headerNames = {}; Map<String, String> headerNames = {};
/// Constructor for the ResponseHeaderBag class. /// Constructor for the ResponseHeaderBag class.
///
/// This constructor initializes a new ResponseHeaderBag instance with the given headers.
/// If no headers are provided, an empty map is used.
///
/// The constructor performs two important initializations:
/// 1. If the 'cache-control' header is not present in the provided headers,
/// it sets an empty 'Cache-Control' header.
/// 2. If the 'date' header is not present, it initializes the 'Date' header
/// with the current date and time.
///
/// These initializations ensure that the response complies with RFC2616 - 14.18,
/// which states that all Responses need to have a Date header.
///
/// @param headers An optional map of headers to initialize the ResponseHeaderBag with.
/// If not provided, an empty map is used.
ResponseHeaderBag([Map<String, List<String?>>? headers]) : super(headers ?? {}) { ResponseHeaderBag([Map<String, List<String?>>? headers]) : super(headers ?? {}) {
if (!headers!.containsKey('cache-control')) { if (!headers!.containsKey('cache-control')) {
set('Cache-Control', ''); set('Cache-Control', '');
@ -37,7 +128,14 @@ class ResponseHeaderBag extends HeaderBag {
} }
} }
/// Returns the headers, with original capitalizations. /// Returns all headers with their original case-sensitive names preserved.
///
/// This method creates a new map of headers where the keys (header names)
/// maintain their original capitalization as stored in the [headerNames] map.
/// If a header name is not found in [headerNames], the original name is used.
///
/// @return A Map<String, List<String?>> where keys are the original case-sensitive
/// header names and values are lists of corresponding header values.
Map<String, List<String?>> allPreserveCase() { Map<String, List<String?>> allPreserveCase() {
final headers = <String, List<String?>>{}; final headers = <String, List<String?>>{};
super.all().forEach((name, value) { super.all().forEach((name, value) {
@ -46,7 +144,15 @@ class ResponseHeaderBag extends HeaderBag {
return headers; return headers;
} }
/// Returns the headers with original capitalizations, excluding cookies. /// Returns all headers with their original case-sensitive names preserved, excluding cookies.
///
/// This method creates a new map of headers where the keys (header names)
/// maintain their original capitalization as stored in the [headerNames] map.
/// It uses the [allPreserveCase] method to get all headers, and then removes
/// the 'Set-Cookie' header (if present) from the result.
///
/// @return A Map<String, List<String?>> where keys are the original case-sensitive
/// header names (excluding 'Set-Cookie') and values are lists of corresponding header values.
Map<String, List<String?>> allPreserveCaseWithoutCookies() { Map<String, List<String?>> allPreserveCaseWithoutCookies() {
final headers = allPreserveCase(); final headers = allPreserveCase();
if (headerNames.containsKey('set-cookie')) { if (headerNames.containsKey('set-cookie')) {
@ -56,6 +162,18 @@ class ResponseHeaderBag extends HeaderBag {
} }
/// Replaces the current headers with new headers. /// Replaces the current headers with new headers.
///
/// This method clears the existing headers and replaces them with the provided ones.
/// It also performs the following actions:
/// 1. Resets the [headerNames] map, which is used to preserve the original case of header names.
/// 2. If the new headers don't include a 'cache-control' header, it sets an empty 'Cache-Control' header.
/// 3. If the new headers don't include a 'date' header, it initializes the 'Date' header with the current date and time.
///
/// These actions ensure that the response always has the necessary headers as per HTTP standards,
/// particularly adhering to RFC2616 - 14.18 which requires all Responses to have a Date header.
///
/// @param headers An optional map of headers to replace the current headers.
/// If not provided, an empty map is used.
@override @override
void replace([Map<String, List<String?>>? headers]) { void replace([Map<String, List<String?>>? headers]) {
headerNames = {}; headerNames = {};
@ -69,6 +187,17 @@ class ResponseHeaderBag extends HeaderBag {
} }
/// Returns all headers, optionally filtered by a key. /// Returns all headers, optionally filtered by a key.
///
/// If a [key] is provided, this method returns a map containing only the header
/// for that key. The key is case-insensitive. If the key is 'set-cookie',
/// it returns all cookies formatted as strings.
///
/// If no [key] is provided, it returns all headers, including all cookies
/// under the 'set-cookie' key.
///
/// @param key An optional header key to filter the results.
/// @return A map where keys are header names and values are lists of header values.
/// For 'set-cookie', each value in the list is a formatted cookie string.
@override @override
Map<String, List<String?>> all([String? key]) { Map<String, List<String?>> all([String? key]) {
final headers = super.all(); final headers = super.all();
@ -91,6 +220,22 @@ class ResponseHeaderBag extends HeaderBag {
} }
/// Sets a header value. /// Sets a header value.
///
/// This method sets a header with the given key and value(s). It handles special cases for
/// certain headers, particularly 'set-cookie' and cache-related headers.
///
/// For 'set-cookie':
/// - If replace is true, it clears existing cookies before setting new ones.
/// - It creates Cookie objects from the provided string values.
///
/// For cache-related headers ('cache-control', 'etag', 'last-modified', 'expires'):
/// - It recalculates the Cache-Control header based on the new values.
/// - It updates the computed cache control directives.
///
/// @param key The name of the header to set.
/// @param values The value(s) to set for the header. Can be a single value or a list.
/// @param replace Whether to replace existing values (true) or append to them (false).
/// Defaults to true.
@override @override
void set(String key, dynamic values, [bool replace = true]) { void set(String key, dynamic values, [bool replace = true]) {
final uniqueKey = key.toLowerCase(); final uniqueKey = key.toLowerCase();
@ -116,7 +261,18 @@ class ResponseHeaderBag extends HeaderBag {
} }
} }
/// Removes a header. /// Removes a header from the response.
///
/// This method removes the specified header from the response. It handles special cases for
/// certain headers:
///
/// - For 'set-cookie': Clears all cookies.
/// - For 'cache-control': Clears the computed cache control directives.
/// - For 'date': Reinitializes the Date header with the current date and time.
///
/// The method is case-insensitive for the header name.
///
/// @param key The name of the header to remove.
@override @override
void remove(String key) { void remove(String key) {
final uniqueKey = key.toLowerCase(); final uniqueKey = key.toLowerCase();
@ -134,26 +290,62 @@ class ResponseHeaderBag extends HeaderBag {
} }
} }
/// Checks if the cache-control directive exists. /// Checks if a specific cache-control directive exists in the computed cache control.
///
/// This method checks whether the given [key] exists as a directive in the
/// [computedCacheControl] map. The [computedCacheControl] map contains
/// parsed cache control directives from the Cache-Control header.
///
/// @param key The cache control directive to check for.
/// @return true if the directive exists, false otherwise.
@override @override
bool hasCacheControlDirective(String key) { bool hasCacheControlDirective(String key) {
return computedCacheControl.containsKey(key); return computedCacheControl.containsKey(key);
} }
/// Gets the value of a cache-control directive. /// Retrieves the value of a specific cache-control directive.
///
/// This method returns the value associated with the given [key] from the
/// [computedCacheControl] map. The [computedCacheControl] map contains
/// parsed cache control directives from the Cache-Control header.
///
/// @param key The cache control directive to retrieve.
/// @return The value of the cache control directive, or null if the directive doesn't exist.
@override @override
dynamic getCacheControlDirective(String key) { dynamic getCacheControlDirective(String key) {
return computedCacheControl[key]; return computedCacheControl[key];
} }
/// Sets a cookie. /// Sets a cookie in the ResponseHeaderBag.
///
/// This method adds or updates a cookie in the [cookies] map structure.
/// If the cookie's domain or path doesn't exist in the map, it creates
/// the necessary nested maps. The cookie is then stored using its name as the key.
///
/// Additionally, it updates the [headerNames] map to ensure that 'set-cookie'
/// is mapped to 'Set-Cookie', maintaining proper header capitalization.
///
/// @param cookie The Cookie object to be set in the response.
void setCookie(Cookie cookie) { void setCookie(Cookie cookie) {
cookies.putIfAbsent(cookie.domain ?? '', () => {}) cookies.putIfAbsent(cookie.domain ?? '', () => {})
.putIfAbsent(cookie.path, () => {})[cookie.name] = cookie; .putIfAbsent(cookie.path, () => {})[cookie.name] = cookie;
headerNames['set-cookie'] = 'Set-Cookie'; headerNames['set-cookie'] = 'Set-Cookie';
} }
/// Removes a cookie from the array, but does not unset it in the browser. /// Removes a cookie from the array, but does not unset it in the browser.
///
/// This method removes the specified cookie from the internal [cookies] structure.
/// It does not send any instructions to the browser to delete the cookie.
///
/// The method navigates through the nested map structure of [cookies],
/// removing the cookie and cleaning up empty maps along the way.
///
/// If all cookies are removed, it also removes the 'set-cookie' entry from [headerNames].
///
/// @param name The name of the cookie to remove.
/// @param path The path of the cookie. Defaults to '/'.
/// @param domain The domain of the cookie. If null, it will attempt to remove
/// the cookie from all domains.
void removeCookie(String name, [String? path = '/', String? domain]) { void removeCookie(String name, [String? path = '/', String? domain]) {
path ??= '/'; path ??= '/';
final domainCookies = cookies[domain] ?? {}; final domainCookies = cookies[domain] ?? {};
@ -172,11 +364,19 @@ class ResponseHeaderBag extends HeaderBag {
} }
} }
/// Returns an array with all cookies. /// Returns an array of cookies based on the specified format.
/// ///
/// @return List<Cookie> /// This method retrieves all cookies stored in the ResponseHeaderBag and returns them
/// in the format specified by the [format] parameter.
/// ///
/// @throws ArgumentError When the format is invalid /// @param format The format in which to return the cookies. Can be either
/// [COOKIES_FLAT] (default) or [COOKIES_ARRAY].
/// - [COOKIES_FLAT]: Returns a flat list of all cookies.
/// - [COOKIES_ARRAY]: Returns a list of all cookies without flattening.
///
/// @return A List<Cookie> containing all cookies in the specified format.
///
/// @throws ArgumentError If the provided format is not valid (i.e., not COOKIES_FLAT or COOKIES_ARRAY).
List<Cookie> getCookies([String format = COOKIES_FLAT]) { List<Cookie> getCookies([String format = COOKIES_FLAT]) {
if (!([COOKIES_FLAT, COOKIES_ARRAY].contains(format))) { if (!([COOKIES_FLAT, COOKIES_ARRAY].contains(format))) {
throw ArgumentError('Format "$format" invalid (${[COOKIES_FLAT, COOKIES_ARRAY].join(', ')}).'); throw ArgumentError('Format "$format" invalid (${[COOKIES_FLAT, COOKIES_ARRAY].join(', ')}).');
@ -195,28 +395,59 @@ class ResponseHeaderBag extends HeaderBag {
} }
} }
return flattenedCookies; return flattenedCookies;
} }
/// Clears a cookie in the browser. /// Clears a cookie in the browser.
/// ///
/// @param bool partitioned /// This method sets a cookie with the given name to null and expires it
/// immediately, effectively clearing it from the browser.
///
/// @param name The name of the cookie to clear.
/// @param path The path for which the cookie is valid. Defaults to '/'.
/// @param domain The domain for which the cookie is valid.
/// @param secure Whether the cookie should only be transmitted over secure protocols. Defaults to false.
/// @param httpOnly Whether the cookie should be accessible only through the HTTP protocol. Defaults to true.
/// @param sameSite The SameSite attribute for the cookie. Can be 'Lax', 'Strict', or 'None'.
/// @param partitioned Whether the cookie should be partitioned. Defaults to false.
void clearCookie(String name, void clearCookie(String name,
[String? path = '/', String? domain, bool secure = false, bool httpOnly = true, String? sameSite, bool partitioned = false]) { [String? path = '/', String? domain, bool secure = false, bool httpOnly = true, String? sameSite, bool partitioned = false]) {
final cookieString = '$name=null; Expires=${DateTime.fromMillisecondsSinceEpoch(1).toUtc().toIso8601String()}; Path=${path ?? "/"}; Domain=$domain${secure ? "; Secure" : ""}${httpOnly ? "; HttpOnly" : ""}${sameSite != null ? "; SameSite=$sameSite" : ""}${partitioned ? "; Partitioned" : ""}'; final cookieString = '$name=null; Expires=${DateTime.fromMillisecondsSinceEpoch(1).toUtc().toIso8601String()}; Path=${path ?? "/"}; Domain=$domain${secure ? "; Secure" : ""}${httpOnly ? "; HttpOnly" : ""}${sameSite != null ? "; SameSite=$sameSite" : ""}${partitioned ? "; Partitioned" : ""}';
setCookie(Cookie.fromString(cookieString)); setCookie(Cookie.fromString(cookieString));
} }
/// Makes a disposition header. /// Creates a Content-Disposition header value.
/// ///
/// @see HeaderUtils::makeDisposition() /// This method generates a Content-Disposition header value based on the provided parameters.
/// It uses the HeaderUtils.makeDisposition method to create the header value.
///
/// @param disposition The disposition type, typically either 'attachment' or 'inline'.
/// @param filename The primary filename to be used in the Content-Disposition header.
/// @param filenameFallback An optional fallback filename to be used if the primary filename
/// contains characters not supported by all user agents. Defaults to an empty string.
///
/// @return A string representing the Content-Disposition header value.
String makeDisposition(String disposition, String filename, [String filenameFallback = '']) { String makeDisposition(String disposition, String filename, [String filenameFallback = '']) {
return HeaderUtils.makeDisposition(disposition, filename, filenameFallback); return HeaderUtils.makeDisposition(disposition, filename, filenameFallback);
} }
/// Returns the calculated value of the cache-control header. /// Computes and returns the value for the Cache-Control header.
/// ///
/// This considers several other headers and calculates or modifies the /// This method determines the appropriate Cache-Control value based on the current state
/// cache-control header to a sensible, conservative value. /// of the response headers and the computed cache control directives.
///
/// The logic is as follows:
/// 1. If no cache control directives have been computed:
/// - If 'Last-Modified' or 'Expires' headers are present, it returns 'private, must-revalidate'.
/// - Otherwise, it returns a conservative default of 'no-cache, private'.
/// 2. If cache control directives have been computed:
/// - If 'public' or 'private' directives are present, it returns the current header as-is.
/// - If 's-maxage' is not present, it appends ', private' to the current header.
/// - Otherwise, it returns the current header as-is.
///
/// This method ensures that appropriate caching behavior is set, defaulting to more
/// restrictive caching when specific directives are not explicitly set.
///
/// @return A String representing the computed Cache-Control header value.
String computeCacheControlValue() { String computeCacheControlValue() {
if (computedCacheControl.isEmpty) { if (computedCacheControl.isEmpty) {
if (hasHeader('Last-Modified') || hasHeader('Expires')) { if (hasHeader('Last-Modified') || hasHeader('Expires')) {
@ -240,7 +471,18 @@ class ResponseHeaderBag extends HeaderBag {
return header; return header;
} }
/// Parses the cache-control header value into a map. /// Parses a Cache-Control header string into a map of directives.
///
/// This method takes a Cache-Control header value as input and breaks it down
/// into individual directives. Each directive is then stored in a map where
/// the key is the directive name and the value is the directive's value (if any).
///
/// The method handles both directives with values (e.g., "max-age=3600") and
/// those without values (e.g., "no-cache").
///
/// @param header A string representing the Cache-Control header value.
/// @return A Map<String, String> where keys are directive names and values are
/// directive values. For directives without values, an empty string is used.
Map<String, String> parseCacheControl(String header) { Map<String, String> parseCacheControl(String header) {
final directives = <String, String>{}; final directives = <String, String>{};
for (var directive in header.split(',')) { for (var directive in header.split(',')) {
@ -250,17 +492,40 @@ class ResponseHeaderBag extends HeaderBag {
return directives; return directives;
} }
/// Initializes the Date header to the current date and time. /// Initializes the Date header with the current UTC date and time.
///
/// This method sets the 'Date' header of the HTTP response to the current
/// date and time in UTC format. The date is formatted according to the
/// ISO 8601 standard.
///
/// The Date header is important for HTTP responses as it informs the
/// client about the time at which the response was generated by the server.
/// This can be useful for caching mechanisms and for calculating the age
/// of the response.
void initDate() { void initDate() {
set('Date', DateTime.now().toUtc().toIso8601String()); set('Date', DateTime.now().toUtc().toIso8601String());
} }
/// Checks if a header exists. /// Checks if a header exists in the ResponseHeaderBag.
///
/// This method checks whether a header with the given [key] exists,
/// regardless of the case of the key. It converts the [key] to lowercase
/// before checking, ensuring case-insensitive matching.
///
/// @param key The name of the header to check for.
/// @return true if the header exists, false otherwise.
bool containsKey(String key) { bool containsKey(String key) {
return super.all().containsKey(key.toLowerCase()); return super.all().containsKey(key.toLowerCase());
} }
/// Gets the value of a header. /// Gets the value of a header.
///
/// This method retrieves the value of the header specified by [key].
/// The key is case-insensitive. If the header exists, it returns all values
/// joined by a comma and space. If the header doesn't exist, it returns null.
///
/// @param key The name of the header to retrieve.
/// @return A String containing the header value(s), or null if the header doesn't exist.
String? value(String key) { String? value(String key) {
return super.all()[key.toLowerCase()]?.join(', '); return super.all()[key.toLowerCase()]?.join(', ');
} }