UploadCommandExecutor.java

package cn.bluejoe.elfinder.controller.executors;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.fileupload.FileItem;
import org.json.JSONObject;

import cn.bluejoe.elfinder.controller.executor.AbstractJsonCommandExecutor;
import cn.bluejoe.elfinder.controller.executor.FsItemEx;
import cn.bluejoe.elfinder.service.FsService;
import sk.iway.iwcm.Adminlog;
import sk.iway.iwcm.Constants;
import sk.iway.iwcm.DB;
import sk.iway.iwcm.FileTools;
import sk.iway.iwcm.Identity;
import sk.iway.iwcm.Logger;
import sk.iway.iwcm.Tools;
import sk.iway.iwcm.common.DocTools;
import sk.iway.iwcm.common.FileIndexerTools;
import sk.iway.iwcm.common.ImageTools;
import sk.iway.iwcm.common.UploadFileTools;
import sk.iway.iwcm.editor.UploadFileAction;
import sk.iway.iwcm.editor.UploadFileForm;
import sk.iway.iwcm.findexer.FileIndexer;
import sk.iway.iwcm.findexer.ResultBean;
import sk.iway.iwcm.gallery.GalleryDB;
import sk.iway.iwcm.gallery.VideoConvert;
import sk.iway.iwcm.i18n.Prop;
import sk.iway.iwcm.io.IwcmFile;
import sk.iway.iwcm.io.IwcmFsDB;
import sk.iway.iwcm.system.metadata.MetadataCleaner;
import sk.iway.iwcm.users.UsersDB;

@SuppressWarnings("java:S116")
public class UploadCommandExecutor extends AbstractJsonCommandExecutor
{

	/**
	 * Odstranenie adresara z cesty k suboru pri uploade na windows z IE (ie bug)
	 */
	private static String fixFileNameDirPath(String fileName)
	{
		try
		{
			int lomka = fileName.lastIndexOf("/");
			if (lomka > 0) fileName = fileName.substring(lomka+1);
			lomka = fileName.lastIndexOf("\\");
			if (lomka > 0) fileName = fileName.substring(lomka+1);
		}
		catch (Exception e)
		{
			sk.iway.iwcm.Logger.error(e);
		}

		fileName = Tools.replace(fileName, "..", ".");

		return fileName;
	}

	private String getFilename(String filepath) {
		int lomka = filepath.lastIndexOf("/");

		if (lomka > -1) {
			return filepath.substring(lomka + 1);
		}

		return filepath;
	}

	private String getDirectory(String filepath)
	{
		int lomka = filepath.lastIndexOf("/");

		if (lomka > 0) {
			return filepath.substring(0, lomka);
		}

		return "";
	}

	private FsItemEx fileItemExFromDirectory(FsItemEx itemEx, String directory) throws IOException
	{
		String[] directoriesArray = Tools.getTokens(directory, "/");
		return new FsItemEx(itemEx, directoriesArray[0]);
	}

	// large file will be splitted into many parts
	class Part
	{
		long _start;
		long _size;
		FileItem _content;

		public Part(long start, long size, FileItem fileItem)
		{
			super();
			this._start = start;
			this._size = size;
			this._content = fileItem;
		}
	}

	// a large file with many parts
	static class Parts
	{
		public static synchronized Parts getOrCreate(
				HttpServletRequest request, String chunkId, String fileName,
				long total, long totalSize)
		{
			//chunkId is not an unique number for files uploaded in one upload form
			String key = String.format("chunk_%s_%s", chunkId, fileName);
			// stores chunks in application context
			Parts parts = (Parts) request.getServletContext().getAttribute(key);

			if (parts == null)
			{
				parts = new Parts(chunkId, fileName, total, totalSize);
				request.getServletContext().setAttribute(key, parts);
			}

			return parts;
		}

		private String _chunkId;
		// number of parts
		private long _numberOfParts;
		private long _totalSize;

