001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.servlet;
016    
017    import com.liferay.portal.kernel.cache.PortalCache;
018    import com.liferay.portal.kernel.cache.SingleVMPoolUtil;
019    import com.liferay.portal.kernel.log.Log;
020    import com.liferay.portal.kernel.log.LogFactoryUtil;
021    import com.liferay.portal.kernel.servlet.HttpHeaders;
022    import com.liferay.portal.kernel.servlet.ServletContextUtil;
023    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
024    import com.liferay.portal.kernel.util.CharPool;
025    import com.liferay.portal.kernel.util.ContentTypes;
026    import com.liferay.portal.kernel.util.FileUtil;
027    import com.liferay.portal.kernel.util.ParamUtil;
028    import com.liferay.portal.kernel.util.PropsKeys;
029    import com.liferay.portal.kernel.util.StringPool;
030    import com.liferay.portal.kernel.util.StringUtil;
031    import com.liferay.portal.kernel.util.Time;
032    import com.liferay.portal.kernel.util.Validator;
033    import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
034    import com.liferay.portal.util.MinifierUtil;
035    import com.liferay.portal.util.PortalUtil;
036    import com.liferay.portal.util.PrefsPropsUtil;
037    import com.liferay.portal.util.PropsValues;
038    
039    import java.io.File;
040    import java.io.IOException;
041    import java.io.Serializable;
042    
043    import java.util.Arrays;
044    import java.util.LinkedHashSet;
045    import java.util.Set;
046    
047    import javax.servlet.ServletContext;
048    import javax.servlet.ServletException;
049    import javax.servlet.http.HttpServlet;
050    import javax.servlet.http.HttpServletRequest;
051    import javax.servlet.http.HttpServletResponse;
052    
053    /**
054     * @author Eduardo Lundgren
055     * @author Edward Han
056     * @author Zsigmond Rab
057     */
058    public class ComboServlet extends HttpServlet {
059    
060            @Override
061            public void service(
062                            HttpServletRequest request, HttpServletResponse response)
063                    throws IOException, ServletException {
064    
065                    try {
066                            doService(request, response);
067                    }
068                    catch (Exception e) {
069                            _log.error(e, e);
070    
071                            PortalUtil.sendError(
072                                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e, request,
073                                    response);
074                    }
075            }
076    
077            protected void doService(
078                            HttpServletRequest request, HttpServletResponse response)
079                    throws Exception {
080    
081                    String contextPath = PortalUtil.getPathContext();
082    
083                    String[] modulePaths = request.getParameterValues("m");
084    
085                    if ((modulePaths == null) || (modulePaths.length == 0)) {
086                            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
087    
088                            return;
089                    }
090    
091                    Set<String> modulePathsSet = new LinkedHashSet<String>(
092                            modulePaths.length);
093    
094                    for (String path : modulePaths) {
095                            modulePathsSet.add(path);
096                    }
097    
098                    modulePaths = modulePathsSet.toArray(new String[modulePathsSet.size()]);
099    
100                    String modulePathsString = null;
101    
102                    byte[][] bytesArray = null;
103    
104                    if (!PropsValues.COMBO_CHECK_TIMESTAMP) {
105                            modulePathsString = Arrays.toString(modulePaths);
106    
107                            bytesArray = (byte[][])_bytesArrayPortalCache.get(
108                                    modulePathsString);
109                    }
110    
111                    String firstModulePath = modulePaths[0];
112    
113                    String extension = FileUtil.getExtension(firstModulePath);
114    
115                    if (bytesArray == null) {
116                            String p = ParamUtil.getString(request, "p");
117    
118                            String minifierType = ParamUtil.getString(request, "minifierType");
119    
120                            if (Validator.isNull(minifierType)) {
121                                    minifierType = "js";
122    
123                                    if (extension.equalsIgnoreCase(_CSS_EXTENSION)) {
124                                            minifierType = "css";
125                                    }
126                            }
127    
128                            if (!minifierType.equals("css") && !minifierType.equals("js")) {
129                                    minifierType = "js";
130                            }
131    
132                            bytesArray = new byte[modulePaths.length][];
133    
134                            for (int i = 0; i < modulePaths.length; i++) {
135                                    String modulePath = modulePaths[i];
136    
137                                    if (!validateModuleExtension(modulePath)) {
138                                            response.setHeader(
139                                                    HttpHeaders.CACHE_CONTROL,
140                                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
141                                            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
142    
143                                            return;
144                                    }
145    
146                                    byte[] bytes = new byte[0];
147    
148                                    if (Validator.isNotNull(modulePath)) {
149                                            modulePath = StringUtil.replaceFirst(
150                                                    p.concat(modulePath), contextPath, StringPool.BLANK);
151    
152                                            if (getFile(modulePath) == null) {
153                                                    response.setHeader(
154                                                            HttpHeaders.CACHE_CONTROL,
155                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
156                                                    response.setStatus(HttpServletResponse.SC_NOT_FOUND);
157    
158                                                    return;
159                                            }
160    
161                                            bytes = getFileContent(
162                                                    request, response, modulePath, minifierType);
163                                    }
164    
165                                    bytesArray[i] = bytes;
166                            }
167    
168                            if ((modulePathsString != null) &&
169                                    !PropsValues.COMBO_CHECK_TIMESTAMP) {
170    
171                                    _bytesArrayPortalCache.put(modulePathsString, bytesArray);
172                            }
173                    }
174    
175                    String contentType = ContentTypes.TEXT_JAVASCRIPT;
176    
177                    if (extension.equalsIgnoreCase(_CSS_EXTENSION)) {
178                            contentType = ContentTypes.TEXT_CSS;
179                    }
180    
181                    response.setContentType(contentType);
182    
183                    ServletResponseUtil.write(response, bytesArray);
184            }
185    
186            protected File getFile(String path) throws IOException {
187                    ServletContext servletContext = getServletContext();
188    
189                    String basePath = ServletContextUtil.getRealPath(
190                            servletContext, _JAVASCRIPT_DIR);
191    
192                    if (basePath == null) {
193                            return null;
194                    }
195    
196                    basePath = StringUtil.replace(
197                            basePath, CharPool.BACK_SLASH, CharPool.SLASH);
198    
199                    File baseDir = new File(basePath);
200    
201                    if (!baseDir.exists()) {
202                            return null;
203                    }
204    
205                    String filePath = ServletContextUtil.getRealPath(servletContext, path);
206    
207                    if (filePath == null) {
208                            return null;
209                    }
210    
211                    filePath = StringUtil.replace(
212                            filePath, CharPool.BACK_SLASH, CharPool.SLASH);
213    
214                    File file = new File(filePath);
215    
216                    if (!file.exists()) {
217                            return null;
218                    }
219    
220                    String baseCanonicalPath = baseDir.getCanonicalPath();
221                    String fileCanonicalPath = file.getCanonicalPath();
222    
223                    if (fileCanonicalPath.indexOf(baseCanonicalPath) == 0) {
224                            return file;
225                    }
226    
227                    return null;
228            }
229    
230            protected byte[] getFileContent(
231                            HttpServletRequest request, HttpServletResponse response,
232                            String path, String minifierType)
233                    throws IOException {
234    
235                    String fileContentKey = path.concat(StringPool.QUESTION).concat(
236                            minifierType);
237    
238                    FileContentBag fileContentBag =
239                            (FileContentBag)_fileContentBagPortalCache.get(fileContentKey);
240    
241                    if ((fileContentBag != null) && !PropsValues.COMBO_CHECK_TIMESTAMP) {
242                            return fileContentBag._fileContent;
243                    }
244    
245                    File file = getFile(path);
246    
247                    if ((fileContentBag != null) && PropsValues.COMBO_CHECK_TIMESTAMP) {
248                            long elapsedTime =
249                                    System.currentTimeMillis() - fileContentBag._lastModified;
250    
251                            if ((file != null) &&
252                                    (elapsedTime <= PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL) &&
253                                    (file.lastModified() == fileContentBag._lastModified)) {
254    
255                                    return fileContentBag._fileContent;
256                            }
257                            else {
258                                    _fileContentBagPortalCache.remove(fileContentKey);
259                            }
260                    }
261    
262                    if (file == null) {
263                            fileContentBag = _EMPTY_FILE_CONTENT_BAG;
264                    }
265                    else {
266                            String stringFileContent = FileUtil.read(file);
267    
268                            if (!StringUtil.endsWith(path, _CSS_MINIFIED_SUFFIX) &&
269                                    !StringUtil.endsWith(path, _JAVASCRIPT_MINIFIED_SUFFIX)) {
270    
271                                    if (minifierType.equals("css")) {
272                                            String cssRealPath = file.getAbsolutePath();
273    
274                                            try {
275                                                    stringFileContent = DynamicCSSUtil.parseSass(
276                                                            request, cssRealPath, stringFileContent);
277                                            }
278                                            catch (Exception e) {
279                                                    _log.error(
280                                                            "Unable to parse SASS on CSS " + cssRealPath, e);
281    
282                                                    if (_log.isDebugEnabled()) {
283                                                            _log.debug(stringFileContent);
284                                                    }
285    
286                                                    response.setHeader(
287                                                            HttpHeaders.CACHE_CONTROL,
288                                                            HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
289                                            }
290    
291                                            stringFileContent = MinifierUtil.minifyCss(
292                                                    stringFileContent);
293                                    }
294                                    else if (minifierType.equals("js")) {
295                                            stringFileContent = MinifierUtil.minifyJavaScript(
296                                                    stringFileContent);
297                                    }
298                            }
299    
300                            fileContentBag = new FileContentBag(
301                                    stringFileContent.getBytes(StringPool.UTF8),
302                                    file.lastModified());
303                    }
304    
305                    if (PropsValues.COMBO_CHECK_TIMESTAMP) {
306                            int timeToLive =
307                                    (int)(PropsValues.COMBO_CHECK_TIMESTAMP_INTERVAL / Time.SECOND);
308    
309                            _fileContentBagPortalCache.put(
310                                    fileContentKey, fileContentBag, timeToLive);
311                    }
312    
313                    return fileContentBag._fileContent;
314            }
315    
316            protected boolean validateModuleExtension(String moduleName)
317                    throws Exception {
318    
319                    boolean validModuleExtension = false;
320    
321                    String[] fileExtensions = PrefsPropsUtil.getStringArray(
322                            PropsKeys.COMBO_ALLOWED_FILE_EXTENSIONS, StringPool.COMMA);
323    
324                    for (int i = 0; i < fileExtensions.length; i++) {
325                            if (StringPool.STAR.equals(fileExtensions[i]) ||
326                                    StringUtil.endsWith(moduleName, fileExtensions[i])) {
327    
328                                    validModuleExtension = true;
329    
330                                    break;
331                            }
332                    }
333    
334                    return validModuleExtension;
335            }
336    
337            private static final String _CSS_EXTENSION = "css";
338    
339            private static final String _CSS_MINIFIED_SUFFIX = "-min.css";
340    
341            private static final FileContentBag _EMPTY_FILE_CONTENT_BAG =
342                    new FileContentBag(new byte[0], 0);
343    
344            private static final String _JAVASCRIPT_DIR = "html/js";
345    
346            private static final String _JAVASCRIPT_MINIFIED_SUFFIX = "-min.js";
347    
348            private static Log _log = LogFactoryUtil.getLog(ComboServlet.class);
349    
350            private PortalCache _bytesArrayPortalCache = SingleVMPoolUtil.getCache(
351                    ComboServlet.class.getName());
352            private PortalCache _fileContentBagPortalCache = SingleVMPoolUtil.getCache(
353                    FileContentBag.class.getName());
354    
355            private static class FileContentBag implements Serializable {
356    
357                    public FileContentBag(byte[] fileContent, long lastModifiedTime) {
358                            _fileContent = fileContent;
359                            _lastModified = lastModifiedTime;
360                    }
361    
362                    private byte[] _fileContent;
363                    private long _lastModified;
364    
365            }
366    
367    }