View Javadoc
1   /*
2    * Copyright 2012-2021 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.admin.design;
17  
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.UnsupportedEncodingException;
21  import java.net.URLDecoder;
22  import java.util.ArrayList;
23  import java.util.List;
24  import java.util.Locale;
25  
26  import org.apache.commons.io.FileUtils;
27  import org.apache.logging.log4j.LogManager;
28  import org.apache.logging.log4j.Logger;
29  import org.codelibs.core.io.FileUtil;
30  import org.codelibs.core.io.ResourceUtil;
31  import org.codelibs.core.lang.StringUtil;
32  import org.codelibs.core.misc.Pair;
33  import org.codelibs.fess.Constants;
34  import org.codelibs.fess.annotation.Secured;
35  import org.codelibs.fess.app.web.base.FessAdminAction;
36  import org.codelibs.fess.exception.FessSystemException;
37  import org.codelibs.fess.util.ComponentUtil;
38  import org.dbflute.optional.OptionalEntity;
39  import org.lastaflute.web.Execute;
40  import org.lastaflute.web.response.HtmlResponse;
41  import org.lastaflute.web.response.StreamResponse;
42  import org.lastaflute.web.ruts.process.ActionRuntime;
43  
44  /**
45   * @author codelibs
46   * @author jflute
47   */
48  public class AdminDesignAction extends FessAdminAction {
49  
50      public static final String ROLE = "admin-design";
51  
52      private static final Logger logger = LogManager.getLogger(AdminDesignAction.class);
53  
54      // ===================================================================================
55      //                                                                           Attribute
56      //                                                                           =========
57  
58      // ===================================================================================
59      //                                                                               Hook
60      //                                                                              ======
61  
62      @Override
63      protected void setupHtmlData(final ActionRuntime runtime) {
64          super.setupHtmlData(runtime);
65          runtime.registerData("fileNameItems", loadFileNameItems());
66          runtime.registerData("jspFileNameItems", loadJspFileNameItems());
67          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDesign()));
68      }
69  
70      @Override
71      protected String getActionRole() {
72          return ROLE;
73      }
74  
75      private List<Pair<String, String>> loadJspFileNameItems() {
76          final List<Pair<String, String>> jspItems = new ArrayList<>();
77          for (final Pair<String, String> p : systemHelper.getDesignJspFileNames()) {
78              jspItems.add(new Pair<>(":" + p.getFirst(), "/" + p.getSecond()));
79          }
80          for (String key : ComponentUtil.getVirtualHostHelper().getVirtualHostPaths()) {
81              if (StringUtil.isBlank(key)) {
82                  key = "/";
83              }
84              for (final Pair<String, String> p : systemHelper.getDesignJspFileNames()) {
85                  jspItems.add(new Pair<>(key + ":" + p.getFirst(), key + "/" + p.getSecond()));
86              }
87          }
88          return jspItems;
89      }
90  
91      private List<String> loadFileNameItems() {
92          final File baseDir = new File(getServletContext().getRealPath("/"));
93          final List<String> fileNameItems = new ArrayList<>();
94          final List<File> fileList = getAccessibleFileList(baseDir);
95          final int length = baseDir.getAbsolutePath().length();
96          for (final File file : fileList) {
97              fileNameItems.add(file.getAbsolutePath().substring(length));
98          }
99          return fileNameItems;
100     }
101 
102     // ===================================================================================
103     //                                                                             Execute
104     //                                                                             =======
105     @Execute
106     @Secured({ ROLE, ROLE + VIEW })
107     public HtmlResponse index() {
108         saveToken();
109         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class);
110     }
111 
112     @Execute
113     @Secured({ ROLE, ROLE + VIEW })
114     public HtmlResponse back() {
115         saveToken();
116         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class);
117     }
118 
119     @Execute
120     @Secured({ ROLE })
121     public HtmlResponse upload(final UploadForm form) {
122         validate(form, messages -> {}, () -> asListHtml(form));
123         verifyToken(this::asListHtml);
124         final String uploadedFileName = form.designFile.getFileName();
125         String fileName = form.designFileName;
126         if (StringUtil.isBlank(fileName)) {
127             fileName = uploadedFileName;
128             try {
129                 int pos = fileName.indexOf('/');
130                 if (pos >= 0) {
131                     fileName = fileName.substring(pos + 1);
132                 }
133                 pos = fileName.indexOf('\\');
134                 if (pos >= 0) {
135                     fileName = fileName.substring(pos + 1);
136                 }
137             } catch (final Exception e) {
138                 throwValidationError(messages -> messages.addErrorsDesignFileNameIsInvalid("designFile"), this::asListHtml);
139             }
140         }
141         if (StringUtil.isBlank(fileName)) {
142             throwValidationError(messages -> messages.addErrorsDesignFileNameIsNotFound("designFile"), this::asListHtml);
143         }
144 
145         File uploadFile;
146         // normalize filename
147         if (checkFileType(fileName, fessConfig.getSupportedUploadedMediaExtentionsAsArray())
148                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedMediaExtentionsAsArray())) {
149             uploadFile = new File(getServletContext().getRealPath("/images/" + fileName));
150         } else if (checkFileType(fileName, fessConfig.getSupportedUploadedCssExtentionsAsArray())
151                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedCssExtentionsAsArray())) {
152             uploadFile = new File(getServletContext().getRealPath("/css/" + fileName));
153         } else if (checkFileType(fileName, fessConfig.getSupportedUploadedJsExtentionsAsArray())
154                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedJsExtentionsAsArray())) {
155             uploadFile = new File(getServletContext().getRealPath("/js/" + fileName));
156         } else if (fessConfig.isSupportedUploadedFile(fileName) || fessConfig.isSupportedUploadedFile(uploadedFileName)) {
157             uploadFile = ResourceUtil.getResourceAsFileNoException(fileName);
158             if (uploadFile == null) {
159                 throwValidationError(messages -> messages.addErrorsDesignFileNameIsNotFound("designFileName"), this::asListHtml);
160                 return null;
161             }
162         } else {
163             throwValidationError(messages -> messages.addErrorsDesignFileIsUnsupportedType("designFileName"), this::asListHtml);
164             return null;
165         }
166 
167         final File parentFile = uploadFile.getParentFile();
168         if (!parentFile.exists() && !parentFile.mkdirs()) {
169             logger.warn("Could not create {}", parentFile.getAbsolutePath());
170         }
171 
172         try {
173             write(uploadFile.getAbsolutePath(), form.designFile.getFileData());
174             final String currentFileName = fileName;
175             saveInfo(messages -> messages.addSuccessUploadDesignFile(GLOBAL, currentFileName));
176         } catch (final Exception e) {
177             logger.error("Failed to write an image file: {}", fileName, e);
178             throwValidationError(messages -> messages.addErrorsFailedToWriteDesignImageFile(GLOBAL), this::asListHtml);
179         }
180         return redirect(getClass());
181     }
182 
183     private boolean checkFileType(final String fileName, final String[] exts) {
184         if (fileName == null) {
185             return false;
186         }
187         final String lFileName = fileName.toLowerCase(Locale.ENGLISH);
188         for (final String ext : exts) {
189             if (lFileName.endsWith("." + ext)) {
190                 return true;
191             }
192         }
193         return false;
194     }
195 
196     @Execute
197     @Secured({ ROLE, ROLE + VIEW })
198     public StreamResponse download(final FileAccessForm form) {
199         final File file = getTargetFile(form.fileName).get();
200         if (file == null) {
201             throwValidationError(messages -> messages.addErrorsTargetFileDoesNotExist(GLOBAL, form.fileName), this::asListHtml);
202             return null;
203         }
204         validate(form, messages -> {}, this::asListHtml);
205         verifyTokenKeep(this::asListHtml);
206         return asStream(file.getName()).contentTypeOctetStream().stream(out -> {
207             try (FileInputStream fis = new FileInputStream(file)) {
208                 out.write(fis);
209             }
210         });
211     }
212 
213     @Execute
214     @Secured({ ROLE })
215     public HtmlResponse delete(final FileAccessForm form) {
216         getTargetFile(form.fileName).ifPresent(file -> {
217             if (!file.delete()) {
218                 logger.error("Failed to delete {}", file.getAbsolutePath());
219                 throwValidationError(messages -> messages.addErrorsFailedToDeleteFile(GLOBAL, form.fileName), this::asListHtml);
220             }
221         }).orElse(() -> {
222             throwValidationError(messages -> messages.addErrorsTargetFileDoesNotExist(GLOBAL, form.fileName), this::asListHtml);
223         });
224         saveInfo(messages -> messages.addSuccessDeleteFile(GLOBAL, form.fileName));
225         validate(form, messages -> {}, this::asListHtml);
226         verifyToken(this::asListHtml);
227         return redirect(getClass());
228     }
229 
230     // -----------------------------------------------------
231     //                                                 Edit
232     //                                                ------
233     @Execute
234     @Secured({ ROLE })
235     public HtmlResponse edit(final EditForm form) {
236         final String jspType = "view";
237         final File jspFile = getJspFile(form.fileName, jspType);
238         try {
239             form.content = new String(FileUtil.readBytes(jspFile), Constants.UTF_8);
240         } catch (final UnsupportedEncodingException e) {
241             throw new FessSystemException("Invalid encoding", e);
242         }
243         saveToken();
244         return asEditHtml(form);
245     }
246 
247     @Execute
248     @Secured({ ROLE })
249     public HtmlResponse editAsUseDefault(final EditForm form) {
250         final String jspType = "orig/view";
251         final File jspFile = getJspFile(form.fileName, jspType);
252         try {
253             form.content = new String(FileUtil.readBytes(jspFile), Constants.UTF_8);
254         } catch (final UnsupportedEncodingException e) {
255             throw new FessSystemException("Invalid encoding", e);
256         }
257         saveToken();
258         return asEditHtml(form);
259     }
260 
261     @Execute
262     @Secured({ ROLE })
263     public HtmlResponse update(final EditForm form) {
264         final String jspType = "view";
265         final File jspFile = getJspFile(form.fileName, jspType);
266 
267         if (form.content == null) {
268             form.content = StringUtil.EMPTY;
269         }
270 
271         validate(form, messages -> {}, () -> asEditHtml(form));
272         verifyToken(() -> asEditHtml(form));
273         try {
274             write(jspFile.getAbsolutePath(), form.content.getBytes(Constants.UTF_8));
275             saveInfo(messages -> messages.addSuccessUpdateDesignJspFile(GLOBAL, jspFile.getAbsolutePath()));
276         } catch (final Exception e) {
277             logger.error("Failed to update {}", form.fileName, e);
278             throwValidationError(messages -> messages.addErrorsFailedToUpdateJspFile(GLOBAL), this::asListHtml);
279         }
280         return redirect(getClass());
281     }
282 
283     // ===================================================================================
284     //                                                                        Assist Logic
285     //                                                                        ============
286     private OptionalEntity<File> getTargetFile(final String fileName) {
287         final File baseDir = new File(getServletContext().getRealPath("/"));
288         final File targetFile = new File(getServletContext().getRealPath(fileName));
289         final List<File> fileList = getAccessibleFileList(baseDir);
290         for (final File file : fileList) {
291             if (targetFile.equals(file)) {
292                 return OptionalEntity.of(targetFile);
293             }
294         }
295         return OptionalEntity.empty();
296     }
297 
298     private List<File> getAccessibleFileList(final File baseDir) {
299         final List<File> fileList = new ArrayList<>(
300                 FileUtils.listFiles(new File(baseDir, "images"), fessConfig.getSupportedUploadedMediaExtentionsAsArray(), true));
301         fileList.addAll(FileUtils.listFiles(new File(baseDir, "css"), fessConfig.getSupportedUploadedCssExtentionsAsArray(), true));
302         fileList.addAll(FileUtils.listFiles(new File(baseDir, "js"), fessConfig.getSupportedUploadedJsExtentionsAsArray(), true));
303         return fileList;
304     }
305 
306     private File getJspFile(final String fileName, final String jspType) {
307         try {
308             final String[] values = URLDecoder.decode(fileName, Constants.UTF_8).split(":");
309             if (values.length != 2) {
310                 throwValidationError(messages -> messages.addErrorsInvalidDesignJspFileName(GLOBAL), this::asListHtml);
311             }
312             final String jspFileName = systemHelper.getDesignJspFileName(values[1]);
313             if (jspFileName == null) {
314                 throwValidationError(messages -> messages.addErrorsInvalidDesignJspFileName(GLOBAL), this::asListHtml);
315             }
316             String path;
317             if ("view".equals(jspType)) {
318                 path = "/WEB-INF/" + jspType + values[0] + "/" + jspFileName;
319             } else {
320                 path = "/WEB-INF/" + jspType + "/" + jspFileName;
321             }
322             final File jspFile = new File(getServletContext().getRealPath(path));
323             if (!jspFile.exists()) {
324                 throwValidationError(messages -> messages.addErrorsDesignJspFileDoesNotExist(GLOBAL), this::asListHtml);
325             }
326             return jspFile;
327         } catch (final UnsupportedEncodingException e) {
328             throw new FessSystemException("Failed to decode " + fileName, e);
329         }
330     }
331 
332     // ===================================================================================
333     //                                                                        Small Helper
334     //                                                                        ============
335 
336     private HtmlResponse asListHtml() {
337         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class);
338     }
339 
340     private HtmlResponse asListHtml(final UploadForm uploadForm) {
341         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class, setup -> {
342             setup.setup(form -> {
343                 copyBeanToBean(uploadForm, form, op -> op.include("designFile", "designFileName"));
344             });
345         });
346     }
347 
348     private HtmlResponse asEditHtml(final EditForm form) {
349         return asHtml(path_AdminDesign_AdminDesignEditJsp).renderWith(data -> {
350             data.register("displayFileName", getJspFile(form.fileName, "view").getAbsolutePath());
351         });
352     }
353 }