View Javadoc
1   /*
2    * Copyright 2012-2017 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.dbflute.optional.OptionalEntity;
35  import org.lastaflute.web.Execute;
36  import org.lastaflute.web.response.ActionResponse;
37  import org.lastaflute.web.response.HtmlResponse;
38  import org.lastaflute.web.response.StreamResponse;
39  import org.lastaflute.web.ruts.process.ActionRuntime;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  /**
44   * @author codelibs
45   * @author jflute
46   */
47  public class AdminDesignAction extends FessAdminAction {
48  
49      private static final Logger logger = LoggerFactory.getLogger(AdminDesignAction.class);
50  
51      // ===================================================================================
52      //                                                                           Attribute
53      //                                                                           =========
54  
55      // ===================================================================================
56      //                                                                               Hook
57      //                                                                              ======
58      @Override
59      public ActionResponse hookBefore(final ActionRuntime runtime) {
60          checkEditorStatus(runtime);
61          return super.hookBefore(runtime);
62      }
63  
64      private void checkEditorStatus(final ActionRuntime runtime) {
65          if (!editable()) {
66              throwValidationError(messages -> messages.addErrorsDesignEditorDisabled(GLOBAL), () -> asListHtml());
67          }
68      }
69  
70      @Override
71      protected void setupHtmlData(final ActionRuntime runtime) {
72          super.setupHtmlData(runtime);
73          runtime.registerData("editable", editable());
74          runtime.registerData("fileNameItems", loadFileNameItems());
75          runtime.registerData("jspFileNameItems", loadJspFileNameItems());
76          runtime.registerData("helpLink", systemHelper.getHelpLink(fessConfig.getOnlineHelpNameDesign()));
77      }
78  
79      private boolean editable() {
80          return fessConfig.isWebDesignEditorEnabled();
81      }
82  
83      private List<Pair<String, String>> loadJspFileNameItems() {
84          final List<Pair<String, String>> jspItems = new ArrayList<>();
85          for (final Pair<String, String> p : systemHelper.getDesignJspFileNames()) {
86              jspItems.add(new Pair<>(":" + p.getFirst(), "/" + p.getSecond()));
87          }
88          for (String key : fessConfig.getVirtualHostPaths()) {
89              if (StringUtil.isBlank(key)) {
90                  key = "/";
91              }
92              for (final Pair<String, String> p : systemHelper.getDesignJspFileNames()) {
93                  jspItems.add(new Pair<>(key + ":" + p.getFirst(), key + "/" + p.getSecond()));
94              }
95          }
96          return jspItems;
97      }
98  
99      private List<String> loadFileNameItems() {
100         final File baseDir = new File(getServletContext().getRealPath("/"));
101         final List<String> fileNameItems = new ArrayList<>();
102         final List<File> fileList = getAccessibleFileList(baseDir);
103         final int length = baseDir.getAbsolutePath().length();
104         for (final File file : fileList) {
105             fileNameItems.add(file.getAbsolutePath().substring(length));
106         }
107         return fileNameItems;
108     }
109 
110     // ===================================================================================
111     //                                                                             Execute
112     //                                                                             =======
113     @Execute
114     public HtmlResponse index() {
115         saveToken();
116         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class);
117     }
118 
119     @Execute
120     public HtmlResponse back() {
121         saveToken();
122         return asHtml(path_AdminDesign_AdminDesignJsp).useForm(DesignForm.class);
123     }
124 
125     @Execute
126     public HtmlResponse upload(final UploadForm form) {
127         validate(form, messages -> {}, () -> asListHtml(form));
128         verifyToken(() -> asListHtml());
129         final String uploadedFileName = form.designFile.getFileName();
130         String fileName = form.designFileName;
131         if (StringUtil.isBlank(fileName)) {
132             fileName = uploadedFileName;
133             try {
134                 int pos = fileName.indexOf('/');
135                 if (pos >= 0) {
136                     fileName = fileName.substring(pos + 1);
137                 }
138                 pos = fileName.indexOf('\\');
139                 if (pos >= 0) {
140                     fileName = fileName.substring(pos + 1);
141                 }
142             } catch (final Exception e) {
143                 throwValidationError(messages -> messages.addErrorsDesignFileNameIsInvalid("designFile"), () -> asListHtml());
144             }
145         }
146         if (StringUtil.isBlank(fileName)) {
147             throwValidationError(messages -> messages.addErrorsDesignFileNameIsNotFound("designFile"), () -> asListHtml());
148         }
149 
150         File uploadFile;
151         // normalize filename
152         if (checkFileType(fileName, fessConfig.getSupportedUploadedMediaExtentionsAsArray())
153                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedMediaExtentionsAsArray())) {
154             uploadFile = new File(getServletContext().getRealPath("/images/" + fileName));
155         } else if (checkFileType(fileName, fessConfig.getSupportedUploadedCssExtentionsAsArray())
156                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedCssExtentionsAsArray())) {
157             uploadFile = new File(getServletContext().getRealPath("/css/" + fileName));
158         } else if (checkFileType(fileName, fessConfig.getSupportedUploadedJsExtentionsAsArray())
159                 && checkFileType(uploadedFileName, fessConfig.getSupportedUploadedJsExtentionsAsArray())) {
160             uploadFile = new File(getServletContext().getRealPath("/js/" + fileName));
161         } else if (fessConfig.isSupportedUploadedFile(fileName) || fessConfig.isSupportedUploadedFile(uploadedFileName)) {
162             uploadFile = ResourceUtil.getResourceAsFileNoException(fileName);
163             if (uploadFile == null) {
164                 throwValidationError(messages -> messages.addErrorsDesignFileNameIsNotFound("designFileName"), () -> asListHtml());
165                 return null;
166             }
167         } else {
168             throwValidationError(messages -> messages.addErrorsDesignFileIsUnsupportedType("designFileName"), () -> asListHtml());
169             return null;
170         }
171 
172         final File parentFile = uploadFile.getParentFile();
173         if (!parentFile.exists() && !parentFile.mkdirs()) {
174             logger.warn("Could not create " + parentFile.getAbsolutePath());
175         }
176 
177         try {
178             write(uploadFile.getAbsolutePath(), form.designFile.getFileData());
179             final String currentFileName = fileName;
180             saveInfo(messages -> messages.addSuccessUploadDesignFile(GLOBAL, currentFileName));
181         } catch (final Exception e) {
182             logger.error("Failed to write an image file: {}", fileName, e);
183             throwValidationError(messages -> messages.addErrorsFailedToWriteDesignImageFile(GLOBAL), () -> asListHtml());
184         }
185         return redirect(getClass());
186     }
187 
188     private boolean checkFileType(final String fileName, final String[] exts) {
189         if (fileName == null) {
190             return false;
191         }
192         final String lFileName = fileName.toLowerCase(Locale.ENGLISH);
193         for (final String ext : exts) {
194             if (lFileName.endsWith("." + ext)) {
195                 return true;
196             }
197         }
198         return false;
199     }
200 
201     @Execute
202     public StreamResponse download(final FileAccessForm form) {
203         final File file = getTargetFile(form.fileName).get();
204         if (file == null) {
205             throwValidationError(messages -> messages.addErrorsTargetFileDoesNotExist(GLOBAL, form.fileName), () -> asListHtml());
206             return null;
207         }
208         validate(form, messages -> {}, () -> asListHtml());
209         verifyToken(() -> asListHtml());
210         return asStream(file.getName()).contentTypeOctetStream().stream(out -> {
211             try (FileInputStream fis = new FileInputStream(file)) {
212                 out.write(fis);
213             }
214         });
215     }
216 
217     @Execute
218     public HtmlResponse delete(final FileAccessForm form) {
219         getTargetFile(form.fileName).ifPresent(file -> {
220             if (!file.delete()) {
221                 logger.error("Failed to delete {}", file.getAbsolutePath());
222                 throwValidationError(messages -> messages.addErrorsFailedToDeleteFile(GLOBAL, form.fileName), () -> asListHtml());
223             }
224         }).orElse(() -> {
225             throwValidationError(messages -> messages.addErrorsTargetFileDoesNotExist(GLOBAL, form.fileName), () -> asListHtml());
226         });
227         saveInfo(messages -> messages.addSuccessDeleteFile(GLOBAL, form.fileName));
228         validate(form, messages -> {}, () -> asListHtml());
229         verifyToken(() -> asListHtml());
230         return redirect(getClass());
231     }
232 
233     // -----------------------------------------------------
234     //                                                 Edit
235     //                                                ------
236     @Execute
237     public HtmlResponse edit(final EditForm form) {
238         final String jspType = "view";
239         final File jspFile = getJspFile(form.fileName, jspType);
240         try {
241             form.content = new String(FileUtil.readBytes(jspFile), Constants.UTF_8);
242         } catch (final UnsupportedEncodingException e) {
243             throw new FessSystemException("Invalid encoding", e);
244         }
245         saveToken();
246         return asEditHtml(form);
247     }
248 
249     @Execute
250     public HtmlResponse editAsUseDefault(final EditForm form) {
251         final String jspType = "orig/view";
252         final File jspFile = getJspFile(form.fileName, jspType);
253         try {
254             form.content = new String(FileUtil.readBytes(jspFile), Constants.UTF_8);
255         } catch (final UnsupportedEncodingException e) {
256             throw new FessSystemException("Invalid encoding", e);
257         }
258         saveToken();
259         return asEditHtml(form);
260     }
261 
262     @Execute
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), () -> 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         fileList.addAll(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), () -> asListHtml());
311             }
312             final String jspFileName = systemHelper.getDesignJspFileName(values[1]);
313             if (jspFileName == null) {
314                 throwValidationError(messages -> messages.addErrorsInvalidDesignJspFileName(GLOBAL), () -> 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), () -> 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 }