/*
* LinShare is an open source filesharing software, part of the LinPKI software
* suite, developed by Linagora.
*
* Copyright (C) 2015-2016 LINAGORA
*
* 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, provided you comply with the Additional Terms applicable for
* LinShare software by Linagora pursuant to Section 7 of the GNU Affero General
* Public License, subsections (b), (c), and (e), pursuant to which you must
* notably (i) retain the display of the “LinShare™” trademark/logo at the top
* of the interface window, the display of the “You are using the Open Source
* and free version of LinShare™, powered by Linagora © 2009–2016. Contribute to
* Linshare R&D by subscribing to an Enterprise offer!” infobox and in the
* e-mails sent with the Program, (ii) retain all hypertext links between
* LinShare and linshare.org, between linagora.com and Linagora, and (iii)
* refrain from infringing Linagora intellectual property rights over its
* trademarks and commercial brands. Other Additional Terms apply, see
* <http://www.linagora.com/licenses/> for more details.
*
* 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 and
* its applicable Additional Terms for LinShare along with this program. If not,
* see <http://www.gnu.org/licenses/> for the GNU Affero General Public License
* version 3 and <http://www.linagora.com/licenses/> for the Additional Terms
* applicable to LinShare software.
*/
package org.linagora.linshare.core.service.impl;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import org.apache.commons.lang.Validate;
import org.linagora.linshare.core.business.service.DocumentEntryBusinessService;
import org.linagora.linshare.core.business.service.OperationHistoryBusinessService;
import org.linagora.linshare.core.dao.MimeTypeMagicNumberDao;
import org.linagora.linshare.core.domain.constants.ContainerQuotaType;
import org.linagora.linshare.core.domain.constants.LogAction;
import org.linagora.linshare.core.domain.constants.OperationHistoryTypeEnum;
import org.linagora.linshare.core.domain.entities.AbstractDomain;
import org.linagora.linshare.core.domain.entities.Account;
import org.linagora.linshare.core.domain.entities.AntivirusLogEntry;
import org.linagora.linshare.core.domain.entities.DocumentEntry;
import org.linagora.linshare.core.domain.entities.Functionality;
import org.linagora.linshare.core.domain.entities.LogEntry;
import org.linagora.linshare.core.domain.entities.OperationHistory;
import org.linagora.linshare.core.domain.entities.StringValueFunctionality;
import org.linagora.linshare.core.domain.entities.SystemAccount;
import org.linagora.linshare.core.domain.entities.Thread;
import org.linagora.linshare.core.domain.entities.ThreadEntry;
import org.linagora.linshare.core.domain.entities.ThreadLogEntry;
import org.linagora.linshare.core.domain.entities.ThreadMember;
import org.linagora.linshare.core.domain.entities.User;
import org.linagora.linshare.core.exception.BusinessErrorCode;
import org.linagora.linshare.core.exception.BusinessException;
import org.linagora.linshare.core.exception.TechnicalErrorCode;
import org.linagora.linshare.core.exception.TechnicalException;
import org.linagora.linshare.core.rac.ThreadEntryResourceAccessControl;
import org.linagora.linshare.core.repository.ThreadMemberRepository;
import org.linagora.linshare.core.service.AntiSamyService;
import org.linagora.linshare.core.service.FunctionalityReadOnlyService;
import org.linagora.linshare.core.service.LogEntryService;
import org.linagora.linshare.core.service.MimeTypeService;
import org.linagora.linshare.core.service.QuotaService;
import org.linagora.linshare.core.service.ThreadEntryService;
import org.linagora.linshare.core.service.VirusScannerService;
public class ThreadEntryServiceImpl extends GenericEntryServiceImpl<Account, ThreadEntry>
implements ThreadEntryService {
private final DocumentEntryBusinessService documentEntryBusinessService;
private final LogEntryService logEntryService;
private final FunctionalityReadOnlyService functionalityReadOnlyService;
private final MimeTypeService mimeTypeService;
private final VirusScannerService virusScannerService;
private final ThreadMemberRepository threadMemberRepository;
private final MimeTypeMagicNumberDao mimeTypeIdentifier;
private final AntiSamyService antiSamyService;
private final OperationHistoryBusinessService operationHistoryBusinessService;
private final QuotaService quotaService;
public ThreadEntryServiceImpl(DocumentEntryBusinessService documentEntryBusinessService,
LogEntryService logEntryService, FunctionalityReadOnlyService functionalityReadOnlyService,
MimeTypeService mimeTypeService, VirusScannerService virusScannerService,
ThreadMemberRepository threadMemberRepository, MimeTypeMagicNumberDao mimeTypeIdentifier,
AntiSamyService antiSamyService, ThreadEntryResourceAccessControl rac,
OperationHistoryBusinessService operationHistoryBusinessService, QuotaService quotaService) {
super(rac);
this.documentEntryBusinessService = documentEntryBusinessService;
this.logEntryService = logEntryService;
this.functionalityReadOnlyService = functionalityReadOnlyService;
this.mimeTypeService = mimeTypeService;
this.virusScannerService = virusScannerService;
this.threadMemberRepository = threadMemberRepository;
this.mimeTypeIdentifier = mimeTypeIdentifier;
this.antiSamyService = antiSamyService;
this.operationHistoryBusinessService = operationHistoryBusinessService;
this.quotaService = quotaService;
}
@Override
public ThreadEntry createThreadEntry(Account actor, Account owner, Thread thread, File tempFile, String filename)
throws BusinessException {
checkCreatePermission(actor, owner, ThreadEntry.class, BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, null, thread);
filename = sanitizeFileName(filename); // throws
Long size = tempFile.length();
ThreadEntry threadEntry = null;
try {
String mimeType = mimeTypeIdentifier.getMimeType(tempFile);
AbstractDomain domain = owner.getDomain();
checkSpace(thread, size);
// check if the file MimeType is allowed
Functionality mimeFunctionality = functionalityReadOnlyService.getMimeTypeFunctionality(domain);
if (mimeFunctionality.getActivationPolicy().getStatus()) {
mimeTypeService.checkFileMimeType(owner, filename, mimeType);
}
Functionality antivirusFunctionality = functionalityReadOnlyService.getAntivirusFunctionality(domain);
if (antivirusFunctionality.getActivationPolicy().getStatus()) {
checkVirus(filename, owner, tempFile);
}
// want a timestamp on doc ?
String timeStampingUrl = null;
StringValueFunctionality timeStampingFunctionality = functionalityReadOnlyService
.getTimeStampingFunctionality(domain);
if (timeStampingFunctionality.getActivationPolicy().getStatus()) {
timeStampingUrl = timeStampingFunctionality.getValue();
}
Functionality enciphermentFunctionality = functionalityReadOnlyService.getEnciphermentFunctionality(domain);
Boolean checkIfIsCiphered = enciphermentFunctionality.getActivationPolicy().getStatus();
threadEntry = documentEntryBusinessService.createThreadEntry(thread, tempFile, size, filename,
checkIfIsCiphered, timeStampingUrl, mimeType);
logEntryService.create(new ThreadLogEntry(owner, threadEntry, LogAction.THREAD_UPLOAD_ENTRY,
"Uploading a file in a thread."));
addToQuota(thread, size);
} finally {
try {
logger.debug("deleting temp file : " + tempFile.getName());
tempFile.delete(); // remove the temporary file
} catch (Exception e) {
logger.error("can not delete temp file : " + e.getMessage());
}
}
return threadEntry;
}
@Override
public ThreadEntry copyFromDocumentEntry(Account actor, Account member,
Thread thread, DocumentEntry documentEntry)
throws BusinessException {
checkCreatePermission(actor, member, ThreadEntry.class,
BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, null, thread);
checkSpace(thread, documentEntry.getSize());
AbstractDomain domain = member.getDomain();
// check if the file MimeType is allowed
Functionality mimeFunctionality = functionalityReadOnlyService.getMimeTypeFunctionality(domain);
if (mimeFunctionality.getActivationPolicy().getStatus()) {
mimeTypeService.checkFileMimeType(member, documentEntry.getName(), documentEntry.getType());
}
ThreadEntry threadEntry = documentEntryBusinessService.copyFromDocumentEntry(thread, documentEntry);
logEntryService.create(new ThreadLogEntry(member, threadEntry, LogAction.THREAD_UPLOAD_ENTRY,
"Uploading a file in a thread."));
addToQuota(thread, documentEntry.getSize());
return threadEntry;
}
public DocumentEntry copyFromThreadEntry(Account actor, Account member, Thread thread, ThreadEntry threadEntry) {
checkCreatePermission(actor, member, ThreadEntry.class,
BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, null, thread);
checkSpace(member, threadEntry.getSize());
AbstractDomain domain = member.getDomain();
// check if the file MimeType is allowed
Functionality mimeFunctionality = functionalityReadOnlyService.getMimeTypeFunctionality(domain);
if (mimeFunctionality.getActivationPolicy().getStatus()) {
mimeTypeService.checkFileMimeType(member, threadEntry.getName(), threadEntry.getType());
}
DocumentEntry documentEntry = documentEntryBusinessService
.copyFromThreadEntry(member, threadEntry,
threadEntry.getExpirationDate());
addToQuota(member, documentEntry.getSize());
return documentEntry;
}
@Override
public ThreadEntry find(Account actor, Account owner, String threadEntryUuid) throws BusinessException {
ThreadEntry threadEntry = documentEntryBusinessService
.findThreadEntryById(threadEntryUuid);
if (threadEntry == null) {
throw new BusinessException(BusinessErrorCode.THREAD_ENTRY_NOT_FOUND,
"Thread entry with uuid : " + threadEntryUuid + " not found.");
}
checkReadPermission(actor, owner, ThreadEntry.class, BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, threadEntry);
return threadEntry;
}
@Override
public void deleteThreadEntry(Account actor, Account owner, ThreadEntry threadEntry) throws BusinessException {
Thread thread = (Thread) threadEntry.getEntryOwner();
try {
checkDeletePermission(actor, owner, ThreadEntry.class, BusinessErrorCode.THREAD_ENTRY_FORBIDDEN,
threadEntry, thread);
ThreadLogEntry log = new ThreadLogEntry(owner, threadEntry, LogAction.THREAD_REMOVE_ENTRY,
"Deleting a thread entry.");
documentEntryBusinessService.deleteThreadEntry(threadEntry);
logEntryService.create(log);
delFromQuota(thread, threadEntry.getSize());
} catch (IllegalArgumentException e) {
logger.error("Could not delete thread entry " + threadEntry.getUuid() + " in thread " + thread.getLsUuid()
+ " by account " + owner.getLsUuid() + ", reason : ", e);
throw new TechnicalException(TechnicalErrorCode.COULD_NOT_DELETE_DOCUMENT, "Could not delete document");
}
}
@Override
public void deleteInconsistentThreadEntry(SystemAccount actor, ThreadEntry threadEntry) throws BusinessException {
Thread thread = (Thread) threadEntry.getEntryOwner();
try {
ThreadLogEntry log = new ThreadLogEntry(actor, threadEntry, LogAction.THREAD_REMOVE_INCONSISTENCY_ENTRY,
"Deleting an inconsistent thread entry.");
logEntryService.create(LogEntryService.WARN, log);
documentEntryBusinessService.deleteThreadEntry(threadEntry);
delFromQuota(thread, threadEntry.getSize());
} catch (IllegalArgumentException e) {
logger.error("Could not delete thread entry " + threadEntry.getUuid() + " in thread " + thread.getLsUuid()
+ " by account " + actor.getLsUuid() + ", reason : ", e);
throw new TechnicalException(TechnicalErrorCode.COULD_NOT_DELETE_DOCUMENT, "Could not delete document");
}
}
@Override
public List<ThreadEntry> findAllThreadEntries(Account actor, Account owner, Thread thread) throws BusinessException {
checkListPermission(actor, owner, ThreadEntry.class,
BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, null, thread);
checkListPermission(actor, owner, ThreadEntry.class, BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, null, thread);
return documentEntryBusinessService.findAllThreadEntries(thread);
}
@Override
public InputStream getDocumentStream(Account actor, Account owner, String uuid) throws BusinessException {
ThreadEntry threadEntry = find(actor, owner, uuid);
checkDownloadPermission(actor, owner, ThreadEntry.class,
BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, threadEntry);
logEntryService.create(new ThreadLogEntry(actor, threadEntry, LogAction.THREAD_DOWNLOAD_ENTRY, "Downloading a file in a thread."));
return documentEntryBusinessService.getDocumentStream(threadEntry);
}
@Override
public InputStream getDocumentThumbnailStream(Account actor, Account owner, String uuid) throws BusinessException {
ThreadEntry threadEntry = find(actor, owner, uuid);
checkThumbNailDownloadPermission(actor, owner, ThreadEntry.class,
BusinessErrorCode.THREAD_ENTRY_FORBIDDEN, threadEntry);
return documentEntryBusinessService.getThreadEntryThumbnailStream(threadEntry);
}
@Override
public boolean documentHasThumbnail(Account actor, String uuid) {
ThreadEntry threadEntry = documentEntryBusinessService.findThreadEntryById(uuid);
if (threadEntry == null) {
logger.error("Can't find document entry, are you sure it is not a share ? : " + uuid);
return false;
}
if (!this.isThreadMember((Thread) threadEntry.getEntryOwner(), (User) actor)) {
return false;
}
String thmbUUID = threadEntry.getDocument().getThmbUuid();
return (thmbUUID != null && thmbUUID.length() > 0);
}
@Override
public ThreadEntry updateFileProperties(Account actor, Account owner,
String threadEntryUuid, String fileComment, String metaData,
String newName) throws BusinessException {
ThreadEntry threadEntry = documentEntryBusinessService.findThreadEntryById(threadEntryUuid);
// Avoid overwritting metadata in database to null when update
// threadEntry from interface.
if (metaData == null) {
metaData = threadEntry.getMetaData();
}
if (newName == null) {
newName = threadEntry.getName();
}
if (fileComment == null) {
fileComment = threadEntry.getComment();
}
if (!this.canUpload((Thread) threadEntry.getEntryOwner(), (User) owner)) {
throw new BusinessException(BusinessErrorCode.FORBIDDEN,
"You are not authorized to update this document.");
}
return documentEntryBusinessService.updateFileProperties(threadEntry, fileComment, metaData,
sanitizeFileName(newName));
}
private String sanitizeFileName(String fileName) throws BusinessException {
fileName = fileName.replace("\\", "_");
fileName = fileName.replace(":", "_");
fileName = antiSamyService.clean(fileName);
if (fileName.isEmpty()) {
throw new BusinessException(BusinessErrorCode.INVALID_FILENAME, "fileName is empty after the xss filter");
}
return fileName;
}
private Boolean checkVirus(String fileName, Account actor, File file) throws BusinessException {
if (logger.isDebugEnabled()) {
logger.debug("antivirus activation:" + !virusScannerService.isDisabled());
}
boolean checkStatus = false;
try {
checkStatus = virusScannerService.check(file);
} catch (TechnicalException e) {
LogEntry logEntry = new AntivirusLogEntry(actor, LogAction.ANTIVIRUS_SCAN_FAILED, e.getMessage());
logger.error("File scan failed: antivirus enabled but not available ?");
logEntryService.create(LogEntryService.ERROR, logEntry);
throw new BusinessException(BusinessErrorCode.FILE_SCAN_FAILED, "File scan failed", e);
}
if (logger.isDebugEnabled()) {
logger.debug("antivirus scan result : " + checkStatus);
}
// check if the file contains virus
if (!checkStatus) {
LogEntry logEntry = new AntivirusLogEntry(actor, LogAction.FILE_WITH_VIRUS, fileName);
logEntryService.create(LogEntryService.WARN, logEntry);
logger.warn(actor.getLsUuid() + " tried to upload a file containing virus:" + fileName);
String[] extras = { fileName };
throw new BusinessException(BusinessErrorCode.FILE_CONTAINS_VIRUS, "File contains virus", extras);
}
return checkStatus;
}
protected void checkSpace(Thread thread, long size) throws BusinessException {
quotaService.checkIfUserCanAddFile(thread, size, ContainerQuotaType.WORK_GROUP);
}
protected void checkSpace(Account owner, long size) throws BusinessException {
quotaService.checkIfUserCanAddFile(owner, size, ContainerQuotaType.USER);
}
protected void addToQuota(Thread thread, Long size) {
OperationHistory oh = new OperationHistory(thread, thread.getDomain(), size, OperationHistoryTypeEnum.CREATE,
ContainerQuotaType.WORK_GROUP);
operationHistoryBusinessService.create(oh);
}
protected void addToQuota(Account owner, Long size) {
OperationHistory oh = new OperationHistory(owner, owner.getDomain(), size, OperationHistoryTypeEnum.CREATE,
ContainerQuotaType.USER);
operationHistoryBusinessService.create(oh);
}
protected void delFromQuota(Thread thread, Long size) {
OperationHistory oh = new OperationHistory(thread, thread.getDomain(), size, OperationHistoryTypeEnum.DELETE,
ContainerQuotaType.WORK_GROUP);
operationHistoryBusinessService.create(oh);
}
/**
* PERMISSIONS
*/
private boolean isThreadMember(Thread thread, User user) {
ThreadMember threadMember = threadMemberRepository.findUserThreadMember(thread, user);
return threadMember != null;
}
private boolean canUpload(Thread thread, User user) {
ThreadMember threadMember = threadMemberRepository.findUserThreadMember(thread, user);
return threadMember.getCanUpload();
}
@Override
public List<ThreadEntry> findMoreRecentByName(Account actor, Thread thread) throws BusinessException {
Validate.notNull(actor, "Actor must be set.");
Validate.notNull(thread, "Thread must be set.");
if (!isThreadMember(thread, (User) actor)) {
throw new BusinessException(BusinessErrorCode.THREAD_ENTRY_FORBIDDEN,
"The actor is not member of the thread.");
}
return documentEntryBusinessService.findMoreRecentByName(thread);
}
}