view src/nabble/model/FileUpload.java @ 5:b74139388033

nabble_img relative path
author Franklin Schmidt <fschmidt@gmail.com>
date Sun, 07 Jul 2019 17:15:31 -0600
parents 7ecd1a4ef557
children c4ed473452d4
line wrap: on
line source

package nabble.model;

import fschmidt.db.DbDatabase;
import fschmidt.db.Listener;
import fschmidt.html.HtmlTag;
import fschmidt.util.java.ImageUtils;
import nabble.view.web.forum.FileDownload;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.image.ImagingOpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


public final class FileUpload {
	private static final Logger logger = LoggerFactory.getLogger(FileUpload.class);

	public static final int MAX_IMAGE_SIZE = 1048576;
	public static final int MAX_FILE_SIZE = 5242880;

	public static class FileDetails {
		private String name;
		private Date date;

		public FileDetails(String name, Date date) {
			this.name = name;
			this.date = date;
		}

		public String getName() { return name; }
		public Date getDate() { return date; }
	}

	private static void addToSql(Message.Source src,StringBuilder sql) {
		Message.SourceType type = src.getMessageSourceType();
		sql.append( " from file_" ).append( type.getName() );
		String idField = type.getIdField();
		if( idField == null ) {
			sql.append( " where true" );
		} else {
			sql.append( " where " ).append( idField ).append( " = ?" );
		}
	}

	private static int setParams(Message.Source src,PreparedStatement pstmt)
		throws SQLException
	{
		int i = 0;
		if( src.getMessageSourceType().getIdField() != null ) {
			pstmt.setLong(++i,src.getSourceId());
		}
		return i;
	}

