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.app.web.go;
17  
18  import java.io.IOException;
19  import java.io.UnsupportedEncodingException;
20  import java.net.URLEncoder;
21  import java.util.Map;
22  
23  import org.apache.logging.log4j.LogManager;
24  import org.apache.logging.log4j.Logger;
25  import org.codelibs.core.lang.StringUtil;
26  import org.codelibs.core.net.URLUtil;
27  import org.codelibs.fess.Constants;
28  import org.codelibs.fess.app.web.base.FessSearchAction;
29  import org.codelibs.fess.app.web.error.ErrorAction;
30  import org.codelibs.fess.crawler.util.CharUtil;
31  import org.codelibs.fess.helper.PathMappingHelper;
32  import org.codelibs.fess.helper.SearchLogHelper;
33  import org.codelibs.fess.helper.ViewHelper;
34  import org.codelibs.fess.opensearch.log.exentity.ClickLog;
35  import org.codelibs.fess.util.ComponentUtil;
36  import org.codelibs.fess.util.DocumentUtil;
37  import org.dbflute.util.DfTypeUtil;
38  import org.lastaflute.web.Execute;
39  import org.lastaflute.web.response.ActionResponse;
40  import org.lastaflute.web.response.HtmlResponse;
41  import org.lastaflute.web.response.StreamResponse;
42  
43  import jakarta.annotation.Resource;
44  
45  /**
46   * Action class for handling document redirection requests.
47   * This action processes "go" requests that redirect users to specific documents
48   * while tracking click events and handling various URL types including file system paths.
49   */
50  public class GoAction extends FessSearchAction {
51  
52      /**
53       * Default constructor for GoAction.
54       */
55      public GoAction() {
56          super();
57      }
58  
59      // ===================================================================================
60      //                                                                            Constant
61      //
62      /** Logger for this class. */
63      private static final Logger logger = LogManager.getLogger(GoAction.class);
64  
65      /** Helper for URL path mapping and transformation. */
66      @Resource
67      protected PathMappingHelper pathMappingHelper;
68  
69      // ===================================================================================
70      //                                                                           Attribute
71      //
72  
73      // ===================================================================================
74      //                                                                               Hook
75      //                                                                              ======
76  
77      // ===================================================================================
78      //                                                                      Search Execute
79      //                                                                      ==============
80      /**
81       * Handles document redirection requests.
82       * Validates the document ID, logs click events if enabled, and redirects
83       * to the target URL or serves file content directly if configured.
84       *
85       * @param form the go form containing document ID and tracking parameters
86       * @return action response for redirection or content streaming
87       * @throws IOException if an I/O error occurs during content retrieval
88       */
89      @Execute
90      public ActionResponse index(final GoForm form) throws IOException {
91          validate(form, messages -> {}, () -> asHtml(virtualHost(path_Error_ErrorJsp)));
92          if (isLoginRequired()) {
93              return redirectToLogin();
94          }
95  
96          Map<String, Object> doc = null;
97          try {
98              doc = searchHelper
99                      .getDocumentByDocId(form.docId, new String[] { fessConfig.getIndexFieldUrl(), fessConfig.getIndexFieldConfigId() },
100                             getUserBean())
101                     .orElse(null);
102         } catch (final Exception e) {
103             logger.warn("Failed to request: {}", form.docId, e);
104         }
105         if (doc == null) {
106             saveError(messages -> messages.addErrorsDocidNotFound(GLOBAL, form.docId));
107             return redirect(ErrorAction.class);
108         }
109         final String url = DocumentUtil.getValue(doc, fessConfig.getIndexFieldUrl(), String.class);
110         if (url == null) {
111             saveError(messages -> messages.addErrorsDocumentNotFound(GLOBAL, form.docId));
112             return redirect(ErrorAction.class);
113         }
114 
115         if (fessConfig.isSearchLog()) {
116             final String userSessionId = userInfoHelper.getUserCode();
117             if (userSessionId != null) {
118                 final SearchLogHelper searchLogHelper = ComponentUtil.getSearchLogHelper();
119                 final ClickLog clickLog = new ClickLog();
120                 clickLog.setUrlId((String) doc.get(fessConfig.getIndexFieldId()));
121                 clickLog.setUrl(url);
122                 clickLog.setRequestedAt(systemHelper.getCurrentTimeAsLocalDateTime());
123                 clickLog.setQueryRequestedAt(DfTypeUtil.toLocalDateTime(Long.parseLong(form.rt)));
124                 clickLog.setUserSessionId(userSessionId);
125                 clickLog.setDocId(form.docId);
126                 clickLog.setQueryId(form.queryId);
127                 if (form.order != null) {
128                     clickLog.setOrder(form.order);
129                 }
130                 searchLogHelper.addClickLog(clickLog);
131             }
132         }
133 
134         final String targetUrl = pathMappingHelper.replaceUrl(url);
135 
136         String hash;
137         if (StringUtil.isNotBlank(form.hash)) {
138             final String value = URLUtil.decode(form.hash, Constants.UTF_8);
139             if (targetUrl.indexOf('#') == -1) {
140                 final StringBuilder buf = new StringBuilder(value.length() + 100);
141                 for (final char c : value.toCharArray()) {
142                     if (CharUtil.isUrlChar(c) || c == ' ') {
143                         buf.append(c);
144                     } else {
145                         try {
146                             buf.append(URLEncoder.encode(String.valueOf(c), Constants.UTF_8));
147                         } catch (final UnsupportedEncodingException e) {
148                             // NOP
149                         }
150                     }
151                 }
152                 hash = buf.toString();
153             } else {
154                 hash = StringUtil.EMPTY;
155             }
156         } else {
157             hash = StringUtil.EMPTY;
158         }
159 
160         if (!isFileSystemPath(targetUrl)) {
161             if (isValidRedirectUrl(targetUrl)) {
162                 return HtmlResponse.fromRedirectPathAsIs(DocumentUtil.encodeUrl(targetUrl + hash));
163             } else {
164                 logger.warn("Invalid redirect URL detected: {}", targetUrl);
165                 saveError(messages -> messages.addErrorsDocumentNotFound(GLOBAL, form.docId));
166                 return redirect(ErrorAction.class);
167             }
168         }
169         if (!fessConfig.isSearchFileProxyEnabled()) {
170             return HtmlResponse.fromRedirectPathAsIs(targetUrl + hash);
171         }
172         final ViewHelper viewHelper = ComponentUtil.getViewHelper();
173         try {
174             final StreamResponse response = viewHelper.asContentResponse(doc);
175             if (response.getHttpStatus().orElse(200) == 404) {
176                 logger.debug("Document not found: url={}", targetUrl);
177                 saveError(messages -> messages.addErrorsNotFoundOnFileSystem(GLOBAL, targetUrl));
178                 return redirect(ErrorAction.class);
179             }
180             return response;
181         } catch (final Exception e) {
182             logger.warn("Failed to load: {}", doc, e);
183             saveError(messages -> messages.addErrorsNotLoadFromServer(GLOBAL, targetUrl));
184             return redirect(ErrorAction.class);
185         }
186     }
187 
188     /**
189      * Checks if the given URL represents a file system path.
190      * Determines if the URL uses file system protocols that may require
191      * special handling for content serving.
192      *
193      * @param url the URL to check
194      * @return true if the URL is a file system path, false otherwise
195      */
196     protected boolean isFileSystemPath(final String url) {
197         return ComponentUtil.getProtocolHelper().isFileSystemPath(url);
198     }
199 
200     /**
201      * Validates if the URL is safe for redirection.
202      *
203      * @param url the URL to validate
204      * @return true if the URL is valid for redirection, false otherwise
205      */
206     protected boolean isValidRedirectUrl(final String url) {
207         if (StringUtil.isBlank(url)) {
208             return false;
209         }
210         final String lowerUrl = url.toLowerCase();
211         if (lowerUrl.startsWith("http://") || lowerUrl.startsWith("https://")) {
212             return true;
213         }
214         if (lowerUrl.startsWith("javascript:") || lowerUrl.startsWith("data:") || lowerUrl.startsWith("vbscript:")) {
215             return false;
216         }
217         return true;
218     }
219 }