/* * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Andreas Kalogeropoulos * Anahide Tchertchian * Thierry Delprat * Florent Guillaume */ package org.nuxeo.ecm.webapp.filemanager; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jboss.seam.ScopeType; import org.jboss.seam.annotations.In; import org.jboss.seam.annotations.Install; import org.jboss.seam.annotations.Name; import org.jboss.seam.annotations.Scope; import org.jboss.seam.annotations.remoting.WebRemote; import org.jboss.seam.core.Events; import org.jboss.seam.faces.FacesMessages; import org.jboss.seam.international.StatusMessage; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.Blobs; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.RecoverableClientException; import org.nuxeo.ecm.core.api.impl.blob.FileBlob; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.schema.FacetNames; import org.nuxeo.ecm.platform.filemanager.api.FileManager; import org.nuxeo.ecm.platform.types.TypeManager; import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; import org.nuxeo.ecm.platform.ui.web.api.UserAction; import org.nuxeo.ecm.platform.ui.web.util.files.FileUtils; import org.nuxeo.ecm.platform.web.common.exceptionhandling.ExceptionHelper; import org.nuxeo.ecm.webapp.clipboard.ClipboardActions; import org.nuxeo.ecm.webapp.contentbrowser.DocumentActions; import org.nuxeo.ecm.webapp.helpers.EventManager; import org.nuxeo.ecm.webapp.helpers.EventNames; import org.nuxeo.runtime.api.Framework; import org.richfaces.event.FileUploadEvent; import org.richfaces.model.UploadedFile; @Name("FileManageActions") @Scope(ScopeType.EVENT) @Install(precedence = Install.FRAMEWORK) public class FileManageActionsBean implements FileManageActions { private static final Log log = LogFactory.getLog(FileManageActionsBean.class); public static final String TRANSF_ERROR = "TRANSF_ERROR"; public static final String SECURITY_ERROR = "SECURITY_ERROR"; public static final String MOVE_ERROR = "MOVE_ERROR"; public static final String COPY_ERROR = "COPY_ERROR"; public static final String PASTE_ERROR = "PASTE_ERROR"; public static final String MOVE_IMPOSSIBLE = "MOVE_IMPOSSIBLE"; public static final String MOVE_PUBLISH = "MOVE_PUBLISH"; public static final String MOVE_OK = "MOVE_OK"; protected static final String FILES_SCHEMA = "files"; protected static final String FILES_PROPERTY = FILES_SCHEMA + ":files"; // TODO NXP-13568: this should not be hardcoded on the doc type protected static final String SECTION_DOCTYPE = "Section"; @In(create = true, required = false) protected CoreSession documentManager; @In(create = true) protected TypeManager typeManager; @In(create = true) protected NavigationContext navigationContext; @In(create = true) protected transient DocumentActions documentActions; @In(create = true) protected ClipboardActions clipboardActions; @In(create = true, required = false) protected UploadItemHolder fileUploadHolder; @In(create = true, required = false) protected UploadItemHolderCycleManager fileUploadHolderCycle; /** * Helper field to get the filename to remove. * * @since 5.9.2 */ protected String fileToRemove; @In(create = true, required = false) protected FacesMessages facesMessages; @In(create = true) protected Map<String, String> messages; protected FileManager fileManager; /** * Used to keep track of the path of the uploaded file (NXP-16745) */ protected List<String> tmpFilePaths = new ArrayList<String>(); protected FileManager getFileManagerService() { if (fileManager == null) { fileManager = Framework.getService(FileManager.class); } return fileManager; } @Override public String display() { return "view_documents"; } /** * Creates a document from the file held in the fileUploadHolder. Takes responsibility for the fileUploadHolder * temporary file. */ @Override public String addFile() { NxUploadedFile uploadedFile = fileUploadHolder.getUploadedFiles().iterator().next(); Blob blob = uploadedFile.getBlob(); if (blob == null || blob.getFilename() == null) { facesMessages.add(StatusMessage.Severity.ERROR, messages.get("fileImporter.error.nullUploadedFile")); return navigationContext.getActionResult(navigationContext.getCurrentDocument(), UserAction.AFTER_CREATE); } FileUtils.configureFileBlob(blob); DocumentModel currentDocument = navigationContext.getCurrentDocument(); String path = currentDocument.getPathAsString(); DocumentModel createdDoc = null; try { createdDoc = getFileManagerService().createDocumentFromBlob(documentManager, blob, path, true, blob.getFilename()); } catch (IOException e) { throw new NuxeoException("Can not write blob for" + blob.getFilename(), e); } EventManager.raiseEventsOnDocumentSelected(createdDoc); Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument); facesMessages.add(StatusMessage.Severity.INFO, messages.get("document_saved"), messages.get(createdDoc.getType())); return navigationContext.getActionResult(createdDoc, UserAction.AFTER_CREATE); } @Override @Deprecated // TODO: update the Seam remoting-based desktop plugins to stop calling // this method @WebRemote public boolean canWrite() { // let the FolderImporter and FileImporter plugin handle the security // checks to avoid hardcoded behavior return true; } protected String getErrorMessage(String errorType, String errorInfo) { return getErrorMessage(errorType, errorInfo, "message.operation.fails.generic"); } protected String getErrorMessage(String errorType, String errorInfo, String errorLabel) { return String.format("%s |(%s)| %s", errorType, errorInfo, messages.get(errorLabel)); } /** * @deprecated use addBinaryFileFromPlugin with a Blob argument API to avoid loading the content in memory */ @Override @Deprecated @WebRemote public String addFileFromPlugin(String content, String mimetype, String fullName, String morePath, Boolean UseBase64) { byte[] bcontent; if (UseBase64.booleanValue()) { bcontent = Base64.decodeBase64(content); } else { bcontent = content.getBytes(); } return addBinaryFileFromPlugin(bcontent, mimetype, fullName, morePath); } @Override @WebRemote public String addBinaryFileFromPlugin(Blob blob, String fullName, String morePath) { DocumentModel currentDocument = navigationContext.getCurrentDocument(); String curPath = currentDocument.getPathAsString(); String path = curPath + morePath; return createDocumentFromBlob(blob, fullName, path); } @Override @WebRemote public String addBinaryFileFromPlugin(Blob blob, String fullName, DocumentModel targetContainer) { return createDocumentFromBlob(blob, fullName, targetContainer.getPathAsString()); } protected String createDocumentFromBlob(Blob blob, String fullName, String path) { DocumentModel createdDoc; try { createdDoc = getFileManagerService().createDocumentFromBlob(documentManager, blob, path, true, fullName); } catch (NuxeoException | IOException t) { Throwable unwrappedError = ExceptionHelper.unwrapException(t); if (ExceptionHelper.isSecurityError(unwrappedError)) { // security check failed log.debug("No permissions creating " + fullName); return getErrorMessage(SECURITY_ERROR, fullName, "Error.Insuffisant.Rights"); } else { // log error stack trace for server side debugging while giving // a generic and localized error message to the client log.error("Error importing " + fullName, t); return getErrorMessage(TRANSF_ERROR, fullName); } } if (createdDoc == null) { log.error("could not create the document " + fullName); return getErrorMessage(TRANSF_ERROR, fullName); } // update the context, raise events to update the seam context DocumentModel currentDocument = navigationContext.getCurrentDocument(); if (currentDocument.getRef().equals(createdDoc.getRef())) { navigationContext.updateDocumentContext(createdDoc); } Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument); EventManager.raiseEventsOnDocumentSelected(createdDoc); return createdDoc.getName(); } /** * @deprecated Use addBinaryFileFromPlugin(Blob, String, String) to avoid loading the data in memory as a Bytes * array */ @Deprecated public String addBinaryFileFromPlugin(byte[] content, String mimetype, String fullName, String morePath) { Blob blob = Blobs.createBlob(content); return addBinaryFileFromPlugin(blob, fullName, morePath); } @Override @WebRemote public String addFolderFromPlugin(String fullName, String morePath) { try { DocumentModel currentDocument = navigationContext.getCurrentDocument(); String curPath = currentDocument.getPathAsString(); if (!currentDocument.isFolder()) { curPath = curPath.substring(0, curPath.lastIndexOf('/')); } String path = curPath + morePath; DocumentModel createdDoc; try { createdDoc = getFileManagerService().createFolder(documentManager, fullName, path, true); } catch (NuxeoException | IOException t) { Throwable unwrappedError = ExceptionHelper.unwrapException(t); if (ExceptionHelper.isSecurityError(unwrappedError)) { // security check failed log.debug("No permissions creating folder " + fullName); return getErrorMessage(SECURITY_ERROR, fullName, "Error.Insuffisant.Rights"); } else { log.error("Couldn't create the folder " + fullName); return getErrorMessage(TRANSF_ERROR, fullName); } } if (createdDoc == null) { log.error("Couldn't create the folder " + fullName); return getErrorMessage(TRANSF_ERROR, fullName); } EventManager.raiseEventsOnDocumentSelected(createdDoc); Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument); return createdDoc.getName(); } catch (RecoverableClientException e) { throw e; } catch (NuxeoException e) { throw new RecoverableClientException("Cannot validate, caught exception", "error.db.fs", null, e); } } @WebRemote protected String checkMoveAllowed(DocumentRef docRef, DocumentRef containerRef) { DocumentModel doc = documentManager.getDocument(docRef); DocumentModel container = documentManager.getDocument(containerRef); // check that we are not trying to move a folder inside itself if ((container.getPathAsString() + "/").startsWith(doc.getPathAsString() + "/")) { facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_impossible")); return MOVE_IMPOSSIBLE; } if (!doc.isProxy() && container.hasFacet(FacetNames.PUBLISH_SPACE) && !doc.hasFacet(FacetNames.PUBLISH_SPACE)) { // we try to do a publication check browse in sections if (!documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) { // only publish via D&D if this can be done directly (no wf) // => need to have write access facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_insuffisant_rights")); // TODO: this should be PUBLISH_IMPOSSIBLE return MOVE_IMPOSSIBLE; } if (doc.hasFacet(FacetNames.PUBLISHABLE)) { return MOVE_PUBLISH; } else { facesMessages.add(StatusMessage.Severity.WARN, messages.get("publish_impossible")); // TODO: this should be PUBLISH_IMPOSSIBLE return MOVE_IMPOSSIBLE; } } // this is a real move operation (not a publication) // check the right to remove the document from the source container if (!documentManager.hasPermission(doc.getParentRef(), SecurityConstants.REMOVE_CHILDREN) || !documentManager.hasPermission(doc.getRef(), SecurityConstants.REMOVE)) { facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_impossible")); return MOVE_IMPOSSIBLE; } // check that we have the right to create the copy in the target if (!documentManager.hasPermission(containerRef, SecurityConstants.ADD_CHILDREN)) { facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_insuffisant_rights")); return MOVE_IMPOSSIBLE; } if (doc.isProxy()) { if (!container.hasFacet(FacetNames.PUBLISH_SPACE)) { // do not allow to move a published document back in a // workspace facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_impossible")); return MOVE_IMPOSSIBLE; } } else { // check allowed content types constraints for non-proxy documents if (!typeManager.isAllowedSubType(doc.getType(), container.getType(), container)) { facesMessages.add(StatusMessage.Severity.WARN, messages.get("move_impossible")); return MOVE_IMPOSSIBLE; } } return MOVE_OK; } @Override @WebRemote public String moveWithId(String docId, String containerId) { try { String debug = "move " + docId + " into " + containerId; log.debug(debug); if (docId.startsWith("docRef:")) { docId = docId.split("docRef:")[1]; } if (docId.startsWith("docClipboardRef:")) { docId = docId.split("docClipboardRef:")[1]; } DocumentRef srcRef = new IdRef(docId); String dst = containerId; if (dst.startsWith("docRef:")) { dst = dst.split("docRef:")[1]; } if (dst.startsWith("nodeRef:")) { dst = dst.split("nodeRef:")[1]; } DocumentRef dstRef = new IdRef(dst); String moveStatus = checkMoveAllowed(srcRef, dstRef); if (moveStatus.equals(MOVE_IMPOSSIBLE)) { return debug; } String action = "document_moved"; if (moveStatus.equals(MOVE_PUBLISH)) { DocumentModel srcDoc = documentManager.getDocument(srcRef); DocumentModel dstDoc = documentManager.getDocument(dstRef); documentManager.publishDocument(srcDoc, dstDoc); action = "document_published"; } else { documentManager.move(srcRef, dstRef, null); } // delCopyWithId(docId); documentManager.save(); DocumentModel currentDocument = navigationContext.getCurrentDocument(); EventManager.raiseEventsOnDocumentChildrenChange(currentDocument); // notify current container Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, currentDocument); // notify the other container DocumentModel otherContainer = documentManager.getDocument(dstRef); Events.instance().raiseEvent(EventNames.DOCUMENT_CHILDREN_CHANGED, otherContainer); facesMessages.add(StatusMessage.Severity.INFO, messages.get(action), messages.get(documentManager.getDocument(srcRef).getType())); return debug; } catch (RecoverableClientException e) { throw e; } catch (NuxeoException e) { throw new RecoverableClientException("Cannot validate, caught exception", "message.operation.fails.generic", null, e); } } @Override @WebRemote public String copyWithId(String docId) { try { String debug = "copying " + docId; log.debug(debug); if (docId.startsWith("docRef:")) { docId = docId.split("docRef:")[1]; } if (docId.startsWith("docClipboardRef:")) { docId = docId.split("docClipboardRef:")[1]; } DocumentRef srcRef = new IdRef(docId); DocumentModel srcDoc = documentManager.getDocument(srcRef); List<DocumentModel> docsToAdd = new ArrayList<DocumentModel>(); docsToAdd.add(srcDoc); clipboardActions.putSelectionInWorkList(docsToAdd, Boolean.TRUE); return debug; } catch (RecoverableClientException e) { throw e; } catch (NuxeoException e) { throw new RecoverableClientException("Cannot validate, caught exception", "message.operation.fails.generic", null, e); } } @Override @WebRemote public String pasteWithId(String docId) { try { String debug = "pasting " + docId; log.debug(debug); if (docId.startsWith("pasteRef_")) { docId = docId.split("pasteRef_")[1]; } if (docId.startsWith("docClipboardRef:")) { docId = docId.split("docClipboardRef:")[1]; } DocumentRef srcRef = new IdRef(docId); DocumentModel srcDoc = documentManager.getDocument(srcRef); List<DocumentModel> pasteDocs = new ArrayList<DocumentModel>(); pasteDocs.add(srcDoc); clipboardActions.pasteDocumentList(pasteDocs); return debug; } catch (RecoverableClientException e) { throw e; } catch (NuxeoException e) { throw new RecoverableClientException("Cannot validate, caught exception", "message.operation.fails.generic", null, e); } } public void processUpload(FileUploadEvent uploadEvent) { try { if (fileUploadHolder != null) { FileBlob blob = getBlob(uploadEvent); tmpFilePaths.add(blob.getFile().getPath()); fileUploadHolder.getUploadedFiles().add(new NxUploadedFile(blob)); } else { log.error("Unable to reach fileUploadHolder"); } } catch (IOException e) { log.error(e, e); } } protected static String getJSFUploadTmpDirPath() { String jstTmpFileDir = Framework.getProperty(NUXEO_JSF_TMP_DIR_PROP); if (StringUtils.isBlank(jstTmpFileDir)) { jstTmpFileDir = null; } return jstTmpFileDir; } public static FileBlob getBlob(FileUploadEvent uploadEvent) throws IOException { // copy to a temporary file we own // TODO check how we can reuse RichFaces' temporary file String jstTmpFileDir = getJSFUploadTmpDirPath(); File tmpDir = null; if (jstTmpFileDir != null) { tmpDir = new File(jstTmpFileDir); } UploadedFile uploadedFile = uploadEvent.getUploadedFile(); try (InputStream in = uploadedFile.getInputStream()) { FileBlob blob = new FileBlob(in, uploadedFile.getContentType(), null, tmpDir); // NXP-21171: With Firefox 50 and its new File system API, a bug occurs with the filename containing // a slash. As it is not supposed to happen, sanitize the filename. blob.setFilename(FileUtils.getCleanFileName(uploadedFile.getName())); return blob; } } public void validateMultiplesUpload() throws FileNotFoundException, IOException { DocumentModel current = navigationContext.getCurrentDocument(); validateMultipleUploadForDocument(current); } @SuppressWarnings({ "unchecked", "rawtypes" }) public void validateMultipleUploadForDocument(DocumentModel current) throws FileNotFoundException, IOException { if (!current.hasSchema(FILES_SCHEMA)) { return; } Collection<NxUploadedFile> nxuploadFiles = getUploadedFiles(); try { ArrayList files = (ArrayList) current.getPropertyValue(FILES_PROPERTY); if (nxuploadFiles != null) { for (NxUploadedFile uploadItem : nxuploadFiles) { Blob blob = uploadItem.getBlob(); FileUtils.configureFileBlob(blob); HashMap<String, Object> fileMap = new HashMap<String, Object>(1); fileMap.put("file", blob); if (!files.contains(fileMap)) { files.add(fileMap); } } } current.setPropertyValue(FILES_PROPERTY, files); documentActions.updateDocument(current, Boolean.TRUE); } finally { if (nxuploadFiles != null) { for (NxUploadedFile uploadItem : nxuploadFiles) { File tempFile = uploadItem.getFile(); // Tmp file that have been moved are assumed to not be temporary anymore if (tempFile != null && tempFile.exists() && tmpFilePaths.contains(tempFile.getPath())) { Framework.trackFile(tempFile, tempFile); } } } tmpFilePaths.clear(); } } @SuppressWarnings({ "rawtypes" }) public void performAction(ActionEvent event) { FacesContext context = FacesContext.getCurrentInstance(); ExternalContext eContext = context.getExternalContext(); String index = eContext.getRequestParameterMap().get("index"); try { DocumentModel current = navigationContext.getCurrentDocument(); if (!current.hasSchema(FILES_SCHEMA)) { return; } ArrayList files = (ArrayList) current.getPropertyValue(FILES_PROPERTY); Object file = CollectionUtils.get(files, Integer.valueOf(index).intValue()); files.remove(file); current.setPropertyValue(FILES_PROPERTY, files); documentActions.updateDocument(current, Boolean.TRUE); } catch (IndexOutOfBoundsException | NuxeoException e) { log.error(e, e); throw e; } } public String validate() { NxUploadedFile uploadedFile; if (fileUploadHolder == null || fileUploadHolder.getUploadedFiles().isEmpty() || (uploadedFile = fileUploadHolder.getUploadedFiles().iterator().next()) == null) { facesMessages.add(StatusMessage.Severity.ERROR, messages.get("fileImporter.error.nullUploadedFile")); return null; } try { return addFile(); } catch (RecoverableClientException e) { throw e; } catch (NuxeoException e) { throw new RecoverableClientException("Cannot validate, caught exception", "message.operation.fails.generic", null, e); } finally { if (uploadedFile != null && uploadedFile.getFile().exists()) { Framework.trackFile(uploadedFile.getFile(), uploadedFile.getFile()); } } } @Override public InputStream getFileUpload() { if (fileUploadHolder != null) { return fileUploadHolder.getFileUpload(); } else { return null; } } @Override public void setFileUpload(InputStream fileUpload) { if (fileUploadHolder != null) { fileUploadHolder.setFileUpload(fileUpload); } } @Override public String getFileName() { if (fileUploadHolder != null) { return fileUploadHolder.getFileName(); } return null; } @Override public void setFileName(String fileName) { if (fileUploadHolder != null) { fileUploadHolder.setFileName(fileName); } } public DocumentModel getChangeableDocument() { return navigationContext.getChangeableDocument(); } public void setChangeableDocument(DocumentModel changeableDocument) { navigationContext.setChangeableDocument(changeableDocument); } public Collection<NxUploadedFile> getUploadedFiles() { if (fileUploadHolder != null) { return fileUploadHolder.getUploadedFiles(); } else { return null; } } public void setUploadedFiles(Collection<NxUploadedFile> uploadedFiles) { if (fileUploadHolder != null) { fileUploadHolder.setUploadedFiles(uploadedFiles); } tmpFilePaths.clear(); if (uploadedFiles != null) { for (NxUploadedFile file : uploadedFiles) { tmpFilePaths.add(file.getFile().getPath()); } } } @Override @WebRemote public String removeSingleUploadedFile() { return removeAllUploadedFile(); } @Override public void setFileToRemove(String fileToRemove) { this.fileToRemove = fileToRemove; } @Override public String removeOneOrAllUploadedFiles(ActionEvent action) { if (StringUtils.isBlank(fileToRemove)) { return removeAllUploadedFile(); } else { return removeUploadedFile(fileToRemove); } } @Override @WebRemote public String removeAllUploadedFile() { if (fileUploadHolder != null) { Collection<NxUploadedFile> files = getUploadedFiles(); if (files != null) { for (NxUploadedFile item : files) { item.getFile().delete(); } } setUploadedFiles(new ArrayList<NxUploadedFile>()); } return ""; } @Override @WebRemote public String removeUploadedFile(String fileName) { NxUploadedFile fileToDelete = null; // Retrieve only the real filename // IE stores the full path of the file as the filename (ie. // Z:\\path\\to\\file) fileName = FilenameUtils.getName(fileName); Collection<NxUploadedFile> files = getUploadedFiles(); if (files != null) { for (NxUploadedFile file : files) { String uploadedFileName = file.getName(); if (fileName.equals(uploadedFileName)) { fileToDelete = file; break; } } } if (fileToDelete != null) { fileToDelete.getFile().delete(); files.remove(fileToDelete); setUploadedFiles(files); } return ""; } }