View Javadoc
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 }