		private String _fileName;

		// all chunks
		Map<Long, Part> _parts = new HashMap<>();

		public Parts(String chunkId, String fileName, long numberOfParts,
				long totalSize)
		{
			_chunkId = chunkId;
			_fileName = fileName;
			_numberOfParts = numberOfParts;
			_totalSize = totalSize;
		}

		public synchronized void addPart(long partIndex, Part part)
		{
			_parts.put(partIndex, part);
		}

		public boolean isReady()
		{
			return _parts.size() == _numberOfParts;
		}

		public InputStream openInputStream() throws IOException //NOSONAR
		{
			return new InputStream()
			{
				long partIndex = 0;
				Part part = _parts.get(partIndex);
				InputStream is = part._content.getInputStream();

				@Override
				public int read() throws IOException
				{
					while (true)
					{
						// current part is not read completely
						int c = is.read();
						if (c != -1)
						{
							return c;
						}

						// next part?
						if (partIndex == _numberOfParts - 1)
						{
							is.close();
							return -1;
						}

						part = _parts.get(++partIndex);
						is.close();
						is = part._content.getInputStream();
					}
				}

				@Override
				public int read(byte[] buffer, int start, int bufferSize) throws IOException
				{
					while (true)
					{
						// current part is not read completely
						int c = is.read(buffer, start, bufferSize);
						if (c != -1)
						{
							return c;
						}

						// next part?
						if (partIndex == _numberOfParts - 1)
						{
							is.close();
							return -1;
						}

						part = _parts.get(++partIndex);
						is.close();
						is = part._content.getInputStream();
					}
				}
			};
		}

		public void checkParts() throws IOException
		{
			long totalSize = 0;

			for (long i = 0; i < _numberOfParts; i++)
			{
				Part part = _parts.get(i);
				totalSize += part._size;
			}

			if (totalSize != _totalSize)
				throw new IOException(String.format(
						"invalid file size: excepted %d, but is %d",
						_totalSize, totalSize));
		}

		public void removeFromApplicationContext(HttpServletRequest request)
		{
			String key = String.format("chunk_%s_%s", _chunkId, _fileName);
			request.getServletContext().removeAttribute(key);

			for (long i = 0; i < _numberOfParts; i++)
			{
				_parts.get(i)._content.delete();
			}
		}
	}

	interface FileWriter
	{
		FsItemEx createAndSave(String fileName, InputStream is, long size)
				throws IOException;
	}

