1 /*
2 * Copyright 2012-2025 CodeLibs Project and the Others.
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 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13 * either express or implied. See the License for the specific language
14 * governing permissions and limitations under the License.
15 */
16 package org.codelibs.fess.helper;
17
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.UUID;
22
23 import org.codelibs.core.collection.LruHashMap;
24 import org.codelibs.core.lang.StringUtil;
25 import org.codelibs.fess.Constants;
26 import org.codelibs.fess.mylasta.direction.FessConfig;
27 import org.codelibs.fess.util.ComponentUtil;
28 import org.lastaflute.core.security.PrimaryCipher;
29 import org.lastaflute.web.login.TypicalUserBean;
30 import org.lastaflute.web.servlet.session.SessionManager;
31 import org.lastaflute.web.util.LaRequestUtil;
32 import org.lastaflute.web.util.LaResponseUtil;
33
34 import jakarta.servlet.http.Cookie;
35 import jakarta.servlet.http.HttpServletRequest;
36 import jakarta.servlet.http.HttpSession;
37
38 /**
39 * Helper class for managing user information and session tracking in Fess search system.
40 * This class handles user identification through cookies, session management, and query tracking.
41 * It provides functionality for generating unique user codes, managing user sessions,
42 * and tracking search result document IDs for analytics and personalization.
43 *
44 */
45 public class UserInfoHelper {
46
47 /**
48 * Default constructor for UserInfoHelper.
49 */
50 public UserInfoHelper() {
51 // Default constructor
52 }
53
54 /** The session attribute key for storing user bean information */
55 protected static final String USER_BEAN = "lastaflute.action.USER_BEAN.FessUserBean";
56
57 /** The maximum size of the result document IDs cache */
58 protected int resultDocIdsCacheSize = 20;
59
60 /** The name of the cookie used for user identification */
61 protected String cookieName = "fsid";
62
63 /** The domain for the user identification cookie */
64 protected String cookieDomain;
65
66 /** The maximum age of the user identification cookie in seconds (default: 1 month) */
67 protected int cookieMaxAge = 30 * 24 * 60 * 60;// 1 month
68
69 /** The path for the user identification cookie */
70 protected String cookiePath = "/";
71
72 /** Whether the user identification cookie should be secure (HTTPS only) */
73 protected Boolean cookieSecure;
74
75 /** Whether the user identification cookie should be HTTP-only */
76 protected boolean httpOnly = true;
77
78 /**
79 * Retrieves the user code for the current request.
80 * The user code is used to uniquely identify users across sessions and requests.
81 * It checks multiple sources in order: request attribute, request parameter, cookie, user bean, or generates a new one.
82 *
83 * @return the user code string, or null if no valid session exists
84 */
85 public String getUserCode() {
86 return LaRequestUtil.getOptionalRequest().map(request -> {
87 String userCode = (String) request.getAttribute(Constants.USER_CODE);
88 if (StringUtil.isNotBlank(userCode)) {
89 return userCode;
90 }
91
92 userCode = getUserCodeFromRequest(request);
93 if (StringUtil.isNotBlank(userCode)) {
94 return userCode;
95 }
96
97 if (!request.isRequestedSessionIdValid()) {
98 return null;
99 }
100
101 userCode = getUserCodeFromCookie(request);
102 if (StringUtil.isBlank(userCode)) {
103 userCode = getUserCodeFromUserBean(request);
104 if (StringUtil.isBlank(userCode)) {
105 userCode = getId();
106 }
107 }
108
109 if (StringUtil.isNotBlank(userCode)) {
110 updateUserSessionId(userCode);
111 }
112 return userCode;
113 }).orElse(null);
114 }
115
116 /**
117 * Extracts the user code from the user bean stored in the session.
118 * This method retrieves the authenticated user information and creates an encrypted user code.
119 *
120 * @param request the HTTP servlet request
121 * @return the user code from the user bean, or null if not found or invalid
122 */
123 protected String getUserCodeFromUserBean(final HttpServletRequest request) {
124 final SessionManager sessionManager = ComponentUtil.getComponent(SessionManager.class);
125 String userCode = sessionManager.getAttribute(USER_BEAN, TypicalUserBean.class)
126 .filter(u -> !Constants.EMPTY_USER_ID.equals(u.getUserId()))
127 .map(u -> u.getUserId().toString())
128 .orElse(StringUtil.EMPTY);
129 if (StringUtil.isBlank(userCode)) {
130 return null;
131 }
132
133 userCode = createUserCodeFromUserId(userCode);
134 request.setAttribute(Constants.USER_CODE, userCode);
135 deleteUserCodeFromCookie(request);
136 return userCode;
137 }
138
139 /**
140 * Creates an encrypted user code from a user ID.
141 * The user ID is encrypted using the primary cipher and validated against the configuration.
142 *
143 * @param userCode the raw user ID to encrypt
144 * @return the encrypted and validated user code, or null if invalid
145 */
146 protected String createUserCodeFromUserId(String userCode) {
147 final FessConfig fessConfig = ComponentUtil.getFessConfig();
148 final PrimaryCipher cipher = ComponentUtil.getPrimaryCipher();
149 userCode = cipher.encrypt(userCode);
150 if (fessConfig.isValidUserCode(userCode)) {
151 return userCode;
152 }
153 return null;
154 }
155
156 /**
157 * Deletes the user code cookie from the client browser.
158 * This method removes the user identification cookie by setting it to an empty value with zero max age.
159 *
160 * @param request the HTTP servlet request
161 */
162 public void deleteUserCodeFromCookie(final HttpServletRequest request) {
163 final String cookieValue = getUserCodeFromCookie(request);
164 if (cookieValue != null) {
165 updateCookie(StringUtil.EMPTY, 0);
166 }
167 }
168
169 /**
170 * Extracts the user code from request parameters.
171 * This method looks for the user code in the request parameters and validates it.
172 *
173 * @param request the HTTP servlet request
174 * @return the user code from request parameters, or null if not found or invalid
175 */
176 protected String getUserCodeFromRequest(final HttpServletRequest request) {
177 final FessConfig fessConfig = ComponentUtil.getFessConfig();
178 final String userCode = request.getParameter(fessConfig.getUserCodeRequestParameter());
179 if (StringUtil.isBlank(userCode)) {
180 return null;
181 }
182
183 if (fessConfig.isValidUserCode(userCode)) {
184 request.setAttribute(Constants.USER_CODE, userCode);
185 return userCode;
186 }
187 return null;
188 }
189
190 /**
191 * Generates a new unique identifier for user tracking.
192 * Creates a UUID and removes hyphens to create a clean identifier string.
193 *
194 * @return a new unique identifier string
195 */
196 protected String getId() {
197 return UUID.randomUUID().toString().replace("-", StringUtil.EMPTY);
198 }
199
200 /**
201 * Updates the user session with the provided user code.
202 * This method registers the user info with the search log helper and updates the cookie.
203 *
204 * @param userCode the user code to associate with the session
205 */
206 protected void updateUserSessionId(final String userCode) {
207 ComponentUtil.getSearchLogHelper().getUserInfo(userCode);
208
209 LaRequestUtil.getOptionalRequest().ifPresent(req -> req.setAttribute(Constants.USER_CODE, userCode));
210
211 updateCookie(userCode, cookieMaxAge);
212 }
213
214 /**
215 * Updates the user identification cookie with the specified user code and max age.
216 * Configures the cookie with security settings including domain, path, secure flag, and HTTP-only flag.
217 *
218 * @param userCode the user code to store in the cookie
219 * @param age the maximum age of the cookie in seconds
220 */
221 protected void updateCookie(final String userCode, final int age) {
222 final Cookie cookie = new Cookie(cookieName, userCode);
223 cookie.setMaxAge(age);
224 cookie.setHttpOnly(httpOnly);
225 if (StringUtil.isNotBlank(cookieDomain)) {
226 cookie.setDomain(cookieDomain);
227 }
228 if (StringUtil.isNotBlank(cookiePath)) {
229 cookie.setPath(cookiePath);
230 }
231 cookie.setSecure(isSecureCookie());
232 LaResponseUtil.getResponse().addCookie(cookie);
233 }
234
235 /**
236 * Determines whether the user identification cookie should be marked as secure.
237 * Checks the configured secure setting or examines request headers to detect HTTPS.
238 *
239 * @return true if the cookie should be secure, false otherwise
240 */
241 protected boolean isSecureCookie() {
242 if (cookieSecure != null) {
243 return cookieSecure;
244 }
245
246 return LaRequestUtil.getOptionalRequest().map(req -> {
247 String forwardedProto = req.getHeader("X-Forwarded-Proto");
248 if ("https".equalsIgnoreCase(forwardedProto)) {
249 return true;
250 }
251 return req.isSecure();
252 }).orElse(false);
253 }
254
255 /**
256 * Extracts the user code from the user identification cookie.
257 * Searches through all request cookies to find the user identification cookie and validates its value.
258 *
259 * @param request the HTTP servlet request
260 * @return the user code from the cookie, or null if not found or invalid
261 */
262 protected String getUserCodeFromCookie(final HttpServletRequest request) {
263 final FessConfig fessConfig = ComponentUtil.getFessConfig();
264 final Cookie[] cookies = request.getCookies();
265 if (cookies != null) {
266 for (final Cookie cookie : cookies) {
267 if (cookieName.equals(cookie.getName()) && fessConfig.isValidUserCode(cookie.getValue())) {
268 return cookie.getValue();
269 }
270 }
271 }
272 return null;
273 }
274
275 /**
276 * Stores the document IDs associated with a search query for tracking purposes.
277 * This method caches the document IDs returned for a specific query to enable click tracking and analytics.
278 *
279 * @param queryId the unique identifier for the search query
280 * @param documentItems the list of document maps containing search results
281 */
282 public void storeQueryId(final String queryId, final List<Map<String, Object>> documentItems) {
283 LaRequestUtil.getOptionalRequest().map(req -> req.getSession(false)).ifPresent(session -> {
284 final FessConfig fessConfig = ComponentUtil.getFessConfig();
285
286 final List<String> docIdList = new ArrayList<>();
287 for (final Map<String, Object> map : documentItems) {
288 final Object docId = map.get(fessConfig.getIndexFieldDocId());
289 if (docId != null && docId.toString().length() > 0) {
290 docIdList.add(docId.toString());
291 }
292 }
293
294 if (!docIdList.isEmpty()) {
295 final Map<String, String[]> resultDocIdsCache = getResultDocIdsCache(session);
296 resultDocIdsCache.put(queryId, docIdList.toArray(new String[docIdList.size()]));
297 }
298 });
299 }
300
301 /**
302 * Retrieves the document IDs associated with a specific query ID.
303 * Used for tracking which documents were displayed for a particular search query.
304 *
305 * @param queryId the unique identifier for the search query
306 * @return an array of document IDs, or an empty array if not found
307 */
308 public String[] getResultDocIds(final String queryId) {
309 return LaRequestUtil.getOptionalRequest().map(req -> req.getSession(false)).map(session -> {
310 final Map<String, String[]> resultUrlCache = getResultDocIdsCache(session);
311 final String[] urls = resultUrlCache.get(queryId);
312 if (urls != null) {
313 return urls;
314 }
315 return StringUtil.EMPTY_STRINGS;
316 }).orElse(StringUtil.EMPTY_STRINGS);
317 }
318
319 /**
320 * Retrieves or creates the result document IDs cache from the session.
321 * The cache is implemented as an LRU map to limit memory usage.
322 *
323 * @param session the HTTP session
324 * @return the result document IDs cache map
325 */
326 private Map<String, String[]> getResultDocIdsCache(final HttpSession session) {
327 @SuppressWarnings("unchecked")
328 Map<String, String[]> resultDocIdsCache = (Map<String, String[]>) session.getAttribute(Constants.RESULT_DOC_ID_CACHE);
329 if (resultDocIdsCache == null) {
330 resultDocIdsCache = new LruHashMap<>(resultDocIdsCacheSize);
331 session.setAttribute(Constants.RESULT_DOC_ID_CACHE, resultDocIdsCache);
332 }
333 return resultDocIdsCache;
334 }
335
336 /**
337 * Sets the maximum size of the result document IDs cache.
338 *
339 * @param resultDocIdsCacheSize the maximum number of entries in the cache
340 */
341 public void setResultDocIdsCacheSize(final int resultDocIdsCacheSize) {
342 this.resultDocIdsCacheSize = resultDocIdsCacheSize;
343 }
344
345 /**
346 * Sets the name of the user identification cookie.
347 *
348 * @param cookieName the name to use for the user identification cookie
349 */
350 public void setCookieName(final String cookieName) {
351 this.cookieName = cookieName;
352 }
353
354 /**
355 * Sets the domain for the user identification cookie.
356 *
357 * @param cookieDomain the domain to use for the user identification cookie
358 */
359 public void setCookieDomain(final String cookieDomain) {
360 this.cookieDomain = cookieDomain;
361 }
362
363 /**
364 * Sets the maximum age of the user identification cookie in seconds.
365 *
366 * @param cookieMaxAge the maximum age in seconds
367 */
368 public void setCookieMaxAge(final int cookieMaxAge) {
369 this.cookieMaxAge = cookieMaxAge;
370 }
371
372 /**
373 * Sets the path for the user identification cookie.
374 *
375 * @param cookiePath the path to use for the user identification cookie
376 */
377 public void setCookiePath(final String cookiePath) {
378 this.cookiePath = cookiePath;
379 }
380
381 /**
382 * Sets whether the user identification cookie should be marked as secure.
383 *
384 * @param cookieSecure true if the cookie should be secure (HTTPS only), false otherwise, or null for auto-detection
385 */
386 public void setCookieSecure(final Boolean cookieSecure) {
387 this.cookieSecure = cookieSecure;
388 }
389
390 /**
391 * Sets whether the user identification cookie should be HTTP-only.
392 *
393 * @param httpOnly true if the cookie should be HTTP-only (not accessible via JavaScript), false otherwise
394 */
395 public void setCookieHttpOnly(final boolean httpOnly) {
396 this.httpOnly = httpOnly;
397 }
398 }