	public static InputStream getFileContent(Message.Source src,String name) {
		if( src==null )
			return null;
		Message.SourceType type = src.getMessageSourceType();
		try {
			final Connection con = src.getSite().getDb().getConnection();
			StringBuilder sql = new StringBuilder();
			sql.append( "select content" );
			addToSql(src,sql);
			sql.append( " and name = ?" );
			final PreparedStatement pstmt = con.prepareStatement(sql.toString());
			boolean isDone = false;
			try {
				int i = setParams(src,pstmt);
				pstmt.setString(++i,name);
				ResultSet rs = pstmt.executeQuery();
				if( !rs.next() )
					return null;
				InputStream rtn = new FilterInputStream(rs.getBinaryStream("content")) {
					public void close() throws IOException {
						super.close();
						try {
							pstmt.close();
							con.close();
						} catch(SQLException e) {
							throw new RuntimeException(e);
						}
					}
				};
				isDone = true;
				return rtn;
			} finally {
				if( !isDone ) {
					pstmt.close();
					con.close();
				}
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	static boolean hasFile(Message.Source src,String name) {
		Message.SourceType type = src.getMessageSourceType();
		try {
			final Connection con = src.getSite().getDb().getConnection();
			StringBuilder sql = new StringBuilder();
			sql.append( "select 'whatever'" );
			addToSql(src,sql);
			sql.append( " and name = ?" );
			final PreparedStatement pstmt = con.prepareStatement(sql.toString());
			try {
				int i = setParams(src,pstmt);
				pstmt.setString(++i,name);
				return pstmt.executeQuery().next();
			} finally {
				pstmt.close();
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public static FileDetails[] getFileDetails(Message.Source src,String prefix) {
		Message.SourceType type = src.getMessageSourceType();
		try {
			final Connection con = src.getSite().getDb().getConnection();
			StringBuilder sql = new StringBuilder();
			sql.append( "select name, date_" );
			addToSql(src,sql);
			if( prefix != null )
				sql.append(  " and name like '" ).append( prefix ).append( "%'" );
			sql.append( " order by name" );
			final PreparedStatement pstmt = con.prepareStatement(sql.toString());
			try {
				setParams(src,pstmt);
				List<FileDetails> names = new ArrayList<FileDetails>();
				ResultSet set = pstmt.executeQuery();
				while (set.next()) {
					names.add(new FileDetails(set.getString("name"), set.getDate("date_")));
				}
				return names.toArray(new FileDetails[0]);
			} finally {
				pstmt.close();
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public static String getName(FileItem fi) {
		String[] a = fi.getName().split("[\\\\/:]");
		String[] b = a[a.length-1].split("\\.");
		String ext2 = b[b.length-1];
		int n = (int)(Math.random()*1000);
		if(b[0].equals("image"))
		b[0] = b[0]+n;
		return b[0]+"."+ext2;
	}

	public static String uploadImage(final FileItem fi,Message.Source src, int resize)
		throws ModelException
	{
		try {
			return uploadImage1(fi,src,resize);
		} catch(IOException e) {
			throw ModelException.newInstance("upload_image_failed",e);
		}
	}

	private static String uploadImage1(final FileItem fi,Message.Source src, int resize)
		throws ModelException, IOException
	{
		InputStream in = fi.getInputStream();
		BufferedImage bi;
		try {
			bi = ImageIO.read(in);
		} catch(IllegalArgumentException e) {
			throw ModelException.newInstance("upload_image_failed",e);
		} catch (RuntimeException e) {
			throw ModelException.newInstance("unknown_image_error",e);
		}
		in.close();
		if (bi == null)
			throw ModelException.newInstance("unsupported_image_type","Unsupported image type - please use PNG, JPG or GIF.");

		String filename = getName(fi);
		filename = filename.replaceAll("[ ']+","_"); // Replace spaces and single quotes with underscores
		int iDot = filename.lastIndexOf('.');
		if( iDot == -1 )
			throw ModelException.newInstance("file_has_no_ending","File must have ending");
		String ending = filename.substring(iDot+1).toLowerCase();
		InputStream bais;
		long size;
		if (resize > 0) {
			try {
				bi = ImageUtils.getThumbnail(bi, resize, resize);
				final byte[] contents = ImageUtils.asOutputStream(bi, ending).toByteArray();
				bi = null;
				size = contents.length;
				bais = new ByteArrayInputStream(contents);
			} catch (ImagingOpException e) {
				throw ModelException.newInstance("unable_to_resize_image","Unable to resize image", e);
			}
		} else {
			bi = null;
			bais = fi.getInputStream();
			size = fi.getSize();
		}
		final InputStream inputStream = bais;
		if (size <= MAX_IMAGE_SIZE) {
			filename = filename.substring(0,iDot+1) + ending;
			return uploadFile2(size,filename,src, true,
				new InputStreamFactory() {
					public InputStream in() throws IOException {
						return inputStream;
					}
				}
			);
		} else {
			throw ModelException.newInstance("image_too_large","Image is too large: use the resize option to make it smaller. Maximum size 1MB.");
		}
	}

	/*
	static boolean isDifferent(final FileItem fi,Message.Source src)
		throws ModelException
	{
		try {
			InputStream inDb = getFileContent(src,fi.getName());
			if( inDb==null )
				return true;
			InputStream inFi = null;
			try {
				inFi = fi.getInputStream();
				return IoUtils.compare(inDb,inFi) != 0;
			} finally {
				if (inFi != null)
					inFi.close();
				inDb.close();
			}
		} catch(IOException e) {
			throw ModelException.newInstance("file_io_exception",e);
		}
	}
	*/

	public static String uploadFile(final FileItem fi,Message.Source src)
		throws ModelException
	{
		try {
			return uploadFile1(fi,src);
		} catch(IOException e) {
			throw ModelException.newInstance("file_io_exception",e);
		}
	}

	private static String uploadFile1(final FileItem fi,Message.Source src)
		throws ModelException, IOException
	{
		String filename = getName(fi);
		filename = filename.replaceAll("[ ']+", "_"); // Replace spaces and single quotes with underscores
		long size = fi.getSize();
		return uploadFile2(size,filename,src, true,
			new InputStreamFactory() {
				public InputStream in() throws IOException {
					return fi.getInputStream();
				}
			}
		);
	}

	private static interface InputStreamFactory {
		public InputStream in() throws IOException;
	}

	private static String uploadFile2(long size, String filename, final Message.Source src, boolean checkSize, InputStreamFactory inf)
		throws ModelException, IOException
	{
		if( "".equals(filename.trim()) )
			throw ModelException.newInstance("empty_filename","empty filename");
		if( size==0 )
			throw ModelException.newInstance("empty_file","empty file");
		if( size > MAX_FILE_SIZE && checkSize )
			throw ModelException.newInstance("file_is_too_large","file is too large - maximum size 5mb");
		synchronized(src) {
			try {
				Connection con = src.getSite().getDb().getConnection();
				try {
					{
						StringBuilder sql = new StringBuilder();
						sql.append( "delete" );
						addToSql(src,sql);
						sql.append( " and name = ?" );
						PreparedStatement pstmt = con.prepareStatement(sql.toString());
						int i = setParams(src,pstmt);
						pstmt.setString(++i,filename);
						pstmt.executeUpdate();
						pstmt.close();
					}
					{
						StringBuilder sql = new StringBuilder();
						Message.SourceType type = src.getMessageSourceType();
						sql.append( "insert into file_" ).append( type.getName() );
						sql.append( " ( name, content" );
						String idField = type.getIdField();
						if( idField != null )
							sql.append( ", " ).append( idField );
						sql.append( " ) values (?,?" );
						if( idField != null )
							sql.append( ",?" );
						sql.append( ")" );
						PreparedStatement pstmt = con.prepareStatement(sql.toString());
						pstmt.setString(1,filename);
						InputStream in = inf.in();
						pstmt.setBinaryStream(2,in,(int)size);
						if( idField != null )
							pstmt.setLong(3,src.getSourceId());
						pstmt.executeUpdate();
						in.close();
						pstmt.close();
					}
					return filename;
				} finally {
					con.close();
					src.getSite().getDb().runAfterCommit(new Runnable(){public void run(){
						fireFileUpdateListeners(src);
					}});
				}
			} catch(SQLException e) {
				throw new RuntimeException(e);
			}
		}
	}


	private static List<Listener<Message.Source>> fileUpdateListeners = new ArrayList<Listener<Message.Source>>();

	public static void addFileUpdateListener(Listener<Message.Source> listener) {
		fileUpdateListeners.add(listener);
	}

	static void fireFileUpdateListeners(Message.Source src) {
		for( Listener<Message.Source> listener : fileUpdateListeners ) {
			listener.event(src);
		}
	}


	static {
		NodeImpl.addPostInsertListener(new Listener<NodeImpl>(){
			public void event(NodeImpl node) {
				Message.Format fmt = node.getMessage().getFormat();
				if( !(fmt instanceof MailMessageFormat) && node.getOwner() instanceof User)
					fixFileTags(node.getMessage(),(User)node.getOwner());
			}
		});
		NodeImpl.addPostUpdateListener(new Listener<NodeImpl>(){
			public void event(NodeImpl node) {
				if( ModelHome.insideImportProcedure.get() )
					return;
				Message.Format fmt = node.getMessage().getFormat();
				if( !(fmt instanceof MailMessageFormat)  )
					deleteUnusedFiles(node.getMessage());
			}
		});
	}

	private static void deleteUnusedFiles(Message message) {
		Message.Source src = message.getSource();
		if (src == null)
			return;
		DbDatabase db = src.getSite().getDb();
		Message.SourceType type = src.getMessageSourceType();
		Set<String> names = getFileInfo(message.parse(),src).keySet();
		StringBuilder sql = new StringBuilder();
		sql.append( "delete" );
		addToSql(src,sql);
		if( !names.isEmpty() ) {
			sql.append( " and name not in (" );
			Iterator<String> iter = names.iterator();
			sql.append( db.arcana().quote(iter.next()) );
			while( iter.hasNext() ) {
				sql.append( ',' );
				sql.append( db.arcana().quote(iter.next()) );
			}
			sql.append( ")" );
		}
		try {
			Connection con = db.getConnection();
			PreparedStatement pstmt = con.prepareStatement(sql.toString());
			setParams(src,pstmt);
			pstmt.executeUpdate();
			pstmt.close();
			con.close();
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public static void deleteFile(String fileName, Message.Source src) {
		Message.SourceType type = src.getMessageSourceType();
		try {
			Connection con = src.getSite().getDb().getConnection();
			try {
				StringBuilder sql = new StringBuilder();
				sql.append( "delete" );
				addToSql(src,sql);
				sql.append( " and name = ?" );
				PreparedStatement pstmt = con.prepareStatement(sql.toString());
				int i = setParams(src,pstmt);
				pstmt.setString(++i,fileName);
				pstmt.executeUpdate();
				pstmt.close();
			} finally {
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	private static void fixFileTags(Message message,User user) {
		for( Object obj : message.parse() ) {
			if( !(obj instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)obj;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("nabble_a") ) {
				fix(tag,"href",message.getSource(),user);
			} else if( tagName.equals("nabble_img") ) {
				fix(tag,"src",message.getSource(),user);
			}
		}
	}

	public static void checkFileTags(Message message,Person visitor) throws ModelException {
		Set<String> fileNames = new HashSet<String>();
		if( visitor instanceof User ) {
			User user = (User)visitor;
			Message.Source srcTemp = Message.SourceType.getType('t').getSource(user.getSite(),user.getId());
			FileDetails[] fileDetails = getFileDetails(srcTemp, null);
			for (FileDetails d : fileDetails) {
				fileNames.add(d.name);
			}
		}
		for( Object obj : message.parse() ) {
			if( !(obj instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)obj;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("nabble_a") ) {
				String href = HtmlTag.unquote(tag.getAttributeValue("href"));
				if (!fileNames.contains(href))
					throw new ModelException.InvalidFile(href);
			} else if( tagName.equals("nabble_img") ) {
				String src = HtmlTag.unquote(tag.getAttributeValue("src"));
				if (!fileNames.contains(src))
					throw new ModelException.InvalidFile(src);
			}
		}
	}

	private static void fix(HtmlTag tag,String fileAttr,Message.Source src,User user)
	{
		String filename = HtmlTag.unquote(tag.getAttributeValue(fileAttr));
		if( filename==null )
			return;
		Message.SourceType type = src.getMessageSourceType();
		try {
			Connection con = src.getSite().getDb().getConnection();
			try {
				{
					PreparedStatement pstmt = con.prepareStatement(
						"select 'x' from file_temp"
						+" where user_id = ?"
						+" and name = ?"
					);
					pstmt.setLong(1,user.getId());
					pstmt.setString(2,filename);
					ResultSet rs = pstmt.executeQuery();
					try {
						if( !rs.next() )
							return;
					} finally {
						rs.close();
						pstmt.close();
					}
				}
				{
					StringBuilder sql = new StringBuilder();
					sql.append( "delete" );
					addToSql(src,sql);
					sql.append( " and name = ?" );
					PreparedStatement pstmt = con.prepareStatement(sql.toString());
					int i = setParams(src,pstmt);
					pstmt.setString(++i,filename);
					pstmt.executeUpdate();
					pstmt.close();
				}
				PreparedStatement pstmt = con.prepareStatement(
					"insert into file_" + type.getName()
					+" (" + type.getIdField() + ", name, content)"
					+" select ?, name, content"
					+" from file_temp"
					+" where user_id = ?"
					+" and name=?"
				);
				pstmt.setLong(1,src.getSourceId());
				pstmt.setLong(2,user.getId());
				pstmt.setString(3,filename);
				pstmt.executeUpdate();
				pstmt.close();
				{
					Statement stmt = con.createStatement();
					stmt.executeUpdate(
						"delete from file_temp"
						+" where date_ < " + Db.arcana.dateSub("now()",1,"day")
					);
					stmt.close();
				}
			} finally {
				con.close();
			}
		} catch(SQLException e) {
			throw new RuntimeException(e);
		}
	}

	public static void processFileTags(List<Object> list,Message.Source src) {
		if( src == null )
			return;
		for( Object obj : list ) {
			if( !(obj instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)obj;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("nabble_a") ) {
				String filename = HtmlTag.unquote(tag.getAttributeValue("href"));
				if( filename==null )
					continue;
				String url = getUrl(filename,src);
				tag.setAttribute("href",HtmlTag.quote(url));
				tag.setName("a");
				if( tag.getAttributeValue("target") == null ) {
					tag.setAttribute("target","\"_top\"");
				}
			} else if( tagName.equals("/nabble_a") ) {
				tag.setName("/a");
			} else if( tagName.equals("nabble_img") ) {
				String filename = HtmlTag.unquote(tag.getAttributeValue("src"));
				if( filename==null )
					continue;
				String url = getPath(filename,src);
				tag.setAttribute("src",HtmlTag.quote(url));
				tag.setName("img");
			}
		}
	}

	static Map<String,String> getFileInfo(List<Object> list,Message.Source src) {
		Map<String,String> info = new HashMap<String,String>();
		for( Object obj : list ) {
			if( !(obj instanceof HtmlTag) )
				continue;
			HtmlTag tag = (HtmlTag)obj;
			String tagName = tag.getName().toLowerCase();
			if( tagName.equals("nabble_a") ) {
				String filename = HtmlTag.unquote(tag.getAttributeValue("href"));
				if( filename==null )
					continue;
				String url = getUrl(filename,src);
				info.put(filename,url);
			} else if( tagName.equals("nabble_img") ) {
				String filename = HtmlTag.unquote(tag.getAttributeValue("src"));
				if( filename==null )
					continue;
				String url = getUrl(filename,src);
				info.put(filename,url);
			}
		}
		return info;
	}

	static String getUrl(String filename,Message.Source src) {
		return FileDownload.url(filename,src);
	}

	static String getPath(String filename,Message.Source src) {
		return FileDownload.path(filename,src);
	}

	public static void saveImage(BufferedImage image, String fileName, Message.Source src)
			throws ModelException
	{
		try {
			// First convert to byte array
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			ImageIO.write(image, "png", baos);
			final byte[] bytes = baos.toByteArray();

			InputStreamFactory factory = new InputStreamFactory() {
				public InputStream in() throws IOException {
					return new ByteArrayInputStream(bytes);
				}
			};
			uploadFile2(bytes.length, fileName, src, true, factory);
		} catch(IOException e) {
			throw ModelException.newInstance("file_io_exception",e);
		}
	}

	public static void saveFile(final byte[] contents, String fileName, Message.Source src)
			throws ModelException
	{
		saveFile(contents, fileName, src, true);
	}

	public static void saveFile(final byte[] contents, String fileName, Message.Source src, boolean checkSize)
			throws ModelException
	{
		try {
			InputStreamFactory factory = new InputStreamFactory() {
				public InputStream in() throws IOException {
					return new ByteArrayInputStream(contents);
				}
			};
			uploadFile2(contents.length, fileName, src, checkSize, factory);
		} catch(IOException e) {
			throw ModelException.newInstance("file_io_exception",e);
		}
	}


	public static FileDetails[] getFiles(Message.Source src) {
		return getFileDetails(src, null);
	}

	public static final class UrlFileItem implements FileItem {
		private final URL url;

		public UrlFileItem(URL url) {
			this.url = url;
		}

		public InputStream getInputStream()
			throws IOException
		{
			try {
				return url.openConnection().getInputStream();
			} catch(IllegalArgumentException e) {
				logger.warn("",e);
				throw new IOException(e.getMessage());
			} catch(RuntimeException e) {
				logger.error("url = "+url,e);
				throw e;
			}
		}

		public String getContentType() {
			throw new UnsupportedOperationException();
		}

		public String getName() {
			String s = url.getPath();
			int i = s.lastIndexOf('/');
			if( i != -1 )
				s = s.substring(i+1);
			return s;
		}

		public boolean isInMemory() {
			return false;
		}

		public long getSize() {
			try {
				long len = url.openConnection().getContentLength();
				if( len == -1 ) {
					len = 0;
					InputStream in = getInputStream();
					while(true) {
						long n = in.skip(1000000L);
						len += n;
						if( n==0 ) {
							if( in.read() == -1 )
								break;
							len++;
						}
					}
					in.close();
				}
				return len;
			} catch(IOException e) {
				throw new RuntimeException(e);
			}
		}

		public byte[] get() {
			throw new UnsupportedOperationException();
		}

		public java.lang.String getString(java.lang.String encoding)
			throws java.io.UnsupportedEncodingException
		{
			throw new UnsupportedOperationException();
		}

		public java.lang.String getString() {
			throw new UnsupportedOperationException();
		}

		public void write(java.io.File file)
			throws java.lang.Exception
		{
			throw new UnsupportedOperationException();
		}

		public void delete() {
			throw new UnsupportedOperationException();
		}

		public java.lang.String getFieldName() {
			throw new UnsupportedOperationException();
		}

		public void setFieldName(java.lang.String name) {
			throw new UnsupportedOperationException();
		}

		public boolean isFormField() {
			return false;
		}

		public void setFormField(boolean state) {
			throw new UnsupportedOperationException();
		}

		public java.io.OutputStream getOutputStream()
			throws java.io.IOException
		{
			throw new UnsupportedOperationException();
		}

		public FileItemHeaders getHeaders() {
			throw new UnsupportedOperationException();
		}

		public void setHeaders(FileItemHeaders fileItemHeaders) {
			throw new UnsupportedOperationException();
		}

		public String toString() {
			return "UrlFileItem-"+url;
		}
	}



	static void nop() {}

	private FileUpload() {}  // never
}