Skip to content

Commit b4962a9

Browse files
committed
Working on the cookie container
1 parent e2257ad commit b4962a9

13 files changed

+611
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Released on Thursday, May 2 2019.
99
- Added ability for binary data restore on assets (#19)
1010
- Added `DownloadAsync` extension method to `IUrlUtilities` elements
1111
- Added more extension methods to `IResponse` (e.g., `SaveToAsync`)
12+
- Introduced improved cookie container (#15)
1213

1314
# 0.10.1
1415

src/AngleSharp.Io.Tests/AngleSharp.Io.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<SignAssembly>true</SignAssembly>
55
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
66
<IsPackable>false</IsPackable>
7+
<LangVersion>7.1</LangVersion>
78
</PropertyGroup>
89

910
<ItemGroup>

src/AngleSharp.Io/AngleSharp.Io.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<SignAssembly>true</SignAssembly>
77
<AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
88
<GenerateDocumentationFile>true</GenerateDocumentationFile>
9+
<LangVersion>7.1</LangVersion>
910
</PropertyGroup>
1011

1112
<ItemGroup>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using AngleSharp.Text;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using static Helpers;
8+
using static NetscapeCookieSerializer;
9+
10+
/// <summary>
11+
/// Represents an advanced cookie provider that allows
12+
/// persistence, snapshots, and more.
13+
/// Stores the cookies in the Netscape compatible file
14+
/// format.
15+
/// </summary>
16+
public class AdvancedCookieProvider : ICookieProvider
17+
{
18+
private readonly IFileHandler _handler;
19+
private readonly Boolean _forceParse;
20+
private readonly Boolean _httpOnlyExtension;
21+
private readonly List<WebCookie> _cookies;
22+
23+
/// <summary>
24+
/// Creates a new cookie provider with the given handler and options.
25+
/// </summary>
26+
/// <param name="handler">The handler responsible for file system interaction.</param>
27+
/// <param name="options">The options to use for the cookie provider.</param>
28+
public AdvancedCookieProvider(IFileHandler handler, AdvancedCookieProviderOptions options = default)
29+
{
30+
_handler = handler;
31+
_forceParse = options.IsForceParse;
32+
_httpOnlyExtension = options.IsHttpOnlyExtension;
33+
_cookies = ReadCookies();
34+
}
35+
36+
String ICookieProvider.GetCookie(Url url)
37+
{
38+
throw new NotImplementedException();
39+
}
40+
41+
void ICookieProvider.SetCookie(Url url, String value)
42+
{
43+
throw new NotImplementedException();
44+
}
45+
46+
/// <summary>
47+
/// Gets the available cookies.
48+
/// </summary>
49+
public IEnumerable<WebCookie> Cookies => _cookies.AsEnumerable();
50+
51+
/// <summary>
52+
/// Gets the cookie with the given key.
53+
/// </summary>
54+
/// <param name="domain">The domain of the cookie.</param>
55+
/// <param name="path">The path of the cookie.</param>
56+
/// <param name="key">The key of the cookie.</param>
57+
/// <returns>If matching cookie, if any.</returns>
58+
public WebCookie FindCookie(String domain, String path, String key)
59+
{
60+
domain = CanonicalDomain(domain);
61+
return _cookies.FirstOrDefault(cookie => cookie.Domain.Is(domain) && cookie.Path.Is(path) && cookie.Key.Is(key));
62+
}
63+
64+
/// <summary>
65+
/// Finds all cookies that match the given domain and optional path.
66+
/// </summary>
67+
/// <param name="domain">The domain to look for.</param>
68+
/// <param name="path">The optional path of the cookie.</param>
69+
/// <returns>The matching cookies.</returns>
70+
public IEnumerable<WebCookie> FindCookies(String domain, String path = null)
71+
{
72+
domain = CanonicalDomain(domain);
73+
var enumerable = _cookies.Where(cookie => cookie.Domain.Is(domain));
74+
75+
if (!String.IsNullOrEmpty(path))
76+
{
77+
enumerable = enumerable
78+
.Where(cookie => CheckPaths(path, cookie.Path));
79+
}
80+
81+
return enumerable;
82+
}
83+
84+
/// <summary>
85+
/// Adds a new cookie to the collection.
86+
/// </summary>
87+
/// <param name="cookie">The cookie to add.</param>
88+
public void AddCookie(WebCookie cookie)
89+
{
90+
_cookies.Remove(FindCookie(cookie.Domain, cookie.Path, cookie.Key));
91+
_cookies.Add(cookie);
92+
WriteCookies();
93+
}
94+
95+
/// <summary>
96+
/// Updates an existing cookie with a new cookie.
97+
/// </summary>
98+
/// <param name="oldCookie">The cookie to update.</param>
99+
/// <param name="newCookie">The updated cookie content.</param>
100+
public void UpdateCookie(WebCookie oldCookie, WebCookie newCookie)
101+
{
102+
_cookies.Remove(FindCookie(oldCookie.Domain, oldCookie.Path, oldCookie.Key));
103+
AddCookie(newCookie);
104+
}
105+
106+
/// <summary>
107+
/// Removes a specific cookie matched by its domain, path, and key.
108+
/// </summary>
109+
/// <param name="domain">The domain to look for.</param>
110+
/// <param name="path">The path to look for.</param>
111+
/// <param name="key">The key of the cookie.</param>
112+
/// <returns>The removed cookie if any.</returns>
113+
public WebCookie RemoveCookie(String domain, String path, String key)
114+
{
115+
var cookie = FindCookie(domain, path, key);
116+
117+
if (cookie != null)
118+
{
119+
_cookies.Remove(cookie);
120+
WriteCookies();
121+
}
122+
123+
return cookie;
124+
}
125+
126+
/// <summary>
127+
/// Removes the cookies found for the provided domain and path.
128+
/// </summary>
129+
/// <param name="domain">The domain to look for.</param>
130+
/// <param name="path">The optional path to match.</param>
131+
/// <returns>The removed cookies.</returns>
132+
public IEnumerable<WebCookie> RemoveCookies(String domain, String path = null)
133+
{
134+
var cookies = FindCookies(domain, path);
135+
136+
if (cookies.Any())
137+
{
138+
_cookies.RemoveAll(cookie => cookies.Contains(cookie));
139+
WriteCookies();
140+
}
141+
142+
return cookies;
143+
}
144+
145+
/// <summary>
146+
/// Removes all currently available cookies.
147+
/// </summary>
148+
/// <returns>The removed cookies.</returns>
149+
public IEnumerable<WebCookie> RemoveAllCookies()
150+
{
151+
var cookies = _cookies.ToArray();
152+
_cookies.RemoveAll(cookie => true);
153+
return cookies;
154+
}
155+
156+
private List<WebCookie> ReadCookies() => Deserialize(_handler.ReadFile(), _forceParse, _httpOnlyExtension);
157+
158+
private void WriteCookies()
159+
{
160+
var selection = _cookies.Where(m => m.IsPersistent);
161+
var content = Serialize(selection, _httpOnlyExtension);
162+
_handler.WriteFile(content);
163+
}
164+
}
165+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Options for the AdvancedCookieProvider.
7+
/// </summary>
8+
public struct AdvancedCookieProviderOptions
9+
{
10+
/// <summary>
11+
/// Gets or sets if parsing is forced. If true, errors
12+
/// will be suppressed.
13+
/// </summary>
14+
public Boolean IsForceParse { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets if http only declarations should be
18+
/// allowed.
19+
/// </summary>
20+
public Boolean IsHttpOnlyExtension { get; set; }
21+
}
22+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
internal class CookieParser
4+
{
5+
}
6+
}

src/AngleSharp.Io/Cookie/Helpers.cs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using AngleSharp.Text;
4+
using System;
5+
using System.Text.RegularExpressions;
6+
7+
internal static class Helpers
8+
{
9+
private static readonly DateTime EpochDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
10+
private static readonly Regex NonAscii = new Regex("[^\\u0001-\\u007f]");
11+
private static readonly String[] MonthsAbbr = new[]
12+
{
13+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
14+
};
15+
private static readonly String[] DaysAbbr = new[]
16+
{
17+
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
18+
};
19+
20+
public static Boolean CheckPaths(String requestPath, String cookiePath)
21+
{
22+
if (!cookiePath.Is(requestPath))
23+
{
24+
if (requestPath.StartsWith(cookiePath))
25+
{
26+
if (cookiePath.EndsWith("/"))
27+
{
28+
return true;
29+
}
30+
else if (requestPath.Length > cookiePath.Length && requestPath[cookiePath.Length] == '/')
31+
{
32+
return true;
33+
}
34+
}
35+
36+
return false;
37+
}
38+
39+
return true;
40+
}
41+
42+
public static String CanonicalDomain(String str)
43+
{
44+
if (str != null)
45+
{
46+
// See: S4.1.2.3 & S5.2.3: ignore leading .
47+
str = str.Trim().TrimStart('.');
48+
49+
if (NonAscii.IsMatch(str))
50+
{
51+
str = Punycode.Encode(str);
52+
}
53+
54+
return str.ToLowerInvariant();
55+
}
56+
57+
return str;
58+
}
59+
60+
public static String DecodeURIComponent(String component)
61+
{
62+
var content = component.UrlDecode();
63+
return TextEncoding.Utf8.GetString(content);
64+
}
65+
66+
public static String EncodeURIComponent(String component)
67+
{
68+
var content = TextEncoding.Utf8.GetBytes(component);
69+
return content.UrlEncode();
70+
}
71+
72+
public static Int32 ToEpoch(DateTime? current)
73+
{
74+
if (current.HasValue)
75+
{
76+
var time = current.Value.ToUniversalTime();
77+
return (Int32)Math.Round(time.Subtract(EpochDate).TotalSeconds);
78+
}
79+
80+
return 0;
81+
}
82+
83+
public static DateTime? FromEpoch(Int32 seconds) => EpochDate.AddSeconds(seconds);
84+
85+
public static String FormatDate(DateTime date) => String.Concat(
86+
DaysAbbr[(Int32)date.DayOfWeek],
87+
", ",
88+
date.Day.ToString().PadLeft(2, '0'),
89+
" ",
90+
MonthsAbbr[date.Month],
91+
" ",
92+
date.Year.ToString(),
93+
" ",
94+
date.Hour.ToString().PadLeft(2, '0'),
95+
":",
96+
date.Minute.ToString().PadLeft(2, '0'),
97+
":",
98+
date.Second.ToString().PadLeft(2, '0'),
99+
" GMT"
100+
);
101+
}
102+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// Represents a file handler.
7+
/// </summary>
8+
public interface IFileHandler
9+
{
10+
/// <summary>
11+
/// Reads the (text) content from the file.
12+
/// </summary>
13+
/// <returns>The content of the file.</returns>
14+
String ReadFile();
15+
16+
/// <summary>
17+
/// Writes the (text) content to the file.
18+
/// </summary>
19+
/// <param name="content">The content to write.</param>
20+
void WriteFile(String content);
21+
}
22+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using System;
4+
using System.IO;
5+
6+
/// <summary>
7+
/// Represents a file handler against the local
8+
/// file system.
9+
/// </summary>
10+
public class LocalFileHandler : IFileHandler
11+
{
12+
private readonly String _filePath;
13+
14+
/// <summary>
15+
/// Creates a new local file handler for the given path.
16+
/// </summary>
17+
/// <param name="filePath">The path to resolve to.</param>
18+
public LocalFileHandler(String filePath) => _filePath = filePath;
19+
20+
String IFileHandler.ReadFile() => File.ReadAllText(_filePath);
21+
22+
void IFileHandler.WriteFile(String content)
23+
{
24+
//TODO Replace with queued async method
25+
File.WriteAllText(_filePath, content);
26+
}
27+
}
28+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace AngleSharp.Io.Cookie
2+
{
3+
using System;
4+
5+
/// <summary>
6+
/// A memory file handler to feed the cookie provider.
7+
/// Ideal for testing and sandboxed ("private") browsing.
8+
/// </summary>
9+
public class MemoryFileHandler : IFileHandler
10+
{
11+
private String _content;
12+
13+
/// <summary>
14+
/// Creates a new memory file handler.
15+
/// If no initial content is provided the handler starts empty.
16+
/// </summary>
17+
/// <param name="initialContent">The optional initial content.</param>
18+
public MemoryFileHandler(String initialContent = "") => _content = initialContent;
19+
20+
String IFileHandler.ReadFile() => _content;
21+
22+
void IFileHandler.WriteFile(String content) => _content = content;
23+
}
24+
}

0 commit comments

Comments
 (0)