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 }