package com.dotmarketing.servlets;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.dotcms.repackage.org.apache.commons.collections.LRUMap;
import com.dotmarketing.beans.Identifier;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.DotStateException;
import com.dotmarketing.business.PermissionAPI;
import com.dotmarketing.business.UserAPI;
import com.dotmarketing.business.web.UserWebAPI;
import com.dotmarketing.business.web.WebAPILocator;
import com.dotmarketing.cache.LiveCache;
import com.dotmarketing.cache.WorkingCache;
import com.dotmarketing.cms.factories.PublicEncryptionFactory;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotHibernateException;
import com.dotmarketing.exception.DotRuntimeException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.portlets.contentlet.business.BinaryContentExporter;
import com.dotmarketing.portlets.contentlet.business.BinaryContentExporterException;
import com.dotmarketing.portlets.contentlet.business.ContentletAPI;
import com.dotmarketing.portlets.contentlet.business.DotContentletStateException;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
import com.dotmarketing.portlets.fileassets.business.IFileAsset;
import com.dotmarketing.portlets.files.business.FileAPI;
import com.dotmarketing.portlets.structure.model.Field;
import com.dotmarketing.util.Config;
import com.dotmarketing.util.Constants;
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import com.dotmarketing.util.WebKeys;
import com.liferay.portal.PortalException;
import com.liferay.portal.SystemException;
import com.liferay.portal.model.User;
import com.liferay.util.FileUtil;
/**
*
* This servlet allows you invoke content exporters over binary fields.
* With the following URL syntax you are able to invoke an specific content exporter on a piece of content.
*
* /contentAsset/{exporter path}/{content identifier}/{binary field - optional}?{byInode=true&}{exporter specific parameters}
*
* {exporter path} is the exporter specific path set by the exporter class. I.E. every exporter must implement an interface method call getPathMapping that
* defines the path of what the exporter is going to be bound. E.G. The com.dotmarketing.portlets.contentlet.business.exporter.ImageResizeFieldExporter binds
* to the resize-image path so it can be invoked as /contentAsset/resize-image/...
*
* {content identifier} is the identifier of the piece of content that wants to be retrieved. Special case occurs when the url parameter "byInode=true" is set
* then the content specific inode must be passed here.
*
* {binary field - optional} is the binary field velocity name (refer to the structure manager to fidn out which is your field velocity name). This url part could be
* obeyed for certain exporters that operate over the entire content instead of an specific field like with an XML content exporter for example.
*
* {exporter specific parameters} is for exporter specific parameters, refer to the exporter documentation. Exporters like the thubmnail generator takes parameters
* like the width or height of the thumbnail to be generated.
*
* @author David Torres 2010
*
*/
public class BinaryExporterServlet extends HttpServlet {
private static final FileAPI fileAPI = APILocator.getFileAPI();
private static final UserAPI userAPI = APILocator.getUserAPI();
Map<String, BinaryContentExporter> exportersByPathMapping;
private static String assetPath = "/assets";
private static String realPath = null;
@SuppressWarnings("unchecked")
@Override
public void init() throws ServletException {
super.init();
if(UtilMethods.isSet(Config.getStringProperty("ASSET_REAL_PATH"))){
realPath = Config.getStringProperty("ASSET_REAL_PATH");
}
if(UtilMethods.isSet(Config.getStringProperty("ASSET_PATH"))){
assetPath = Config.getStringProperty("ASSET_PATH");
}
exportersByPathMapping = new HashMap<String, BinaryContentExporter>();
Iterator<String> keys = Config.getKeys();
while(keys.hasNext()) {
String key = keys.next();
if(key.startsWith("CONTENT_EXPORTERS")) {
String[] exporterClasses = Config.getStringArrayProperty(key);
for(String exporterClassName : exporterClasses) {
try {
Class<BinaryContentExporter> exporterClass = (Class<BinaryContentExporter>) Class.forName(exporterClassName);
BinaryContentExporter exporter = exporterClass.newInstance();
if(exportersByPathMapping.containsKey(exporter.getPathMapping()))
Logger.warn(BinaryExporterServlet.class, "There is already an exporter registered to path " + exporter.getPathMapping() +
" this new exporter: " + exporter.getName() + " will replace the previously registered: " +
exportersByPathMapping.get(exporter.getPathMapping()).getName());
Logger.info(this, "Exporter \"" + exporter.getName() + "\" registered for path /" + exporter.getPathMapping());
exportersByPathMapping.put(exporter.getPathMapping(), exporter);
} catch (ClassNotFoundException e) {
Logger.warn(BinaryExporterServlet.class, e.getMessage(), e);
} catch (InstantiationException e) {
Logger.warn(BinaryExporterServlet.class, e.getMessage(), e);
} catch (IllegalAccessException e) {
Logger.warn(BinaryExporterServlet.class, e.getMessage(), e);
}
}
}
}
}
private static final long serialVersionUID = 1L;
@SuppressWarnings("unchecked")
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String servletPath = req.getServletPath();
String uri = req.getRequestURI().substring(servletPath.length());
String[] uriPieces = uri.split("/");
String exporterPath = uriPieces[1];
String uuid = uriPieces[2];
Map<String, String[]> params = new HashMap<String, String[]>();
params.putAll(req.getParameterMap());
// only set uri params if they are not set in the query string - meaning
// the query string will override the uri params.
Map<String, String[]> uriParams = getURIParams(req);
for(String x: uriParams.keySet()){
if(!params.containsKey(x)){
params.put(x, uriParams.get(x));
}
}
params = sortByKey(params);
String assetInode = null;
String assetIdentifier = null;
boolean byInode = params.containsKey("byInode") ;
if (byInode){
assetInode = uuid;
}
else{
assetIdentifier = uuid;
}
String fieldVarName = uriPieces.length > 3?uriPieces[3]:null;
BinaryContentExporter exporter = exportersByPathMapping.get(exporterPath);
if(exporter == null) {
Logger.warn(this, "No exporter for path " + exporterPath + " is registered. Requested url = " + uri);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
UserWebAPI userWebAPI = WebAPILocator.getUserWebAPI();
ContentletAPI contentAPI = APILocator.getContentletAPI();
BinaryContentExporter.BinaryContentExporterData data = null;
File inputFile = null;
HttpSession session = req.getSession(false);
List<String> tempBinaryImageInodes = null;
if ( session != null && session.getAttribute( Contentlet.TEMP_BINARY_IMAGE_INODES_LIST ) != null ) {
tempBinaryImageInodes = (List<String>) session.getAttribute( Contentlet.TEMP_BINARY_IMAGE_INODES_LIST );
} else {
tempBinaryImageInodes = new ArrayList<String>();
}
boolean isTempBinaryImage = tempBinaryImageInodes.contains(assetInode);
try {
User user = userWebAPI.getLoggedInUser(req);
boolean respectFrontendRoles = !userWebAPI.isLoggedToBackend(req);
String downloadName = "file_asset";
long lang = APILocator.getLanguageAPI().getDefaultLanguage().getId();
try {
String x = null;
if (session != null) {
x = (String) session.getAttribute(WebKeys.HTMLPAGE_LANGUAGE);
} else {
x = (String) req.getAttribute(WebKeys.HTMLPAGE_LANGUAGE);
}
lang = Long.parseLong(x);
} catch(Exception e){
// Number parsing exception
}
boolean isContent = false;
try {
isContent = isContent(uuid, byInode, lang, respectFrontendRoles);
}catch (DotStateException e) {
resp.sendError(404);
return;
}
if (isContent){
Contentlet content = null;
if(byInode) {
if(isTempBinaryImage)
content = contentAPI.find(assetInode, APILocator.getUserAPI().getSystemUser(), respectFrontendRoles);
else
content = contentAPI.find(assetInode, user, respectFrontendRoles);
assetIdentifier = content.getIdentifier();
} else {
boolean live=userWebAPI.isLoggedToFrontend(req);
if (req.getSession(false) != null && req.getSession().getAttribute("tm_date")!=null) {
live=true;
Identifier ident=APILocator.getIdentifierAPI().find(assetIdentifier);
if(UtilMethods.isSet(ident.getSysPublishDate()) || UtilMethods.isSet(ident.getSysExpireDate())) {
Date fdate=new Date(Long.parseLong((String)req.getSession().getAttribute("tm_date")));
if(UtilMethods.isSet(ident.getSysPublishDate()) && ident.getSysPublishDate().before(fdate))
live=false;
if(UtilMethods.isSet(ident.getSysExpireDate()) && ident.getSysExpireDate().before(fdate))
return; // expired!
}
}
content = contentAPI.findContentletByIdentifier(assetIdentifier, live, lang, user, respectFrontendRoles);
assetInode = content.getInode();
}
Field field = content.getStructure().getFieldVar(fieldVarName);
if(field == null){
Logger.debug(this,"Field " + fieldVarName + " does not exists within structure " + content.getStructure().getVelocityVarName());
resp.sendError(404);
return;
}
if(isTempBinaryImage)
inputFile = contentAPI.getBinaryFile(content.getInode(), field.getVelocityVarName(), APILocator.getUserAPI().getSystemUser());
else
inputFile = contentAPI.getBinaryFile(content.getInode(), field.getVelocityVarName(), user);
if(inputFile == null){
Logger.debug(this,"binary file '" + fieldVarName + "' does not exist for inode " + content.getInode());
resp.sendError(404);
return;
}
downloadName = inputFile.getName();
}
else{
// if we are using this as a "Save as" from the image too
fieldVarName = WebKeys.EDITED_IMAGE_FILE_ASSET;
com.dotmarketing.portlets.files.model.File dotFile = null;
// get the identifier from cache
if(byInode) {
dotFile = fileAPI.find(assetInode,user,false);
downloadName = dotFile.getFileName();
//com.dotmarketing.portlets.files.model.File dotFile = APILocator.getFileAPI().get(assetIdentifier, user, respectFrontendRoles);
assetIdentifier = dotFile.getIdentifier();
}
Identifier id = APILocator.getIdentifierAPI().find(assetIdentifier);
// no identifier, no soup!
if(id == null || ! UtilMethods.isSet(id.getInode())){
Logger.debug(this,"Identifier: " + assetIdentifier +"not found");
resp.sendError(404);
return;
}
boolean hasLive = (LiveCache.getPathFromCache(id.getURI(), id.getHostId()) != null);
// no live version and front end, no soup
if(respectFrontendRoles && ! hasLive){
Logger.debug(this,"File :" + id.getInode() +"is not live");
resp.sendError(404);
return;
}
// no permissions, no soup!
if(!APILocator.getPermissionAPI().doesUserHavePermission(id, PermissionAPI.PERMISSION_READ, user)){
Logger.debug(this,"user: " + user + " does not have read on File :" + id.getInode());
if(WebAPILocator.getUserWebAPI().isLoggedToFrontend(req)){
resp.sendError(403);
}else{
resp.sendError(401);
}
return;
}
if(assetInode != null){
inputFile = new File(fileAPI.getRealAssetPath(assetInode, UtilMethods.getFileExtension(dotFile.getFileName())));
}
else if(respectFrontendRoles){
if(realPath != null){
inputFile = new File(realPath + LiveCache.getPathFromCache(id.getURI(), id.getHostId()));
}else{
inputFile = new File(FileUtil.getRealPath(assetPath + LiveCache.getPathFromCache(id.getURI(), id.getHostId())));
}
}else{
if(realPath != null){
inputFile = new File(realPath + WorkingCache.getPathFromCache(id.getURI(), id.getHostId()));
}else{
inputFile = new File(FileUtil.getRealPath(assetPath + WorkingCache.getPathFromCache(id.getURI(), id.getHostId())));
}
}
}
//DOTCMS-5674
if(UtilMethods.isSet(fieldVarName)){
params.put("fieldVarName", new String[]{fieldVarName});
params.put("assetInodeOrIdentifier", new String[]{uuid});
}
data = exporter.exportContent(inputFile, params);
// THIS IS WHERE THE MAGIC HAPPENS
// save to session if user looking to edit a file
if (req.getParameter(WebKeys.IMAGE_TOOL_SAVE_FILES) != null) {
Map<String, String> files;
if ( session != null && session.getAttribute( WebKeys.IMAGE_TOOL_SAVE_FILES ) != null ) {
files = (Map<String, String>) session.getAttribute( WebKeys.IMAGE_TOOL_SAVE_FILES );
} else {
files = new HashMap<>();
}
String ext = UtilMethods.getFileExtension(data.getDataFile().getName());
File tmp = File.createTempFile("binaryexporter", "." +ext);
FileUtil.copyFile(data.getDataFile(), tmp);
tmp.deleteOnExit();
if (req.getParameter("binaryFieldId") != null) {
files.put(req.getParameter("binaryFieldId"), tmp.getCanonicalPath());
} else {
files.put(fieldVarName, tmp.getCanonicalPath());
}
resp.getWriter().println(UtilMethods.encodeURIComponent(PublicEncryptionFactory.encryptString(tmp.getAbsolutePath())));
resp.getWriter().close();
resp.flushBuffer();
return;
}
/*******************************
*
* Start serving the data
*
*******************************/
String mimeType = fileAPI.getMimeType(data.getDataFile().getName());
if (mimeType == null)
mimeType = "application/octet-stream";
resp.setContentType(mimeType);
if (req.getParameter("force_download") != null) {
// if we are downloading a jpeg version of a png or gif
String x = UtilMethods.getFileExtension(downloadName);
String y = UtilMethods.getFileExtension(data.getDataFile().getName());
if(!x.equals(y)){
downloadName = downloadName.replaceAll("\\." + x, "\\." + y);
}
resp.setHeader("Content-Disposition", "attachment; filename=" + downloadName);
resp.setHeader("Content-Type", "application/force-download");
}
int _daysCache = 365;
GregorianCalendar expiration = new GregorianCalendar();
expiration.add(java.util.Calendar.DAY_OF_MONTH, _daysCache);
int seconds = (_daysCache * 24 * 60 * 60);
long _lastModified = data.getDataFile().lastModified();
if(_lastModified < 0) {
_lastModified = 0;
}
// we need to round the _lastmodified to get rid of the milliseconds.
_lastModified = _lastModified / 1000;
_lastModified = _lastModified * 1000;
Date _lastModifiedDate = new java.util.Date(_lastModified);
long _fileLength = data.getDataFile().length();
String _eTag = "dot:" + assetInode + ":" + _lastModified + ":" + _fileLength;
SimpleDateFormat httpDate = new SimpleDateFormat(Constants.RFC2822_FORMAT);
httpDate.setTimeZone(TimeZone.getTimeZone("GMT"));
/* Setting cache friendly headers */
resp.setHeader("Expires", httpDate.format(expiration.getTime()));
resp.setHeader("Cache-Control", "public, max-age="+seconds);
String ifModifiedSince = req.getHeader("If-Modified-Since");
String ifNoneMatch = req.getHeader("If-None-Match");
/*
* If the etag matches then the file is the same
*
*/
if(ifNoneMatch != null){
if(_eTag.equals(ifNoneMatch) || ifNoneMatch.equals("*")){
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED );
return;
}
}
/* Using the If-Modified-Since Header */
if(ifModifiedSince != null){
try{
Date ifModifiedSinceDate = httpDate.parse(ifModifiedSince);
if(_lastModifiedDate.getTime() <= ifModifiedSinceDate.getTime()){
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED );
return;
}
}
catch(Exception e){}
}
resp.setHeader("Last-Modified", httpDate.format(_lastModifiedDate));
resp.setHeader("Content-Length", String.valueOf(_fileLength));
resp.setHeader("ETag", _eTag);
//resp.setHeader("Content-Disposition", "attachment; filename=" + data.getDataFile().getName());
FileInputStream is = new FileInputStream(data.getDataFile());
int count = 0;
byte[] buffer = new byte[4096];
OutputStream servletOutput = resp.getOutputStream();
while((count = is.read(buffer)) > 0) {
servletOutput.write(buffer, 0, count);
}
servletOutput.close();
} catch (DotContentletStateException e) {
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (DotRuntimeException e) {
//Logger.error(BinaryExporterServlet.class, e.getMessage());
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (PortalException e) {
Logger.error(BinaryExporterServlet.class, e.getMessage());
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (SystemException e) {
Logger.error(BinaryExporterServlet.class, e.getMessage());
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (DotDataException e) {
Logger.error(BinaryExporterServlet.class, e.getMessage());
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (DotSecurityException e) {
try {
if(WebAPILocator.getUserWebAPI().isLoggedToFrontend(req)){
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
}else{
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
} catch (Exception e1) {
Logger.error(BinaryExporterServlet.class,e1.getMessage(),e1);
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
}
} catch (BinaryContentExporterException e) {
Logger.debug(BinaryExporterServlet.class, e.getMessage(),e);
Logger.error(BinaryExporterServlet.class, e.getMessage());
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
@SuppressWarnings("unchecked")
private Map sortByKey(Map map) {
List list = new LinkedList(map.entrySet());
Collections.sort(list, new Comparator() {
public int compare(Object o1, Object o2) {
return ((Comparable) ((Map.Entry) (o1)).getKey()).compareTo(((Map.Entry) (o2)).getKey());
}
});
// logger.info(list);
Map result = new LinkedHashMap();
for (Iterator it = list.iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
result.put(entry.getKey(), entry.getValue());
}
return result;
}
// Tries to find out whether this is content or a file
private boolean isContent(String id, boolean byInode, long langId, boolean respectFrontendRoles) throws DotStateException, DotDataException, DotSecurityException{
if(cacheMisses.containsKey(id+byInode)){
throw new DotStateException("404 - Unable to find id:" + id);
}
if(byInode){
try {
Contentlet c =APILocator.getContentletAPI().find(id, userAPI.getSystemUser(), true);
if(c != null && c.getInode() != null)
return true;
} catch (Exception e) {
Logger.debug(this.getClass(), "Unable to find contentlet " + id);
}
try {
if(fileAPI.find(id,userAPI.getSystemUser(),false) != null){
return false;
}
} catch (DotHibernateException e) {
Logger.debug(this.getClass(), "cant find file with inode " + id);
}
}
else{
try {
Identifier identifier = APILocator.getIdentifierAPI().loadFromCache(id);
if(identifier != null){
return "contentlet".equals(identifier.getAssetType());
}
//second check content check from lucene
String luceneQuery = "+identifier:" + id;
Contentlet c = APILocator.getContentletAPI().findContentletByIdentifier(id, false,langId, userAPI.getSystemUser(), false);
if(c != null && UtilMethods.isSet(c.getInode())){
return true;
}
} catch (Exception e) {
Logger.debug(this.getClass(), "cant find identifier " + id);
}
}
cacheMisses.put(id+byInode, true);
throw new DotStateException("404 - Unable to find id:" + id);
}
@SuppressWarnings("deprecation")
private Map cacheMisses = new LRUMap(1000);
private Map<String,String[]> getURIParams(HttpServletRequest request){
String url = request.getRequestURI().toString();
url = (url.startsWith("/")) ? url.substring(1, url.length()) : url;
String p[] = url.split("/");
Map<String, String[]> map = new HashMap<String, String[]>();
String key =null;
for(String x : p){
if(key ==null){
key = x;
}
else{
map.put(key, new String[]{x});
key = null;
}
}
return map;
}
}