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