	@SuppressWarnings("java:S1075")
	@Override
	public void execute(FsService fsService, HttpServletRequest request, ServletContext servletContext, JSONObject json)
			throws Exception
	{
		@SuppressWarnings("unchecked")
		Map<String,FileItem> filesMap = (Map<String,FileItem>) request.getAttribute("MultipartWrapper.files");
		//List<FileItem> files = new ArrayList<FileItem>(filesMap.values());
		final List<FsItemEx> added = new ArrayList<>();

		String target = request.getParameter("target");
		String[] renames = request.getParameterValues("renames[]");

		final FsItemEx dir = super.findItem(fsService, target);
		Prop prop = Prop.getInstance(request);
		Identity user = sk.iway.iwcm.system.elfinder.FsService.getCurrentUser();

		final List<String> notUploaded = new ArrayList<>();
		@SuppressWarnings("unchecked")
		List<String> notUploadedSession = (List<String>) request.getAttribute("MultipartWrapper.notUploaded");
		if (notUploadedSession != null) notUploaded.addAll(notUploadedSession) ;

		if (user != null && UsersDB.isFolderWritable(user.getWritableFolders(), dir.getPath()) && filesMap != null)
		{

			if (renames != null && renames.length > 0) {
				String path = dir.getPath();
				for (String rename : renames) {
					File file = new File(Tools.getRealPath(path + "/" + rename));

					if (file.exists()) {
						int i = 1;

						File fileTo = null;
						do {
							String filename = rename.contains(".") ? rename.substring(0, rename.lastIndexOf(".")) + "-" + (i++) + rename.substring(rename.lastIndexOf("."), rename.length()) : rename + "-" + (i++);
							fileTo = new File(Tools.getRealPath(path + "/" + filename));
						}
						while (fileTo.exists());
						file.renameTo(fileTo); //NOSONAR

						FsItemEx fsItem = new FsItemEx(dir, fileTo.getName());
						added.add(fsItem);
					}
				}
			}

			for (Entry<String, FileItem> entry : filesMap.entrySet())
			{
				String filepath = entry.getKey();
				FileItem fi = entry.getValue();
				String directory = getDirectory(filepath);
				String fileName = getFilename(filepath);
				fileName = fixFileNameDirPath(fileName);

				//wj8 - elfinder - kontrola prav pre typy suborov (#20592)
				String uploadType = "file";
				if(dir.getPath().startsWith("/images")) uploadType = "image";

				if (dir.getPath().startsWith("/files") || dir.getPath().startsWith("/images") || dir.getPath().startsWith("/shared"))
				{
					fileName = DB.internationalToEnglish(fileName);
					fileName = DocTools.removeCharsDir(fileName, true).toLowerCase();

					directory = DB.internationalToEnglish(directory);
					directory = DocTools.removeCharsDir(directory, true).toLowerCase();
				}

				String realPathDir = Tools.getRealPath(dir.getPath());
				File file = new File(realPathDir);

				// is not update
				if (!file.exists() || !file.isFile()) {
					realPathDir = Tools.getRealPath(dir.getPath() + directory + "/" + fileName);
					file = new File(realPathDir);
				}

				final File newFile = file;
				//fi.write(newFile);
				final String uploadTypeFinal = uploadType;
				final String directoryFinal = directory;

				FileWriter fw = new FileWriter()
				{
					@Override
					public FsItemEx createAndSave(String fileName, InputStream is, long size)
							throws IOException
					{
						if (fileName.startsWith("/")) fileName = fileName.substring(1);
						fileName = DB.internationalToEnglish(fileName);
						fileName = DocTools.removeCharsDir(fileName, true).toLowerCase();
						fileName = Tools.replace(fileName, "/", ""+File.separatorChar);

						boolean isAllowedForUpload = UploadFileTools.isFileAllowed(uploadTypeFinal, fileName, size, user, request);

						if (isAllowedForUpload)
						{
							// fis.getName() returns full path such as 'C:\temp\abc.txt' in
							// IE10
							// while returns 'abc.txt' in Chrome
							// see
							// https://github.com/bluejoe2008/elfinder-2.x-servlet/issues/22
							//java.nio.file.Path p = java.nio.file.Paths.get(fileName);
							FsItemEx newFileEx = new FsItemEx(dir, fileName);

							/*
							 * String fileName = fis.getName(); FsItemEx newFile = new
							 * FsItemEx(dir, fileName);
							 */
							//newFileEx.createFile();
							//newFile.writeStream(is);

							File fileToSave = new File(newFile.getParentFile()+File.separator+fileName);
							IwcmFsDB.writeFiletoDest(is, fileToSave, (int)size, true);

							Adminlog.add(Adminlog.TYPE_FILE_UPLOAD, "File upload, path="+dir.getPath()+directoryFinal+"/"+fileName, -1, -1);

							//if (filter.accepts(newFile))
								added.add(newFileEx);

							return newFileEx;
						}
						else
						{
							notUploaded.add(fileName);
							return null;
						}
					}
				};

				// chunked upload
				if (request.getParameter("cid") != null)
				{
					processChunkUpload(request, filesMap, fw);
					if (added.size() > 0)
					{
						FsItemEx newFileEx = added.get(added.size()-1);
						fileName = newFileEx.getName();
					}
				}
				else
				{
					boolean isAllowedForUpload = UploadFileTools.isFileAllowed(uploadType, fileName, fi.getSize(), user, request);

					if (isAllowedForUpload)
					{
						IwcmFsDB.writeFiletoDest(fi.getInputStream(), newFile, (int)fi.getSize(), true);

						Adminlog.add(Adminlog.TYPE_FILE_UPLOAD, "elfinder upload, path="+dir.getPath()+directory+"/"+fileName, -1, -1);
					}
					else
					{
						notUploaded.add(fileName);
					}
				}

				IwcmFile newFileIwcm = new IwcmFile(newFile);
				if (newFileIwcm.exists()) {
					fi.delete();
					FsItemEx fsItem = new FsItemEx(dir, fileName);
					if (!dir.isFolder()) {
						fsItem = dir;
					}

					if (Tools.isEmpty(directory)) {
						added.add(fsItem);
					}
					else {
						FsItemEx itemDirectory = fileItemExFromDirectory(dir, directory);
						if (!added.contains(itemDirectory)) {
							added.add(itemDirectory);
						}
					}
				}

				//POZOR: vo WJ9 je toto presunute do UploadService, pozor ale na cast VideoConvert, kde sa pracuje s FsItem kvoli premenovaniu, to tu treba zachovat

				//kvoli chunked uploadu musime zrekonstruovat nanovo
				newFileIwcm = new IwcmFile(Tools.getRealPath(dir.getPath()+directory+"/"+fileName));

				//vycisti metadata
				MetadataCleaner.removeMetadata(newFileIwcm);

				//ak je treba, aplikujem vodotlac na obrazky
				GalleryDB.applyWatermarkOnUpload(newFileIwcm);

				// ak je to povolene, pokusime sa skonvertovat CMYK obrazok na RGB
				ImageTools.convertCmykToRgb(newFileIwcm.getAbsolutePath());

				//ak je to JPG obrazok, skusime ziskat datum vytvorenia fotografie na zaklade EXIF metadat
				Date dateCreated = GalleryDB.getExifDateOriginal(newFileIwcm);

				if (VideoConvert.isVideoFile(fileName))
				{
					if (fileName.endsWith("."+Constants.getString("defaultVideoFormat"))==false)
					{
						//nie je to mp4, treba skonvertovat
						UploadFileForm my_form = new UploadFileForm();
						my_form.setBitRate(Constants.getInt("defaultVideoBitrate"));
						my_form.setVideoWidth(Constants.getInt("defaultVideoWidth"));
						my_form.setVideoHeight(Constants.getInt("defaultVideoHeight"));
						my_form.setKeepOriginalVideo(false);

						//zmaz povodny added mpg subor
						for (FsItemEx item : added)
						{
							if (item.getPath().endsWith(fileName))
							{
								added.remove(item);
								break;
							}
						}

						String fileURL = VideoConvert.convert(my_form, newFileIwcm.getVirtualPath(), request);
						Logger.debug(this.getClass(), "Converted video: "+fileURL);
						if (Tools.isNotEmpty(fileURL) && fileURL.lastIndexOf("/")>1)
						{
							String videoFileName = fileURL.substring(fileURL.lastIndexOf("/") + 1);
							added.add(new FsItemEx(dir, videoFileName));
							added.add(new FsItemEx(dir, Tools.replace(videoFileName, "."+Constants.getString("defaultVideoFormat"), ".jpg")));
						}
					}
					else
					{
						//pre mp4 vytvorime len screenshot
						String image = VideoConvert.makeScreenshot(newFileIwcm.getAbsolutePath(), null);
						if (image != null)
                        {
                            String imageFilename = new IwcmFile(image).getName();

                            if (Tools.isEmpty(directory))
                            {
                                added.add(new FsItemEx(dir, imageFilename));
                            }
                        }
					}
				}

				if (FileTools.isImage(newFileIwcm.getName())) {
					if (GalleryDB.isGalleryFolder(dir.getPath()+directory)) {
						//we must replace o_ file because it will be used in resize process instead of new file
						IwcmFile orig = new IwcmFile(Tools.getRealPath(dir.getPath()+directory+"/o_"+fileName));
						if (orig.exists()) {
							FileTools.copyFile(newFileIwcm, orig);
						}

						GalleryDB.resizePicture(newFileIwcm.getAbsolutePath(), dir.getPath()+directory);
						added.add(new FsItemEx(dir, "s_"+fileName));
						added.add(new FsItemEx(dir, "o_"+fileName));
					} else if (Constants.getBoolean("imageAlwaysCreateGalleryBean")) {
						GalleryDB.setImage(dir.getPath()+directory, fileName);
					}

					//zapise datum vytvorenia fotografie (ak vieme ziskat)
					if (dateCreated != null) {
						GalleryDB.setUploadDateImage(dir.getPath()+directory, fileName, dateCreated.getTime());
					}
				}

				//ak existuje adresar files, treba indexovat
				if (FileIndexer.isFileIndexerConfigured())
				{
					List<ResultBean> indexedFiles = new ArrayList<>();
					FileIndexerTools.indexFile(dir.getPath() + directory + "/" + fileName, indexedFiles, request);
				}

				UploadFileAction.reflectionLoader(request, user, dir.getPath() + directory + "/" + fileName);
			}
		}
		else
		{
			json.put("error", prop.getText("components.elfinder.commands.upload.error"));
		}
		json.put("added", files2JsonArray(request, added));


		if (notUploaded != null && notUploaded.size()>0)
		{
			String fileNames = notUploaded.toString();
			json.put("error", prop.getText("admin.dragdropupload.forbidden", fileNames));
		}
	}

