/* * * (c) Copyright Ascensio System Limited 2010-2018 * * This program is freeware. You can redistribute it and/or modify it under the terms of the GNU * General Public License (GPL) version 3 as published by the Free Software Foundation (https://www.gnu.org/copyleft/gpl.html). * In accordance with Section 7(a) of the GNU GPL its Section 15 shall be amended to the effect that * Ascensio System SIA expressly excludes the warranty of non-infringement of any third-party rights. * * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY; WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR * FITNESS FOR A PARTICULAR PURPOSE. For more details, see GNU GPL at https://www.gnu.org/copyleft/gpl.html * * You can contact Ascensio System SIA by email at sales@onlyoffice.com * * The interactive user interfaces in modified source and object code versions of ONLYOFFICE must display * Appropriate Legal Notices, as required under Section 5 of the GNU GPL version 3. * * Pursuant to Section 7 § 3(b) of the GNU GPL you must retain the original ONLYOFFICE logo which contains * relevant author attributions when distributing the software. If the display of the logo in its graphic * form is not reasonably feasible for technical reasons, you must include the words "Powered by ONLYOFFICE" * in every copy of the program you distribute. * Pursuant to Section 7 § 3(e) we decline to grant you any rights under trademark law for use of our trademarks. * */ using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Runtime.Serialization; using System.Security; using System.Threading; using System.Web; using ASC.Common; using ASC.Common.Caching; using ASC.Common.Logging; using ASC.Common.Security.Authentication; using ASC.Core; using ASC.Files.Core; using ASC.Files.Core.Data; using ASC.Files.Core.Security; using ASC.Files.Resources; using ASC.MessagingSystem; using ASC.Web.Core.Files; using ASC.Web.Files.Classes; using ASC.Web.Files.Core; using ASC.Web.Files.Helpers; using ASC.Web.Files.Services.DocumentService; using ASC.Web.Files.Services.WCFService.FileOperations; using ASC.Web.Studio.Core; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using File = ASC.Files.Core.File; using SecurityContext = ASC.Core.SecurityContext; namespace ASC.Web.Files.Utils { public class FileConverter { private static readonly object locker = new object(); private static readonly IDictionary conversionQueue = new Dictionary(new FileComparer()); private readonly ICache cache; private static Timer timer; private static readonly object singleThread = new object(); private const int TIMER_PERIOD = 500; public FileUtility FileUtility { get; } public FilesLinkUtility FilesLinkUtility { get; } public IDaoFactory DaoFactory { get; } public SetupInfo SetupInfo { get; } public PathProvider PathProvider { get; } public FileSecurity FileSecurity { get; } public FileMarker FileMarker { get; } public TenantManager TenantManager { get; } public AuthContext AuthContext { get; } public EntryManager EntryManager { get; } public FilesSettingsHelper FilesSettingsHelper { get; } public GlobalFolderHelper GlobalFolderHelper { get; } public FilesMessageService FilesMessageService { get; } public FileShareLink FileShareLink { get; } public DocumentServiceHelper DocumentServiceHelper { get; } public DocumentServiceConnector DocumentServiceConnector { get; } public IServiceProvider ServiceProvider { get; } public IHttpContextAccessor HttpContextAccesor { get; } public ILog Logger { get; } public FileConverter( FileUtility fileUtility, FilesLinkUtility filesLinkUtility, IDaoFactory daoFactory, SetupInfo setupInfo, PathProvider pathProvider, FileSecurity fileSecurity, FileMarker fileMarker, TenantManager tenantManager, AuthContext authContext, EntryManager entryManager, IOptionsMonitor options, FilesSettingsHelper filesSettingsHelper, GlobalFolderHelper globalFolderHelper, FilesMessageService filesMessageService, FileShareLink fileShareLink, DocumentServiceHelper documentServiceHelper, DocumentServiceConnector documentServiceConnector, IServiceProvider serviceProvider) { FileUtility = fileUtility; FilesLinkUtility = filesLinkUtility; DaoFactory = daoFactory; SetupInfo = setupInfo; PathProvider = pathProvider; FileSecurity = fileSecurity; FileMarker = fileMarker; TenantManager = tenantManager; AuthContext = authContext; EntryManager = entryManager; FilesSettingsHelper = filesSettingsHelper; GlobalFolderHelper = globalFolderHelper; FilesMessageService = filesMessageService; FileShareLink = fileShareLink; DocumentServiceHelper = documentServiceHelper; DocumentServiceConnector = documentServiceConnector; ServiceProvider = serviceProvider; Logger = options.CurrentValue; cache = AscCache.Memory; } public FileConverter( FileUtility fileUtility, FilesLinkUtility filesLinkUtility, IDaoFactory daoFactory, SetupInfo setupInfo, PathProvider pathProvider, FileSecurity fileSecurity, FileMarker fileMarker, TenantManager tenantManager, AuthContext authContext, EntryManager entryManager, IOptionsMonitor options, FilesSettingsHelper filesSettingsHelper, GlobalFolderHelper globalFolderHelper, FilesMessageService filesMessageService, FileShareLink fileShareLink, DocumentServiceHelper documentServiceHelper, DocumentServiceConnector documentServiceConnector, IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccesor) : this(fileUtility, filesLinkUtility, daoFactory, setupInfo, pathProvider, fileSecurity, fileMarker, tenantManager, authContext, entryManager, options, filesSettingsHelper, globalFolderHelper, filesMessageService, fileShareLink, documentServiceHelper, documentServiceConnector, serviceProvider) { HttpContextAccesor = httpContextAccesor; } public bool EnableAsUploaded { get { return FileUtility.ExtsMustConvert.Any() && !string.IsNullOrEmpty(FilesLinkUtility.DocServiceConverterUrl); } } public bool MustConvert(File file) { if (file == null) return false; var ext = FileUtility.GetFileExtension(file.Title); return FileUtility.ExtsMustConvert.Contains(ext); } public bool EnableConvert(File file, string toExtension) { if (file == null || string.IsNullOrEmpty(toExtension)) { return false; } if (file.Encrypted) { return false; } var fileExtension = file.ConvertedExtension; if (fileExtension.Trim('.').Equals(toExtension.Trim('.'), StringComparison.OrdinalIgnoreCase)) { return false; } fileExtension = FileUtility.GetFileExtension(file.Title); if (FileUtility.InternalExtension.Values.Contains(toExtension)) { return true; } return FileUtility.ExtsConvertible.Keys.Contains(fileExtension) && FileUtility.ExtsConvertible[fileExtension].Contains(toExtension); } public Stream Exec(File file) { return Exec(file, FileUtility.GetInternalExtension(file.Title)); } public Stream Exec(File file, string toExtension) { if (!EnableConvert(file, toExtension)) { var fileDao = DaoFactory.FileDao; return fileDao.GetFileStream(file); } if (file.ContentLength > SetupInfo.AvailableFileSize) { throw new Exception(string.Format(FilesCommonResource.ErrorMassage_FileSizeConvert, FileSizeComment.FilesSizeToString(SetupInfo.AvailableFileSize))); } var fileUri = PathProvider.GetFileStreamUrl(file); var docKey = DocumentServiceHelper.GetDocKey(file); fileUri = DocumentServiceConnector.ReplaceCommunityAdress(fileUri); DocumentServiceConnector.GetConvertedUri(fileUri, file.ConvertedExtension, toExtension, docKey, null, false, out var convertUri); if (WorkContext.IsMono && ServicePointManager.ServerCertificateValidationCallback == null) { ServicePointManager.ServerCertificateValidationCallback += (s, c, n, p) => true; //HACK: http://ubuntuforums.org/showthread.php?t=1841740 } return new ResponseStream(((HttpWebRequest)WebRequest.Create(convertUri)).GetResponse()); } public File ExecSync(File file, string doc) { var fileDao = DaoFactory.FileDao; var fileSecurity = FileSecurity; if (!fileSecurity.CanRead(file)) { var readLink = FileShareLink.Check(doc, true, fileDao, out file); if (file == null) { throw new ArgumentNullException("file", FilesCommonResource.ErrorMassage_FileNotFound); } if (!readLink) { throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException_ReadFile); } } var fileUri = PathProvider.GetFileStreamUrl(file); var fileExtension = file.ConvertedExtension; var toExtension = FileUtility.GetInternalExtension(file.Title); var docKey = DocumentServiceHelper.GetDocKey(file); fileUri = DocumentServiceConnector.ReplaceCommunityAdress(fileUri); DocumentServiceConnector.GetConvertedUri(fileUri, fileExtension, toExtension, docKey, null, false, out var convertUri); return SaveConvertedFile(file, convertUri); } public void ExecAsync(File file, bool deleteAfter, string password = null) { if (!MustConvert(file)) { throw new ArgumentException(FilesCommonResource.ErrorMassage_NotSupportedFormat); } if (!string.IsNullOrEmpty(file.ConvertedType) || FileUtility.InternalExtension.Values.Contains(FileUtility.GetFileExtension(file.Title))) { return; } FileMarker.RemoveMarkAsNew(file); lock (locker) { if (conversionQueue.ContainsKey(file)) { return; } var queueResult = new ConvertFileOperationResult { Source = string.Format("{{\"id\":\"{0}\", \"version\":\"{1}\"}}", file.ID, file.Version), OperationType = FileOperationType.Convert, Error = string.Empty, Progress = 0, Result = string.Empty, Processed = "", Id = string.Empty, TenantId = TenantManager.GetCurrentTenant().TenantId, Account = AuthContext.CurrentAccount, Delete = deleteAfter, StartDateTime = DateTime.Now, Url = HttpContextAccesor?.HttpContext != null ? HttpContextAccesor.HttpContext.Request.GetUrlRewriter().ToString() : null, Password = password }; conversionQueue.Add(file, queueResult); cache.Insert(GetKey(file), queueResult, TimeSpan.FromMinutes(10)); if (timer == null) { timer = new Timer(CheckConvertFilesStatus, null, 0, Timeout.Infinite); } else { timer.Change(0, Timeout.Infinite); } } } public bool IsConverting(File file) { if (!MustConvert(file) || !string.IsNullOrEmpty(file.ConvertedType)) { return false; } var result = cache.Get(GetKey(file)); return result != null && result.Progress != 100 && string.IsNullOrEmpty(result.Error); } public IEnumerable GetStatus(IEnumerable> filesPair) { var fileSecurity = FileSecurity; var result = new List(); foreach (var pair in filesPair) { var file = pair.Key; var key = GetKey(file); var operation = cache.Get(key); if (operation != null && (pair.Value || fileSecurity.CanRead(file))) { result.Add(operation); lock (locker) { if (operation.Progress == 100) { conversionQueue.Remove(file); cache.Remove(key); } } } } return result; } private string FileJsonSerializer(File file, string folderTitle) { if (file == null) return string.Empty; EntryManager.SetFileStatus(file); return string.Format("{{ \"id\": \"{0}\"," + " \"title\": \"{1}\"," + " \"version\": \"{2}\"," + " \"folderId\": \"{3}\"," + " \"folderTitle\": \"{4}\"," + " \"fileXml\": \"{5}\" }}", file.ID, file.Title, file.Version, file.FolderID, folderTitle ?? "", File.Serialize(file).Replace('"', '\'')); } private void CheckConvertFilesStatus(object _) { if (Monitor.TryEnter(singleThread)) { using var scope = ServiceProvider.CreateScope(); var logger = scope.ServiceProvider.GetService>().CurrentValue; var tenantManager = scope.ServiceProvider.GetService(); var userManager = scope.ServiceProvider.GetService(); var securityContext = scope.ServiceProvider.GetService(); var daoFactory = scope.ServiceProvider.GetService(); try { List filesIsConverting; lock (locker) { timer.Change(Timeout.Infinite, Timeout.Infinite); conversionQueue.Where(x => !string.IsNullOrEmpty(x.Value.Processed) && (x.Value.Progress == 100 && DateTime.UtcNow - x.Value.StopDateTime > TimeSpan.FromMinutes(1) || DateTime.UtcNow - x.Value.StopDateTime > TimeSpan.FromMinutes(10))) .ToList() .ForEach(x => { conversionQueue.Remove(x); cache.Remove(GetKey(x.Key)); }); logger.DebugFormat("Run CheckConvertFilesStatus: count {0}", conversionQueue.Count); if (conversionQueue.Count == 0) { return; } filesIsConverting = conversionQueue .Where(x => string.IsNullOrEmpty(x.Value.Processed)) .Select(x => x.Key) .ToList(); } var fileSecurity = FileSecurity; foreach (var file in filesIsConverting) { var fileUri = file.ID.ToString(); string convertedFileUrl; int operationResultProgress; try { int tenantId; IAccount account; string password; lock (locker) { if (!conversionQueue.Keys.Contains(file)) continue; var operationResult = conversionQueue[file]; if (!string.IsNullOrEmpty(operationResult.Processed)) continue; operationResult.Processed = "1"; tenantId = operationResult.TenantId; account = operationResult.Account; password = operationResult.Password; //if (HttpContext.Current == null && !WorkContext.IsMono) //{ // HttpContext.Current = new HttpContext( // new HttpRequest("hack", operationResult.Url, string.Empty), // new HttpResponse(new StringWriter())); //} cache.Insert(GetKey(file), operationResult, TimeSpan.FromMinutes(10)); } tenantManager.SetCurrentTenant(tenantId); securityContext.AuthenticateMe(account); var user = userManager.GetUsers(account.ID); var culture = string.IsNullOrEmpty(user.CultureName) ? TenantManager.GetCurrentTenant().GetCulture() : CultureInfo.GetCultureInfo(user.CultureName); Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; if (!fileSecurity.CanRead(file) && file.RootFolderType != FolderType.BUNCH) { //No rights in CRM after upload before attach throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException_ReadFile); } if (file.ContentLength > SetupInfo.AvailableFileSize) { throw new Exception(string.Format(FilesCommonResource.ErrorMassage_FileSizeConvert, FileSizeComment.FilesSizeToString(SetupInfo.AvailableFileSize))); } fileUri = PathProvider.GetFileStreamUrl(file); var toExtension = FileUtility.GetInternalExtension(file.Title); var fileExtension = file.ConvertedExtension; var docKey = DocumentServiceHelper.GetDocKey(file); fileUri = DocumentServiceConnector.ReplaceCommunityAdress(fileUri); operationResultProgress = DocumentServiceConnector.GetConvertedUri(fileUri, fileExtension, toExtension, docKey, password, true, out convertedFileUrl); } catch (Exception exception) { var password = exception.InnerException != null && (exception.InnerException is DocumentService.DocumentServiceException documentServiceException) && documentServiceException.Code == DocumentService.DocumentServiceException.ErrorCode.ConvertPassword; logger.Error(string.Format("Error convert {0} with url {1}", file.ID, fileUri), exception); lock (locker) { if (conversionQueue.Keys.Contains(file)) { var operationResult = conversionQueue[file]; if (operationResult.Delete) { conversionQueue.Remove(file); cache.Remove(GetKey(file)); } else { operationResult.Progress = 100; operationResult.StopDateTime = DateTime.UtcNow; operationResult.Error = exception.Message; if (password) operationResult.Result = "password"; cache.Insert(GetKey(file), operationResult, TimeSpan.FromMinutes(10)); } } } continue; } operationResultProgress = Math.Min(operationResultProgress, 100); if (operationResultProgress < 100) { lock (locker) { if (conversionQueue.Keys.Contains(file)) { var operationResult = conversionQueue[file]; if (DateTime.Now - operationResult.StartDateTime > TimeSpan.FromMinutes(10)) { operationResult.StopDateTime = DateTime.UtcNow; operationResult.Error = FilesCommonResource.ErrorMassage_ConvertTimeout; logger.ErrorFormat("CheckConvertFilesStatus timeout: {0} ({1})", file.ID, file.ContentLengthString); } else { operationResult.Processed = ""; } operationResult.Progress = operationResultProgress; cache.Insert(GetKey(file), operationResult, TimeSpan.FromMinutes(10)); } } logger.Debug("CheckConvertFilesStatus iteration continue"); continue; } File newFile = null; var operationResultError = string.Empty; try { newFile = SaveConvertedFile(file, convertedFileUrl); } catch (Exception e) { operationResultError = e.Message; logger.ErrorFormat("{0} ConvertUrl: {1} fromUrl: {2}: {3}", operationResultError, convertedFileUrl, fileUri, e); continue; } finally { lock (locker) { if (conversionQueue.Keys.Contains(file)) { var operationResult = conversionQueue[file]; if (operationResult.Delete) { conversionQueue.Remove(file); cache.Remove(GetKey(file)); } else { if (newFile != null) { var folderDao = daoFactory.FolderDao; var folder = folderDao.GetFolder(newFile.FolderID); var folderTitle = fileSecurity.CanRead(folder) ? folder.Title : null; operationResult.Result = FileJsonSerializer(newFile, folderTitle); } operationResult.Progress = 100; operationResult.StopDateTime = DateTime.UtcNow; operationResult.Processed = "1"; if (!string.IsNullOrEmpty(operationResultError)) { operationResult.Error = operationResultError; } cache.Insert(GetKey(file), operationResult, TimeSpan.FromMinutes(10)); } } } } logger.Debug("CheckConvertFilesStatus iteration end"); } lock (locker) { timer.Change(TIMER_PERIOD, TIMER_PERIOD); } } catch (Exception exception) { logger.Error(exception.Message, exception); lock (locker) { timer.Change(Timeout.Infinite, Timeout.Infinite); } } finally { Monitor.Exit(singleThread); } } } private File SaveConvertedFile(File file, string convertedFileUrl) { var fileSecurity = FileSecurity; var fileDao = DaoFactory.FileDao; var folderDao = DaoFactory.FolderDao; File newFile = null; var newFileTitle = FileUtility.ReplaceFileExtension(file.Title, FileUtility.GetInternalExtension(file.Title)); if (!FilesSettingsHelper.StoreOriginalFiles && fileSecurity.CanEdit(file)) { newFile = (File)file.Clone(); newFile.Version++; } else { var folderId = GlobalFolderHelper.FolderMy; var parent = folderDao.GetFolder(file.FolderID); if (parent != null && fileSecurity.CanCreate(parent)) { folderId = parent.ID; } if (Equals(folderId, 0)) throw new SecurityException(FilesCommonResource.ErrorMassage_FolderNotFound); if (FilesSettingsHelper.UpdateIfExist && (parent != null && folderId != parent.ID || !file.ProviderEntry)) { newFile = fileDao.GetFile(folderId, newFileTitle); if (newFile != null && fileSecurity.CanEdit(newFile) && !EntryManager.FileLockedForMe(newFile.ID) && !FileTracker.IsEditing(newFile.ID)) { newFile.Version++; } else { newFile = null; } } if (newFile == null) { newFile = ServiceProvider.GetService(); newFile.FolderID = folderId; } } newFile.Title = newFileTitle; newFile.ConvertedType = null; newFile.Comment = string.Format(FilesCommonResource.CommentConvert, file.Title); var req = (HttpWebRequest)WebRequest.Create(convertedFileUrl); if (WorkContext.IsMono && ServicePointManager.ServerCertificateValidationCallback == null) { ServicePointManager.ServerCertificateValidationCallback += (s, c, n, p) => true; //HACK: http://ubuntuforums.org/showthread.php?t=1841740 } try { using (var convertedFileStream = new ResponseStream(req.GetResponse())) { newFile.ContentLength = convertedFileStream.Length; newFile = fileDao.SaveFile(newFile, convertedFileStream); } } catch (WebException e) { using var response = e.Response; var httpResponse = (HttpWebResponse)response; var errorString = string.Format("WebException: {0}", httpResponse.StatusCode); if (httpResponse.StatusCode != HttpStatusCode.NotFound) { using var responseStream = response.GetResponseStream(); if (responseStream != null) { using var readStream = new StreamReader(responseStream); var text = readStream.ReadToEnd(); errorString += string.Format(" Error message: {0}", text); } } throw new Exception(errorString); } FilesMessageService.Send(newFile, MessageInitiator.DocsService, MessageAction.FileConverted, newFile.Title); FileMarker.MarkAsNew(newFile); var tagDao = DaoFactory.TagDao; var tags = tagDao.GetTags(file.ID, FileEntryType.File, TagType.System).ToList(); if (tags.Any()) { tags.ForEach(r => r.EntryId = newFile.ID); tagDao.SaveTags(tags); } return newFile; } private static string GetKey(File f) { return string.Format("fileConvertation-{0}", f.ID); } private class FileComparer : IEqualityComparer { public bool Equals(File x, File y) { return x != null && y != null && Equals(x.ID, y.ID) && x.Version == y.Version; } public int GetHashCode(File obj) { return obj.ID.GetHashCode() + obj.Version.GetHashCode(); } } [DataContract(Name = "operation_result", Namespace = "")] private class ConvertFileOperationResult : FileOperationResult { public DateTime StartDateTime { get; set; } public DateTime StopDateTime { get; set; } public int TenantId { get; set; } public IAccount Account { get; set; } public bool Delete { get; set; } public string Url { get; set; } public string Password { get; set; } } } public static class FileConverterExtension { public static DIHelper AddFileConverterService(this DIHelper services) { services.TryAddScoped(); return services .AddFilesLinkUtilityService() .AddFileUtilityService() .AddDaoFactoryService() .AddSetupInfo() .AddPathProviderService() .AddFileSecurityService() .AddFileMarkerService() .AddTenantManagerService() .AddAuthContextService() .AddEntryManagerService() .AddFilesSettingsHelperService() .AddGlobalFolderHelperService() .AddFilesMessageService() .AddFileShareLinkService() .AddDocumentServiceHelperService() .AddDocumentServiceConnectorService(); } } }