1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit;
16
17 import java.io.Serializable;
18 import java.util.Collections;
19 import java.util.Date;
20 import java.util.LinkedHashSet;
21 import java.util.Objects;
22 import java.util.Set;
23 import java.util.concurrent.locks.ReentrantReadWriteLock;
24
25 import org.htmlunit.http.Cookie;
26
27 /**
28 * Manages cookies for a {@link WebClient}.
29 *
30 * <p>This class is thread-safe: all mutator and accessor operations are
31 * protected via a {@link ReentrantReadWriteLock}.
32 * </p>
33 *
34 * <p>Cookie support can be turned off via {@link #setCookiesEnabled(boolean)}.
35 * While disabled, this manager ignores all cookie operations: additions and
36 * removals become no-ops, and accessors behave as if the cookie store were
37 * empty. Subclasses that override these methods must preserve this contract.
38 * </p>
39 *
40 * @author Daniel Gredler
41 * @author Ahmed Ashour
42 * @author Nicolas Belisle
43 * @author Ronald Brill
44 */
45 public class CookieManager implements Serializable {
46
47 private final ReentrantReadWriteLock lock_ = new ReentrantReadWriteLock();
48
49 /** Whether or not cookies are enabled. */
50 private boolean cookiesEnabled_;
51
52 /** The cookies added to this cookie manager. */
53 private final Set<Cookie> cookies_ = new LinkedHashSet<>();
54
55 /**
56 * Creates a new instance.
57 */
58 public CookieManager() {
59 cookiesEnabled_ = true;
60 }
61
62 /**
63 * Enables or disables cookie support. Cookies are enabled by default.
64 * Disabling does not clear existing cookies; it only suppresses all
65 * cookie operations until re-enabled.
66 *
67 * @param enabled {@code true} to enable cookie support, {@code false} to disable it
68 */
69 public void setCookiesEnabled(final boolean enabled) {
70 lock_.writeLock().lock();
71 try {
72 cookiesEnabled_ = enabled;
73 }
74 finally {
75 lock_.writeLock().unlock();
76 }
77 }
78
79 /**
80 * Returns {@code true} if cookies are enabled. Cookies are enabled by default.
81 *
82 * @return {@code true} if cookies are enabled, {@code false} otherwise
83 */
84 public boolean isCookiesEnabled() {
85 lock_.readLock().lock();
86 try {
87 return cookiesEnabled_;
88 }
89 finally {
90 lock_.readLock().unlock();
91 }
92 }
93
94 /**
95 * Returns a snapshot of the currently configured cookies as an
96 * unmodifiable set. The returned set is a copy and will not reflect
97 * later changes to this manager's cookie store.
98 *
99 * @return the currently configured cookies, in an unmodifiable set;
100 * empty if cookie support is disabled
101 */
102 public Set<Cookie> getCookies() {
103 lock_.readLock().lock();
104 try {
105 if (!cookiesEnabled_) {
106 return Collections.emptySet();
107 }
108
109 final Set<Cookie> copy = new LinkedHashSet<>(cookies_);
110 return Collections.unmodifiableSet(copy);
111 }
112 finally {
113 lock_.readLock().unlock();
114 }
115 }
116
117 /**
118 * Removes all cookies whose expiration date is strictly before the
119 * given date. A cookie expiring at exactly {@code date} is not
120 * considered expired by this comparison (consistent with RFC 6265's
121 * treatment of cookie expiry).
122 *
123 * @param date the date to compare against; if {@code null}, this method
124 * does nothing and returns {@code false}
125 * @return {@code true} if one or more cookies were found expired and removed;
126 * {@code false} otherwise, or if cookie support is disabled
127 */
128 public boolean clearExpired(final Date date) {
129 lock_.writeLock().lock();
130 try {
131 if (!cookiesEnabled_ || date == null) {
132 return false;
133 }
134
135 return cookies_.removeIf(cookie ->
136 cookie.getExpires() != null && date.after(cookie.getExpires()));
137 }
138 finally {
139 lock_.writeLock().unlock();
140 }
141 }
142
143 /**
144 * Returns the currently configured cookie with the specified name, or
145 * {@code null} if none exists. If multiple stored cookies happen to
146 * share a name (e.g., differing by domain or path), the first match
147 * encountered in iteration order is returned.
148 *
149 * @param name the cookie name to look up; may be {@code null}
150 * @return the matching cookie, or {@code null} if none exists or cookie
151 * support is disabled
152 */
153 public Cookie getCookie(final String name) {
154 lock_.readLock().lock();
155 try {
156 if (!cookiesEnabled_) {
157 return null;
158 }
159
160 for (final Cookie cookie : cookies_) {
161 if (Objects.equals(cookie.getName(), name)) {
162 return cookie;
163 }
164 }
165 return null;
166 }
167 finally {
168 lock_.readLock().unlock();
169 }
170 }
171
172 /**
173 * Adds the specified cookie, replacing any existing cookie considered
174 * equal to it. If the cookie is already expired relative to the current
175 * time, it replaces the old entry (if any) but is not itself re-added —
176 * this is the mechanism by which a cookie can be deleted by supplying a
177 * past expiration date, per RFC 6265.
178 *
179 * @param cookie the cookie to add
180 */
181 public void addCookie(final Cookie cookie) {
182 lock_.writeLock().lock();
183 try {
184 if (!cookiesEnabled_) {
185 return;
186 }
187
188 cookies_.remove(cookie);
189
190 // don't add expired cookie
191 if (cookie.getExpires() == null || cookie.getExpires().after(new Date())) {
192 cookies_.add(cookie);
193 }
194 }
195 finally {
196 lock_.writeLock().unlock();
197 }
198 }
199
200 /**
201 * Removes the specified cookie, if present. Does nothing if cookie support
202 * is disabled.
203 *
204 * @param cookie the cookie to remove; may be {@code null}, in which case
205 * this method does nothing
206 */
207 public void removeCookie(final Cookie cookie) {
208 lock_.writeLock().lock();
209 try {
210 if (!cookiesEnabled_) {
211 return;
212 }
213
214 cookies_.remove(cookie);
215 }
216 finally {
217 lock_.writeLock().unlock();
218 }
219 }
220
221 /**
222 * Removes all cookies from this manager. Does nothing if cookie support
223 * is disabled.
224 */
225 public void clearCookies() {
226 lock_.writeLock().lock();
227 try {
228 if (!cookiesEnabled_) {
229 return;
230 }
231
232 cookies_.clear();
233 }
234 finally {
235 lock_.writeLock().unlock();
236 }
237 }
238 }