Skip to content
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CHANGELOG

## NEXT

- `CookieJar` now returns `FutureOr` instead of `Future` for all methods.
- Deprecates `delete` in favor of `deleteWhere`.
- Adds `loadAll` to get all saved Cookies.
- Adds `endSession` to delete all session Cookies.

## 4.0.8

- Simply replace `\` to `/` rather than using `Uri` when parsing a storage base directory.
Expand Down
2 changes: 1 addition & 1 deletion example/encryption.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() async {
final cj = PersistCookieJar(ignoreExpires: true, storage: storage);

final uri = Uri.parse('https://xxx.xxx.com/');
await cj.delete(uri);
await cj.deleteWhere((cookie) => cookie.domain == uri.host);
List<Cookie> results;
final cookie = Cookie('test', 'hh')
..expires = DateTime.parse('1970-02-27 13:27:00');
Expand Down
101 changes: 91 additions & 10 deletions lib/src/cookie_jar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:universal_io/io.dart' show Cookie;

import 'jar/default.dart';
Expand All @@ -7,27 +9,106 @@ const _kIsWeb = bool.hasEnvironment('dart.library.js_util')
? bool.fromEnvironment('dart.library.js_util')
: identical(0, 0.0);

/// [CookieJar] is a cookie container and manager for HTTP requests.
/// [CookieJar] is a cookie container and manager for HTTP requests implementing [RFC6265](https://www.rfc-editor.org/rfc/rfc6265.html).
///
/// ## Implementation considerations
/// In most cases it is not needed to implement this interface.
/// Use a `PersistCookieJar` with a custom [Storage] backend.
///
/// ### Cookie value retrieval
/// A cookie jar does not need to retrieve cookies with all attributes present.
/// Retrieved cookies only need to have a valid [Cookie.name] and [Cookie.value].
/// It is up to the implementation to provide further information.
///
/// ### Cookie management
/// According to [RFC6265 section 7.2](https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2)
/// user agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// It must be documented if an implementer does not provide any of the optional
/// [loadAll], [deleteAll] and [deleteWhere] methods.
///
/// ### Public suffix validation
/// The default implementation does not validate the cookie domain against a public
/// suffix list.
/// {@template CookieJar.publicSuffixNote}
/// > NOTE: A "public suffix" is a domain that is controlled by a public
/// > registry, such as "com", "co.uk", and "pvt.k12.wy.us". This step is
/// > essential for preventing attacker.com from disrupting the integrity of
/// > example.com by setting a cookie with a Domain attribute of "com".
/// > Unfortunately, the set of public suffixes (also known as "registry controlled domains")
/// > changes over time. If feasible, user agents SHOULD use an up-to-date
/// > public suffix list, such as the one maintained by the Mozilla project at <http://publicsuffix.org/>.
/// {@endtemplate}
///
/// ### CookieJar limits and eviction policy
/// If a cookie jar has a limit to the number of cookies it can store,
/// the removal policy outlined in [RFC6265 section 5.3](https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3)
/// must be followed:
/// > At any time, the user agent MAY "remove excess cookies" from the cookie store
/// > if the number of cookies sharing a domain field exceeds some implementation-defined
/// > upper bound (such as 50 cookies).
/// >
/// > At any time, the user agent MAY "remove excess cookies" from the cookie store
/// > if the cookie store exceeds some predetermined upper bound (such as 3000 cookies).
/// >
/// > When the user agent removes excess cookies from the cookie store, the user agent MUST
/// > evict cookies in the following priority order:
/// >
/// > Expired cookies.
/// > Cookies that share a domain field with more than a predetermined number of other cookies.
/// > All cookies.
/// >
/// > If two cookies have the same removal priority, the user agent MUST evict the
/// > cookie with the earliest last-access date first.
///
/// It is recommended to set an upper bound to the time a cookie is stored
/// as described in [RFC6265 section 7.3](https://www.rfc-editor.org/rfc/rfc6265.html#section-7.3):
/// > Although servers can set the expiration date for cookies to the distant future,
/// > most user agents do not actually retain cookies for multiple decades.
/// > Rather than choosing gratuitously long expiration periods, servers SHOULD
/// > promote user privacy by selecting reasonable cookie expiration periods based on the purpose of the cookie.
/// > For example, a typical session identifier might reasonably be set to expire in two weeks.
abstract class CookieJar {
/// Creates a [DefaultCookieJar] instance or a dummy [WebCookieJar] if run in a browser.
factory CookieJar({bool ignoreExpires = false}) {
if (_kIsWeb) {
return WebCookieJar();
}
return DefaultCookieJar(ignoreExpires: ignoreExpires);
}

/// Whether the [CookieJar] should ignore expired cookies during saves/loads.
final bool ignoreExpires = false;

/// Save the [cookies] for specified [uri].
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies);
FutureOr<void> saveFromResponse(Uri uri, List<Cookie> cookies);

/// Load the cookies for specified [uri].
Future<List<Cookie>> loadForRequest(Uri uri);
FutureOr<List<Cookie>> loadForRequest(Uri uri);

/// Ends the current session deleting all session cookies.
FutureOr<void> endSession();

/// Loads all cookies in the jar.
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<List<Cookie>> loadAll();

/// Delete all cookies in the [CookieJar].
Future<void> deleteAll();
/// Deletes all cookies in the jar.
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<void> deleteAll();

/// Delete cookies with the specified [uri].
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]);
/// Removes all cookies in this store that satisfy the given [test].
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie store.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<void> deleteWhere(bool Function(Cookie cookie) test);
}
54 changes: 48 additions & 6 deletions lib/src/jar/default.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:universal_io/io.dart' show Cookie;

import '../cookie_jar.dart';
Expand All @@ -10,10 +12,14 @@ import '../serializable_cookie.dart';
/// cleared after the app exited.
///
/// In order to save cookies into storages, use [PersistCookieJar] instead.
///
/// ### Public suffix validation
/// This cookie jar implementation does not validate the cookie domain against a
/// public suffix list.
/// {@macro CookieJar.publicSuffixNote}
class DefaultCookieJar implements CookieJar {
DefaultCookieJar({this.ignoreExpires = false});

@override
final bool ignoreExpires;

/// An array to save cookies.
Expand Down Expand Up @@ -88,7 +94,7 @@ class DefaultCookieJar implements CookieJar {
}

@override
Future<List<Cookie>> loadForRequest(Uri uri) async {
FutureOr<List<Cookie>> loadForRequest(Uri uri) {
final list = <Cookie>[];
final urlPath = uri.path;
// Load cookies without "domain" attribute, include port.
Expand Down Expand Up @@ -137,7 +143,7 @@ class DefaultCookieJar implements CookieJar {
}

@override
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {
FutureOr<void> saveFromResponse(Uri uri, List<Cookie> cookies) {
for (final cookie in cookies) {
String? domain = cookie.domain;
String path;
Expand Down Expand Up @@ -173,8 +179,8 @@ class DefaultCookieJar implements CookieJar {
/// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
///
/// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
@Deprecated('Use deleteWhere instead')
FutureOr<void> delete(Uri uri, [bool withDomainSharedCookie = false]) {
final host = uri.host;
hostCookies.remove(host);
if (withDomainSharedCookie) {
Expand All @@ -186,7 +192,7 @@ class DefaultCookieJar implements CookieJar {

/// Delete all cookies stored in the memory.
@override
Future<void> deleteAll() async {
FutureOr<void> deleteAll() {
domainCookies.clear();
hostCookies.clear();
}
Expand All @@ -206,4 +212,40 @@ class DefaultCookieJar implements CookieJar {
final list = path.split('/')..removeLast();
return list.join('/');
}

@override
void deleteWhere(bool Function(Cookie cookie) test) {
// Traverse all managed cookies and delete entries matching `test`.
for (final group in _cookies) {
for (final domainPair in group.values) {
for (final pathPair in domainPair.values) {
pathPair.removeWhere((key, value) => test(value.cookie));
}
}
}
}

@override
void endSession() {
deleteWhere((cookie) {
return cookie.expires == null && cookie.maxAge == null;
});
}

@override
FutureOr<List<Cookie>> loadAll() {
final list = <Cookie>[];

for (final group in _cookies) {
for (final domainPair in group.values) {
for (final pathPair in domainPair.values) {
for (final value in pathPair.values) {
list.add(value.cookie);
}
}
}
}

return list;
}
}
12 changes: 11 additions & 1 deletion lib/src/jar/persist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'default.dart';
/// [PersistCookieJar] is a cookie manager which implements
/// the standard cookie policy declared in RFC.
/// [PersistCookieJar] persists the cookies in files, if the application exit,
/// the cookies always exist unless user explicitly called [delete].
/// the cookies always exist unless user explicitly deleted with [deleteWhere].
class PersistCookieJar extends DefaultCookieJar {
/// [persistSession] is whether persisting the cookies that without
/// "expires" or "max-age" attribute.
Expand Down Expand Up @@ -157,6 +157,7 @@ class PersistCookieJar extends DefaultCookieJar {
/// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
///
/// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
@Deprecated('Use deleteWhere instead')
@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
await _checkInitialized();
Expand All @@ -171,6 +172,15 @@ class PersistCookieJar extends DefaultCookieJar {
}
}

@override
Future<void> deleteWhere(bool Function(Cookie cookie) test) async {
await _checkInitialized();
super.deleteWhere(test);

await storage.write(_indexKey, json.encode(_hostSet.toList()));
await storage.write(_domainsKey, json.encode(domainCookies));
}

/// Delete all cookies files in the [storage] and the memory.
@override
Future<void> deleteAll() async {
Expand Down
15 changes: 9 additions & 6 deletions lib/src/jar/web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import '../cookie_jar.dart';
/// A [WebCookieJar] will do nothing to handle cookies
/// since they are already handled by XHRs.
class WebCookieJar implements CookieJar {
WebCookieJar({this.ignoreExpires = false});
WebCookieJar();

@override
final bool ignoreExpires;
void deleteWhere(bool Function(Cookie cookie) test) {}

@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {}
void deleteAll() {}

@override
Future<void> deleteAll() async {}
List<Cookie> loadForRequest(Uri uri) => [];

@override
Future<List<Cookie>> loadForRequest(Uri uri) async => [];
void saveFromResponse(Uri uri, List<Cookie> cookies) {}

@override
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {}
void endSession() {}

@override
List<Cookie> loadAll() => [];
}
44 changes: 38 additions & 6 deletions migration_guide.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Migration Guide

This document gathered all breaking changes and migrations requirement between versions.
This document gathered all breaking changes and migration requirements between versions.

<!--
When new content need to be added to the migration guide, make sure they're following the format:
Expand All @@ -10,27 +10,59 @@ When new content need to be added to the migration guide, make sure they're foll

## Breaking versions

# NEXT

Version 5.0 brings a few refinements to the `CookieJar` interface.
Breaking changes include:

- Usage of `FutureOr` in interfaces.
Going forward a `CookieJar` can also return synchronously. If every call is
properly awaited, nothing should break.
Usage in an `unawaited` method is no longer possible. The `WebCookieJar` has
been migrated to always complete synchronously.

- Changing Cookie deletion:
To allow implementers further flexibility the `delete` method has been removed
from the `CookieJar` interface. Users should migrate to the more flexible
`deleteWhere` method:
```dart
final jar = CookieJar();
// Check what Cookies you want to have deleted.
jar.deleteWhere((cookie) {
return cookie.domain == 'example.com' || cookie.name == 'cookie1';
}));
```

- Optional Cookie management interface:
Cookie management interfaces like `deleteAll`, `deleteWhere` or `loadAll` have
been made optional. It is up to the implementer to support these operations.
Consult your implementers' documentation.

- Optional extra Cookie parameters:
When loading Cookies in any way from the store (`loadForRequest`, `deleteWhere` or `loadAll`)
implementers only have to provide the `Cookie.name` and `Cookie.value` attributes.

- [4.0.0](#400)

# 4.0.0

In version 3.0, when the path of cookie is not specified, '/' will be used.
In version 3.0, when the path of Cookie is not specified, '/' will be used.
However, this is not standard.
In this case, the current URL path should be used.
Therefore, in 4.x, we updated it, but this will cause the
**PersistCookieJar in 4.x to be incompatible with the PersistCookieJar in 3.0**,
that is say, we cannot continue to read and write old cookies in 4.x.
We can upgrade to 4.x through one of following two steps:
that is to say, we cannot continue to read and write old Cookies in 4.x.
We can upgrade to 4.x through one of the following two steps:

- Delete old cookies (Note: The following code should only be executed once):
- Delete old Cookies (Note: The following code should only be executed once):
```dart
// `ignoreExpires` and `oldPath` must be same with 3.x .
PersistCookieJar(
ignoreExpires: true,
storage: FileStorage(oldPath),
).deleteAll();
```
- Or use a new cookie file (ignore old cookies).
- Or use a new Cookie file (ignore old Cookies).
```dart
PersistCookieJar(
// The path should be different from that in 3.x。
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: cookie_jar
description: A cookie manager for http requests in Dart, help you to deal with the cookie policies and persistence.
version: 4.0.8
version: 5.0.0-dev.1
repository: https://github.com/flutterchina/cookie_jar
issue_tracker: https://github.com/flutterchina/cookie_jar/issues

environment:
sdk: '>=2.15.0 <3.0.0'
sdk: ">=2.15.0 <4.0.0"

dependencies:
meta: ^1.5.0
Expand Down
Loading