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.base;
17  
18  import static org.codelibs.core.stream.StreamUtil.split;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.nio.file.Path;
23  import java.nio.file.Paths;
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import org.codelibs.core.lang.StringUtil;
28  import org.codelibs.fess.exception.UserRoleLoginException;
29  import org.codelibs.fess.helper.CrawlingConfigHelper;
30  import org.codelibs.fess.helper.PermissionHelper;
31  import org.codelibs.fess.util.ComponentUtil;
32  import org.dbflute.optional.OptionalThing;
33  import org.lastaflute.di.util.LdiFileUtil;
34  import org.lastaflute.web.login.LoginManager;
35  import org.lastaflute.web.response.ActionResponse;
36  import org.lastaflute.web.ruts.process.ActionRuntime;
37  import org.lastaflute.web.util.LaServletContextUtil;
38  import org.lastaflute.web.validation.VaErrorHook;
39  
40  import jakarta.annotation.Resource;
41  import jakarta.servlet.ServletContext;
42  
43  /**
44   * Base action class for admin pages in Fess.
45   * <p>
46   * This abstract class provides common functionality for all admin actions,
47   * including authentication, authorization, and HTML data setup.
48   * </p>
49   *
50   */
51  public abstract class FessAdminAction extends FessBaseAction {
52  
53      /** Constant suffix for view names. */
54      public static final String VIEW = "-view";
55  
56      /**
57       * Default constructor.
58       */
59      public FessAdminAction() {
60          super();
61      }
62  
63      // ===================================================================================
64      //                                                                           Attribute
65      //                                                                           =========
66      /** Helper for crawling configuration management. */
67      @Resource
68      protected CrawlingConfigHelper crawlingConfigHelper;
69  
70      // ===================================================================================
71      //                                                                        Small Helper
72      //                                                                        ============
73  
74      /**
75       * Sets up HTML data for admin pages.
76       * <p>
77       * This method configures common data needed for admin pages including
78       * editable flags, user roles, and forum links.
79       * </p>
80       *
81       * @param runtime the action runtime context
82       */
83      @Override
84      protected void setupHtmlData(final ActionRuntime runtime) {
85          super.setupHtmlData(runtime);
86          systemHelper.setupAdminHtmlData(this, runtime);
87  
88          final Boolean editable =
89                  getUserBean().map(user -> user.hasRoles(fessConfig.getAuthenticationAdminRolesAsArray()) || user.hasRole(getActionRole()))
90                          .orElse(false);
91          runtime.registerData("editable", editable);
92          runtime.registerData("editableClass", editable ? StringUtil.EMPTY : "disabled");
93          runtime.registerData("fesenType", fessConfig.getFesenType());
94          final String forumLink = systemHelper.getForumLink();
95          if (StringUtil.isNotBlank(forumLink)) {
96              runtime.registerData("forumLink", forumLink);
97          }
98      }
99  
100     /**
101      * Get the action role.
102      * @return The action role.
103      */
104     protected abstract String getActionRole();
105 
106     /**
107      * Writes data to the specified file path.
108      *
109      * @param path the file path to write to
110      * @param data the data to write
111      */
112     protected void write(final String path, final byte[] data) {
113         validateFilePath(path);
114         LdiFileUtil.write(path, data);
115     }
116 
117     /**
118      * Validates the file path.
119      *
120      * @param path the file path to validate
121      */
122     protected void validateFilePath(final String path) {
123         if (StringUtil.isBlank(path)) {
124             throw new IllegalArgumentException("File path cannot be blank.");
125         }
126         try {
127             final Path filePath = Paths.get(path).normalize();
128             final String normalizedPath = filePath.toString();
129             if (normalizedPath.contains("..")) {
130                 throw new IllegalArgumentException("Invalid file path: path=" + path);
131             }
132             final File file = filePath.toFile();
133             final String canonicalPath = file.getCanonicalPath();
134             final String[] allowedPathProperties = { "fess.var.path", "fess.webapp.path", "fess.conf.path" };
135             boolean isAllowed = false;
136             final List<String> allowedPaths = new ArrayList<>();
137             for (final String prop : allowedPathProperties) {
138                 final String basePath = System.getProperty(prop);
139                 if (basePath != null) {
140                     final String baseCanonicalPath = new File(basePath).getCanonicalPath();
141                     allowedPaths.add(baseCanonicalPath);
142                     if (canonicalPath.startsWith(baseCanonicalPath)) {
143                         isAllowed = true;
144                         break;
145                     }
146                 }
147             }
148             if (!allowedPaths.isEmpty() && !isAllowed) {
149                 throw new IllegalArgumentException(
150                         "File path is outside allowed directory: path=" + canonicalPath + ", allowed=" + allowedPaths);
151             }
152         } catch (final IOException e) {
153             throw new IllegalArgumentException("Invalid file path: path=" + path, e);
154         }
155     }
156 
157     /**
158      * Gets the servlet context.
159      *
160      * @return the servlet context
161      */
162     protected ServletContext getServletContext() {
163         return LaServletContextUtil.getServletContext();
164     }
165 
166     /**
167      * Verifies that the CRUD mode matches the expected mode.
168      *
169      * @param crudMode the actual CRUD mode
170      * @param expectedMode the expected CRUD mode
171      * @param errorHook the error hook to call if verification fails
172      */
173     protected void verifyCrudMode(final int crudMode, final int expectedMode, final VaErrorHook errorHook) {
174         if (crudMode != expectedMode) {
175             throwValidationError(messages -> {
176                 messages.addErrorsCrudInvalidMode(GLOBAL, String.valueOf(expectedMode), String.valueOf(crudMode));
177             }, errorHook);
178         }
179     }
180 
181     /**
182      * Encodes permission strings into an array.
183      *
184      * @param permissionsText the permissions text (newline-separated)
185      * @return encoded permission array
186      */
187     protected static String[] encodePermissions(final String permissionsText) {
188         final PermissionHelper permissionHelper = ComponentUtil.getPermissionHelper();
189         return split(permissionsText, "\n")
190                 .get(stream -> stream.map(permissionHelper::encode).filter(StringUtil::isNotBlank).distinct().toArray(String[]::new));
191     }
192 
193     // ===================================================================================
194     //                                                                            Document
195     //                                                                            ========
196     /**
197      * {@inheritDoc} <br>
198      * Application Origin Methods:
199      * <pre>
200      * <span style="font-size: 130%; color: #553000">[Small Helper]</span>
201      * o saveInfo() <span style="color: #3F7E5E">// save messages to session</span>
202      * o write() <span style="color: #3F7E5E">// write text to specified file</span>
203      * o copyBeanToBean() <span style="color: #3F7E5E">// copy bean to bean by BeanUtil</span>
204      * o getServletContext() <span style="color: #3F7E5E">// get servlet context</span>
205      * </pre>
206      */
207     @Override
208     public void document1_CallableSuperMethod() {
209         super.document1_CallableSuperMethod();
210     }
211 
212     // ===================================================================================
213     //                                                                           User Info
214     //                                                                           =========
215     /**
216      * Gets the login manager for this admin action.
217      *
218      * @return the login manager wrapped in OptionalThing
219      */
220     @Override
221     protected OptionalThing<LoginManager> myLoginManager() {
222         return OptionalThing.of(fessLoginAssist);
223     }
224 
225     // ===================================================================================
226     //                                                                               Hook
227     //                                                                              ======
228     /**
229      * Handles the prologue phase of action execution.
230      * <p>
231      * This method catches UserRoleLoginException and redirects to the
232      * appropriate action class.
233      * </p>
234      *
235      * @param runtime the action runtime context
236      * @return the action response, or redirect response if login exception occurs
237      */
238     @Override
239     public ActionResponse godHandPrologue(final ActionRuntime runtime) {
240         try {
241             return superGodHandPrologue(runtime);
242         } catch (final UserRoleLoginException e) {
243             activityHelper.accessDenied(getUserBean(), runtime.getRequestPath());
244             return redirect(e.getActionClass());
245         }
246     }
247 
248     /**
249      * Calls the parent's godHandPrologue method.
250      * <p>
251      * This method exists to allow subclasses or tests to override
252      * the behavior of the parent class invocation.
253      * </p>
254      *
255      * @param runtime the action runtime context
256      * @return the action response from the parent
257      */
258     protected ActionResponse superGodHandPrologue(final ActionRuntime runtime) {
259         return super.godHandPrologue(runtime);
260     }
261 
262     /**
263      * Hook method called before action execution.
264      * <p>
265      * This method logs user access activity for the current request.
266      * </p>
267      *
268      * @param runtime the action runtime context
269      * @return the action response from the parent hook
270      */
271     @Override
272     public ActionResponse hookBefore(final ActionRuntime runtime) {
273         final String requestPath = runtime.getRequestPath();
274         final String executeName = runtime.getExecuteMethod().getName();
275         activityHelper.access(getUserBean(), requestPath, executeName);
276         return super.hookBefore(runtime);
277     }
278 
279     /**
280      * Hook method called after action execution completes.
281      * <p>
282      * This method performs cleanup operations by calling the parent hook.
283      * </p>
284      *
285      * @param runtime the action runtime context
286      */
287     @Override
288     public void hookFinally(final ActionRuntime runtime) {
289         super.hookFinally(runtime);
290     }
291 
292 }