	private void processChunkUpload(HttpServletRequest request, Map<String,FileItem> filesMap, FileWriter fw)
				throws NumberFormatException, IOException
		{
			// cid : unique id of chunked uploading file
			String cid = request.getParameter("cid");
			// solr-5.5.2.tgz.48_65.part
			String chunk = request.getParameter("chunk");

			// 100270176,2088962,136813192
			String range = request.getParameter("range");
			String[] tokens = range.split(",");

			Logger.debug(UploadCommandExecutor.class, "chunk="+chunk+" range="+range+" cid="+cid);

			Matcher m = Pattern.compile("(.*)\\.([0-9]+)\\_([0-9]+)\\.part") //NOSONAR
					.matcher(chunk);

			if (m.find())
			{
				String fileName = m.group(1);
				String uploadPath = request.getParameter("upload_path[]");
				if (Tools.isNotEmpty(uploadPath) && uploadPath.startsWith("/"))
				{
					fileName = uploadPath;
				}
				long index = Long.parseLong(m.group(2));
				long total = Long.parseLong(m.group(3));

				Parts parts = Parts.getOrCreate(request, cid, fileName, total + 1,
						Long.parseLong(tokens[2]));

				long start = Long.parseLong(tokens[0]);
				long size = Long.parseLong(tokens[1]);

				Logger.debug(UploadCommandExecutor.class, String.format("uploaded part(%d/%d) of file: %s",
						index, total, fileName));

				parts.addPart(index, new Part(start, size, filesMap.get("upload[]")));
				Logger.debug(UploadCommandExecutor.class, String.format(">>>>%d", parts._parts.size()));
				if (parts.isReady())
				{
					parts.checkParts();

					Logger.debug(UploadCommandExecutor.class, String.format("file is uploadded completely: %s",
							fileName));

					long totalSize = 0;
					for (long i = 0; i < parts._numberOfParts; i++)
					{
						totalSize += parts._parts.get(i)._content.getSize();
					}

					fw.createAndSave(fileName, parts.openInputStream(), totalSize);

					// remove from application context
					parts.removeFromApplicationContext(request);
				}
			}
		}
}