/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.server.ngclient; import java.awt.Dimension; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.wicket.util.file.FileCleaner; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.upload.DiskFileItem; import org.apache.wicket.util.upload.DiskFileItemFactory; import org.apache.wicket.util.upload.FileItem; import org.apache.wicket.util.upload.FileUploadBase.FileSizeLimitExceededException; import org.apache.wicket.util.upload.FileUploadException; import org.apache.wicket.util.upload.ServletFileUpload; import org.sablo.websocket.CurrentWindow; import org.sablo.websocket.WebsocketSessionManager; import com.servoy.j2db.AbstractActiveSolutionHandler; import com.servoy.j2db.FlattenedSolution; import com.servoy.j2db.IApplication; import com.servoy.j2db.IDebugClientHandler; import com.servoy.j2db.MediaURLStreamHandler; import com.servoy.j2db.persistence.IRepository; import com.servoy.j2db.persistence.Media; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.SolutionMetaData; import com.servoy.j2db.plugins.IMediaUploadCallback; import com.servoy.j2db.plugins.IUploadData; import com.servoy.j2db.server.ngclient.eventthread.NGClientWebsocketSessionWindows; import com.servoy.j2db.server.shared.ApplicationServerRegistry; import com.servoy.j2db.server.shared.IApplicationServer; import com.servoy.j2db.ui.IMediaFieldConstants; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.HTTPUtils; import com.servoy.j2db.util.ImageLoader; import com.servoy.j2db.util.MimeTypes; import com.servoy.j2db.util.SecuritySupport; import com.servoy.j2db.util.Settings; import com.servoy.j2db.util.UUID; import com.servoy.j2db.util.Utils; /** * Supported resources URLs:<br><br> * * Get: * <ul> * <li>/resources/fs/[rootSolutionName]/[media.name.including.mediafolderpath] - for flattened solution access - useful when resources such as CSS link to each other relatively by name/path:</li> * <li>/resources/fs/[rootSolutionName]/[media.name.including.mediafolderpath]?uuid=... - for SolutionModel altered media access ((dynamic)) flattened solution of a specific client; not cached) * <li>/resources/dynamic/[dynamic_uuid] - for on-the-fly content (for example being served directly from the database); 'dynamic_uuid' is the one returned by MediaResourcesServlet.getMediaInfo(byte[]))</li> * </ul> * Post: * <ul> * <li>/resources/upload/[clientuuid]/[formName]/[elementName]/[propertyName] - for binary upload targeting an element property</li> * <li>/resources/upload/[clientuuid] - for binary upload of files selected with the built-in file selector</li> * </ul> * * @author jcompagner */ @SuppressWarnings("nls") @WebServlet("/resources/*") public class MediaResourcesServlet extends HttpServlet { public static final String FLATTENED_SOLUTION_ACCESS = "fs"; public static final String DYNAMIC_DATA_ACCESS = "dynamic"; private static File tempDir; private static final ConcurrentHashMap<String, MediaInfo> dynamicMediasMap = new ConcurrentHashMap<>(); public static MediaInfo createMediaInfo(byte[] mediaBytes, String fileName, String contentType, String contentDisposition) { MediaInfo mediaInfo = new MediaInfo(UUID.randomUUID().toString(), fileName, contentType == null ? MimeTypes.getContentType(mediaBytes, null) : contentType, contentDisposition, mediaBytes); dynamicMediasMap.put(mediaInfo.getName(), mediaInfo); return mediaInfo; } public static MediaInfo createMediaInfo(byte[] mediaBytes) { return createMediaInfo(mediaBytes, null, null, null); } private static void cleanupDynamicMediasMap(boolean forceAll) { long now = System.currentTimeMillis(); for (MediaInfo mediaInfo : dynamicMediasMap.values()) { if (forceAll || now - mediaInfo.getLastAccessedTimeStamp() > 3600000) { mediaInfo.destroy(); dynamicMediasMap.remove(mediaInfo.getName()); } } } @Override public void init(ServletConfig context) throws ServletException { super.init(context); try { tempDir = (File)context.getServletContext().getAttribute("javax.servlet.context.tempdir"); if (tempDir != null) { tempDir = new File(tempDir, DYNAMIC_DATA_ACCESS); deleteAll(tempDir); tempDir.mkdir(); } } catch (Exception ex) { Debug.error("Cannot create temp folder for dynamic resources", ex); tempDir = null; } } @Override public void destroy() { super.destroy(); cleanupDynamicMediasMap(true); if (tempDir != null) { deleteAll(tempDir); } FileCleaner.destroy(); } private void deleteAll(File f) { if (!f.exists()) return; if (f.isDirectory()) { for (File fl : f.listFiles()) deleteAll(fl); } f.delete(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { boolean found = false; String path = req.getPathInfo(); if (path.startsWith("/")) path = path.substring(1); String[] paths = path.split("/"); if (paths.length > 1) { String accessType = paths[0]; switch (accessType) { case FLATTENED_SOLUTION_ACCESS : if (paths.length >= 3) { String clientUUID = req.getParameter("uuid"); StringBuffer mediaName = new StringBuffer(); for (int i = 2; i < paths.length - 1; i++) mediaName.append(paths[i]).append('/'); mediaName.append(paths[paths.length - 1]); if (clientUUID == null) found = sendFlattenedSolutionBasedMedia(req, resp, paths[1], mediaName.toString()); else found = sendClientFlattenedSolutionBasedMedia(req, resp, clientUUID, mediaName.toString()); } break; case DYNAMIC_DATA_ACCESS : if (paths.length == 2) found = sendDynamicData(req, resp, paths[1]); break; default : break; } } else if ("servoy_blobloader".equals(path)) { String encrypted = req.getParameter("blob"); try { String decrypt = SecuritySupport.decrypt(Settings.getInstance(), encrypted); String clientUUID = req.getParameter("uuid"); found = sendData(resp, MediaURLStreamHandler.getBlobLoaderMedia(getClient(clientUUID), decrypt), MediaURLStreamHandler.getBlobLoaderMimeType(decrypt), MediaURLStreamHandler.getBlobLoaderFileName(decrypt), null); } catch (Exception e) { Debug.error("could not decrypt blobloader: " + encrypted); } } if (!found) resp.sendError(HttpServletResponse.SC_NOT_FOUND); } private boolean sendDynamicData(HttpServletRequest request, HttpServletResponse response, String dynamicID) throws IOException { if (dynamicMediasMap.containsKey(dynamicID)) { MediaInfo mediaInfo = dynamicMediasMap.get(dynamicID); mediaInfo.touch(); cleanupDynamicMediasMap(false); if (HTTPUtils.checkAndSetUnmodified(request, response, mediaInfo.getLastModifiedTimeStamp())) return true; return sendData(response, mediaInfo.getData(), mediaInfo.getContentType(), mediaInfo.getFileName(), mediaInfo.getContentDisposition()); } return false; } private boolean sendFlattenedSolutionBasedMedia(HttpServletRequest request, HttpServletResponse response, String rootSolutionName, String mediaName) throws IOException { FlattenedSolution fs = null; try { IApplicationServer as = ApplicationServerRegistry.getService(IApplicationServer.class); SolutionMetaData solutionMetaData = (SolutionMetaData)ApplicationServerRegistry.get().getLocalRepository().getRootObjectMetaData(rootSolutionName, IRepository.SOLUTIONS); if (solutionMetaData == null) { Debug.error("Solution '" + rootSolutionName + "' was not found when sending media data for '" + mediaName + "'."); return false; } fs = new FlattenedSolution(solutionMetaData, new AbstractActiveSolutionHandler(as) { @Override public IRepository getRepository() { return ApplicationServerRegistry.get().getLocalRepository(); } }); } catch (RepositoryException e) { Debug.error(e); } try { Media media = fs.getMedia(mediaName); if (media != null) { return sendData(request, response, fs, media); } } finally { fs.close(null); } return false; } private boolean sendData(HttpServletRequest request, HttpServletResponse response, FlattenedSolution fs, Media media) throws IOException { if (ApplicationServerRegistry.get().isDeveloperStartup()) { response.setHeader("Cache-Control", "max-age=0, must-revalidate, proxy-revalidate"); } // cache resources on client until changed if (HTTPUtils.checkAndSetUnmodified(request, response, media.getLastModifiedTime() != -1 ? media.getLastModifiedTime() : fs.getLastModifiedTime())) return true; return sendData(response, media.getMediaData(), media.getMimeType(), media.getName(), null); } private boolean sendClientFlattenedSolutionBasedMedia(HttpServletRequest request, HttpServletResponse response, String clientUUID, String mediaName) throws IOException { IApplication client = getClient(clientUUID); if (client != null) { FlattenedSolution fs = client.getFlattenedSolution(); if (fs != null) { Media media = fs.getMedia(mediaName); if (media != null) { return sendData(request, response, fs, media); } } } return false; } /** * @param clientUUID * @return */ private IApplication getClient(String clientUUID) { // try to look it up as clientId. (solution model) INGClientWebsocketSession wsSession = (INGClientWebsocketSession)WebsocketSessionManager.getSession(WebsocketSessionFactory.CLIENT_ENDPOINT, clientUUID); IApplication client = null; if (wsSession == null) { IDebugClientHandler debugClientHandler = ApplicationServerRegistry.get().getDebugClientHandler(); if (debugClientHandler != null) { client = debugClientHandler.getDebugNGClient(); } } else { client = wsSession.getClient(); } return client; } private boolean sendData(HttpServletResponse resp, byte[] mediaData, String contentType, String fileName, String contentDisposition) throws IOException { boolean dataWasSent = false; if (mediaData != null && mediaData.length > 0) { String ct = contentType; if (ct == null) { ct = MimeTypes.getContentType(mediaData, fileName); } if (ct != null) resp.setContentType(ct); resp.setContentLength(mediaData.length); if (fileName != null) { resp.setHeader("Content-disposition", (contentDisposition == null ? "attachment" : contentDisposition) + "; filename=\"" + fileName + "\""); } ServletOutputStream outputStream = resp.getOutputStream(); outputStream.write(mediaData); outputStream.flush(); dataWasSent = true; } return dataWasSent; } @Override protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String path = req.getPathInfo(); if (path.startsWith("/")) path = path.substring(1); String[] paths = path.split("/"); if ((paths.length == 2 || paths.length == 5) && paths[0].equals("upload")) { if (req.getHeader("Content-Type") != null && req.getHeader("Content-Type").startsWith("multipart/form-data")) { String clientID = paths[1]; final INGClientWebsocketSession wsSession = (INGClientWebsocketSession)WebsocketSessionManager.getSession( WebsocketSessionFactory.CLIENT_ENDPOINT, clientID); try { String formName = paths.length == 5 ? paths[2] : null; String elementName = paths.length == 5 ? paths[3] : null; final String propertyName = paths.length == 5 ? paths[4] : null; if (wsSession != null) { Settings settings = Settings.getInstance(); File fileUploadDir = null; String uploadDir = settings.getProperty("servoy.ng_web_client.temp.uploadir"); if (uploadDir != null) { fileUploadDir = new File(uploadDir); if (!fileUploadDir.exists() && !fileUploadDir.mkdirs()) { fileUploadDir = null; Debug.error("Couldn't use the property 'servoy.ng_web_client.temp.uploadir' value: '" + uploadDir + "', directory could not be created or doesn't exists"); } } int tempFileThreshold = Utils.getAsInteger(settings.getProperty("servoy.ng_web_client.tempfile.threshold", "50"), false) * 1000; ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(tempFileThreshold, fileUploadDir)); long maxUpload = Utils.getAsLong(settings.getProperty("servoy.webclient.maxuploadsize", "0"), false); if (maxUpload > 0) upload.setFileSizeMax(maxUpload * 1000); Iterator<FileItem> iterator = upload.parseRequest(req).iterator(); final ArrayList<FileUploadData> aFileUploadData = new ArrayList<FileUploadData>(); while (iterator.hasNext()) { FileItem item = iterator.next(); if (formName != null && elementName != null && propertyName != null) { final Map<String, Object> fileData = new HashMap<String, Object>(); fileData.put("", item.get()); fileData.put(IMediaFieldConstants.FILENAME, item.getName()); fileData.put(IMediaFieldConstants.MIMETYPE, item.getContentType()); final IWebFormUI form = wsSession.getClient().getFormManager().getForm(formName).getFormUI(); final WebFormComponent webComponent = form.getWebComponent(elementName); CurrentWindow.runForWindow(new NGClientWebsocketSessionWindows(wsSession), new Runnable() { @Override public void run() { form.getDataAdapterList().pushChanges(webComponent, propertyName, fileData); wsSession.valueChanged(); } }); } else { // it is a file from the built-in file selector aFileUploadData.add(new FileUploadData(item)); } } if (aFileUploadData.size() > 0) { final IMediaUploadCallback mediaUploadCallback = ((NGClient)wsSession.getClient()).getMediaUploadCallback(); if (mediaUploadCallback != null) { // leave time for this request to finish, before executing the callback, so the file // dialog can do its close ((NGClient)wsSession.getClient()).invokeLater(new Runnable() { @Override public void run() { mediaUploadCallback.uploadComplete(aFileUploadData.toArray(new FileUploadData[aFileUploadData.size()])); mediaUploadCallback.onSubmit(); } }); } } } } catch (FileSizeLimitExceededException ex) { res.setStatus(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); res.getWriter().print( wsSession.getClient().getI18NMessage("servoy.filechooser.sizeExceeded", new Object[] { ex.getPermittedSize() / 1000 + "KB" })); } catch (FileUploadException ex) { throw new ServletException(ex.toString()); } } } } public static final class MediaInfo { private static final long MAX_DATA_SIZE_FOR_IN_MEMORY = 5242880; // 5MB private final String name; private final String fileName; private final String contentType; private final String contentDisposition; private final long modifiedTimeStamp; private long accessedTimeStamp; private final Dimension mediaSize; private byte[] data; MediaInfo(String name, String fileName, String contentType, String contentDisposition, byte[] data) { this.name = name; this.fileName = fileName; this.contentType = contentType; this.contentDisposition = contentDisposition; modifiedTimeStamp = accessedTimeStamp = System.currentTimeMillis(); this.mediaSize = ImageLoader.getSize(data); if (data.length < MAX_DATA_SIZE_FOR_IN_MEMORY) { this.data = data; } else { this.data = null; if (MediaResourcesServlet.tempDir != null) { Utils.writeFile(new File(MediaResourcesServlet.tempDir, name), data); } else { Debug.error("Cannot save dynamic data to servlet temp dir!"); } } } public String getName() { return name; } public String getFileName() { return fileName; } public String getContentDisposition() { return contentDisposition; } public String getContentType() { return contentType; } public long getLastModifiedTimeStamp() { return modifiedTimeStamp; } public Dimension getMediaSize() { return mediaSize; } public byte[] getData() { if (data == null) { return Utils.readFile(new File(MediaResourcesServlet.tempDir, name), -1); } return data; } public void touch() { accessedTimeStamp = System.currentTimeMillis(); } public long getLastAccessedTimeStamp() { return accessedTimeStamp; } public void destroy() { if (data == null) { try { new File(MediaResourcesServlet.tempDir, name).delete(); } catch (Exception ex) { Debug.error(ex); } } else { data = null; } } } private static final class FileUploadData implements IUploadData { private final FileItem item; private FileUploadData(FileItem item) { this.item = item; } public String getName() { String name = item.getName(); // when uploading from localhost some browsers will specify the entire path, we strip it // down to just the file name name = Strings.lastPathComponent(name, '/'); name = Strings.lastPathComponent(name, '\\'); name = name.replace('\\', '/'); String[] tokenized = name.split("/"); //$NON-NLS-1$ return tokenized[tokenized.length - 1]; } public String getContentType() { return item.getContentType(); } public byte[] getBytes() { return item.get(); } /** * @see com.servoy.j2db.plugins.IUploadData#getFile() */ public File getFile() { if (item instanceof DiskFileItem) { return ((DiskFileItem)item).getStoreLocation(); } return null; } /* * @see com.servoy.j2db.plugins.IUploadData#getInputStream() */ public InputStream getInputStream() throws IOException { return item.getInputStream(); } @Override public long lastModified() { return System.currentTimeMillis(); } } }