/* * * (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.Diagnostics; using System.IO; using System.Runtime.Serialization; using System.Security; using ASC.Common; using ASC.Common.Logging; using ASC.Core; using ASC.Core.Common; using ASC.Core.Common.Configuration; using ASC.Core.Users; using ASC.FederatedLogin; using ASC.FederatedLogin.Helpers; using ASC.FederatedLogin.LoginProviders; 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.Core.Users; using ASC.Web.Files.Classes; using ASC.Web.Files.HttpHandlers; using ASC.Web.Files.Services.WCFService; using ASC.Web.Files.ThirdPartyApp; using ASC.Web.Files.Utils; using ASC.Web.Studio.Core; using DocuSign.eSign.Api; using DocuSign.eSign.Client; using DocuSign.eSign.Model; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; namespace ASC.Web.Files.Helpers { public class DocuSignToken { public ILog Log { get; set; } public const string AppAttr = "docusign"; public TokenHelper TokenHelper { get; } public AuthContext AuthContext { get; } public ConsumerFactory ConsumerFactory { get; } public DocuSignToken( TokenHelper tokenHelper, IOptionsMonitor options, AuthContext authContext, ConsumerFactory consumerFactory) { TokenHelper = tokenHelper; AuthContext = authContext; ConsumerFactory = consumerFactory; Log = options.CurrentValue; } public OAuth20Token GetToken() { return TokenHelper.GetToken(AppAttr); } public void DeleteToken(Guid? userId = null) { TokenHelper.DeleteToken(AppAttr, userId); } public void SaveToken(OAuth20Token token) { if (token == null) throw new ArgumentNullException("token"); TokenHelper.SaveToken(new Token(token, AppAttr)); } internal string GetRefreshedToken(OAuth20Token token) { if (token.IsExpired) { try { Log.Info("DocuSign refresh token for user " + AuthContext.CurrentAccount.ID); var refreshed = ConsumerFactory.Get().RefreshToken(token.RefreshToken); if (refreshed != null) { token.AccessToken = refreshed.AccessToken; token.RefreshToken = refreshed.RefreshToken; token.ExpiresIn = refreshed.ExpiresIn; token.Timestamp = DateTime.UtcNow; SaveToken(token); } } catch (Exception ex) { Log.Error("DocuSign refresh token for user " + AuthContext.CurrentAccount.ID, ex); } } return token.AccessToken; } } public class DocuSignHelper { public ILog Log { get; set; } public const string UserField = "userId"; public static readonly List SupportedFormats = new List { ".as", ".asl", ".doc", ".docm", ".docx", ".dot", ".dotm", ".dotx", ".htm", ".html", ".msg", ".pdf", ".pdx", ".rtf", ".txt", ".wpd", ".wps", ".wpt", ".xps", ".emz", ".svg", ".svgz", ".vdx", ".vss", ".vst", ".bmp", ".cdr", ".dcx", ".gif", ".ico", ".jpg", ".jpeg", ".pct", ".pic", ".png", ".rgb", ".sam", ".tga", ".tif", ".tiff", ".wpg", ".dps", ".dpt", ".pot", ".potx", ".pps", ".ppt", ".pptm", ".pptx", ".csv", ".et", ".ett", ".xls", ".xlsm", ".xlsx", ".xlt" }; public static long MaxFileSize = 25L * 1024L * 1024L; public static int MaxEmailLength = 10000; public DocuSignToken DocuSignToken { get; } public FileSecurity FileSecurity { get; } public IDaoFactory DaoFactory { get; } public BaseCommonLinkUtility BaseCommonLinkUtility { get; } public UserManager UserManager { get; } public AuthContext AuthContext { get; } public DisplayUserSettingsHelper DisplayUserSettingsHelper { get; } public FileMarker FileMarker { get; } public GlobalFolderHelper GlobalFolderHelper { get; } public FilesMessageService FilesMessageService { get; } public FilesLinkUtility FilesLinkUtility { get; } public IServiceProvider ServiceProvider { get; } public ConsumerFactory ConsumerFactory { get; } public DocuSignHelper( DocuSignToken docuSignToken, FileSecurity fileSecurity, IDaoFactory daoFactory, IOptionsMonitor options, BaseCommonLinkUtility baseCommonLinkUtility, UserManager userManager, AuthContext authContext, DisplayUserSettingsHelper displayUserSettingsHelper, FileMarker fileMarker, GlobalFolderHelper globalFolderHelper, FilesMessageService filesMessageService, FilesLinkUtility filesLinkUtility, IServiceProvider serviceProvider, ConsumerFactory consumerFactory) { DocuSignToken = docuSignToken; FileSecurity = fileSecurity; DaoFactory = daoFactory; BaseCommonLinkUtility = baseCommonLinkUtility; UserManager = userManager; AuthContext = authContext; DisplayUserSettingsHelper = displayUserSettingsHelper; FileMarker = fileMarker; GlobalFolderHelper = globalFolderHelper; FilesMessageService = filesMessageService; FilesLinkUtility = filesLinkUtility; ServiceProvider = serviceProvider; ConsumerFactory = consumerFactory; Log = options.CurrentValue; } public bool ValidateToken(OAuth20Token token) { GetDocuSignAccount(token); return true; } public string SendDocuSign(T fileId, DocuSignData docuSignData, IDictionary requestHeaders) { if (docuSignData == null) throw new ArgumentNullException("docuSignData"); var token = DocuSignToken.GetToken(); var account = GetDocuSignAccount(token); var configuration = GetConfiguration(account, token); var document = CreateDocument(fileId, docuSignData.Name, docuSignData.FolderId, out var sourceFile); var url = CreateEnvelope(account.AccountId, document, docuSignData, configuration); FilesMessageService.Send(sourceFile, requestHeaders, MessageAction.DocumentSendToSign, "DocuSign", sourceFile.Title); return url; } private DocuSignAccount GetDocuSignAccount(OAuth20Token token) { if (token == null) throw new ArgumentNullException("token"); var userInfoString = RequestHelper.PerformRequest(ConsumerFactory.Get().DocuSignHost + "/oauth/userinfo", headers: new Dictionary { { "Authorization", "Bearer " + DocuSignToken.GetRefreshedToken(token) } }); Log.Debug("DocuSing userInfo: " + userInfoString); var userInfo = (DocuSignUserInfo)JsonConvert.DeserializeObject(userInfoString, typeof(DocuSignUserInfo)); if (userInfo.Accounts == null || userInfo.Accounts.Count == 0) throw new Exception("Account is null"); var account = userInfo.Accounts[0]; return account; } private DocuSign.eSign.Client.Configuration GetConfiguration(DocuSignAccount account, OAuth20Token token) { if (account == null) throw new ArgumentNullException("account"); if (token == null) throw new ArgumentNullException("token"); var apiClient = new ApiClient(account.BaseUri + "/restapi"); var configuration = new DocuSign.eSign.Client.Configuration { ApiClient = apiClient }; configuration.AddDefaultHeader("Authorization", "Bearer " + DocuSignToken.GetRefreshedToken(token)); return configuration; } private Document CreateDocument(T fileId, string documentName, string folderId, out File file) { var fileDao = DaoFactory.GetFileDao(); file = fileDao.GetFile(fileId); if (file == null) throw new Exception(FilesCommonResource.ErrorMassage_FileNotFound); if (!FileSecurity.CanRead(file)) throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException_ReadFile); if (!SupportedFormats.Contains(FileUtility.GetFileExtension(file.Title))) throw new ArgumentException(FilesCommonResource.ErrorMassage_NotSupportedFormat); if (file.ContentLength > MaxFileSize) throw new Exception(FileSizeComment.GetFileSizeExceptionString(MaxFileSize)); byte[] fileBytes; using (var stream = fileDao.GetFileStream(file)) { var buffer = new byte[16 * 1024]; using var ms = new MemoryStream(); int read; while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) { ms.Write(buffer, 0, read); } fileBytes = ms.ToArray(); } if (string.IsNullOrEmpty(documentName)) { documentName = file.Title; } var document = new Document { DocumentBase64 = Convert.ToBase64String(fileBytes), DocumentFields = new List { new NameValue {Name = FilesLinkUtility.FolderId, Value = folderId}, new NameValue {Name = FilesLinkUtility.FileTitle, Value = file.Title}, }, DocumentId = "1", //file.ID.ToString(), FileExtension = FileUtility.GetFileExtension(file.Title), Name = documentName, }; return document; } private string CreateEnvelope(string accountId, Document document, DocuSignData docuSignData, DocuSign.eSign.Client.Configuration configuration) { var eventNotification = new EventNotification { EnvelopeEvents = new List { //new EnvelopeEvent {EnvelopeEventStatusCode = DocuSignStatus.Sent.ToString()}, //new EnvelopeEvent {EnvelopeEventStatusCode = DocuSignStatus.Delivered.ToString()}, new EnvelopeEvent {EnvelopeEventStatusCode = DocuSignStatus.Completed.ToString()}, new EnvelopeEvent {EnvelopeEventStatusCode = DocuSignStatus.Declined.ToString()}, new EnvelopeEvent {EnvelopeEventStatusCode = DocuSignStatus.Voided.ToString()}, }, IncludeDocumentFields = "true", //RecipientEvents = new List // { // new RecipientEvent {RecipientEventStatusCode = "Sent"}, // new RecipientEvent {RecipientEventStatusCode = "Delivered"}, // new RecipientEvent {RecipientEventStatusCode = "Completed"}, // new RecipientEvent {RecipientEventStatusCode = "Declined"}, // new RecipientEvent {RecipientEventStatusCode = "AuthenticationFailed"}, // new RecipientEvent {RecipientEventStatusCode = "AutoResponded"}, // }, Url = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandlerService.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=webhook"), }; Log.Debug("DocuSign hook url: " + eventNotification.Url); var signers = new List(); docuSignData.Users.ForEach(uid => { try { var user = UserManager.GetUsers(uid); signers.Add(new Signer { Email = user.Email, Name = user.DisplayUserName(false, DisplayUserSettingsHelper), RecipientId = user.ID.ToString(), }); } catch (Exception ex) { Log.Error("Signer is undefined", ex); } }); var envelopeDefinition = new EnvelopeDefinition { CustomFields = new CustomFields { TextCustomFields = new List { new TextCustomField {Name = UserField, Value = AuthContext.CurrentAccount.ID.ToString()}, } }, Documents = new List { document }, EmailBlurb = docuSignData.Message, EmailSubject = docuSignData.Name, EventNotification = eventNotification, Recipients = new Recipients { Signers = signers, }, Status = "created", }; var envelopesApi = new EnvelopesApi(configuration); var envelopeSummary = envelopesApi.CreateEnvelope(accountId, envelopeDefinition); Log.Debug("DocuSign createdEnvelope: " + envelopeSummary.EnvelopeId); var envelopeId = envelopeSummary.EnvelopeId; var url = envelopesApi.CreateSenderView(accountId, envelopeId, new ReturnUrlRequest { ReturnUrl = BaseCommonLinkUtility.GetFullAbsolutePath(DocuSignHandlerService.Path(FilesLinkUtility) + "?" + FilesLinkUtility.Action + "=redirect") }); Log.Debug("DocuSign senderView: " + url.Url); return url.Url; } public File SaveDocument(string envelopeId, string documentId, string documentName, T folderId) { if (string.IsNullOrEmpty(envelopeId)) throw new ArgumentNullException("envelopeId"); if (string.IsNullOrEmpty(documentId)) throw new ArgumentNullException("documentId"); var token = DocuSignToken.GetToken(); var account = GetDocuSignAccount(token); var configuration = GetConfiguration(account, token); var fileDao = DaoFactory.GetFileDao(); var folderDao = DaoFactory.GetFolderDao(); if (string.IsNullOrEmpty(documentName)) { documentName = "new.pdf"; } Folder folder; if (folderId == null || (folder = folderDao.GetFolder(folderId)) == null || folder.RootFolderType == FolderType.TRASH || !FileSecurity.CanCreate(folder)) { if (GlobalFolderHelper.FolderMy != 0) { folderId = GlobalFolderHelper.GetFolderMy(); } else { throw new SecurityException(FilesCommonResource.ErrorMassage_SecurityException_Create); } } var file = ServiceProvider.GetService>(); file.FolderID = folderId; file.Comment = FilesCommonResource.CommentCreateByDocuSign; file.Title = FileUtility.ReplaceFileExtension(documentName, ".pdf"); var envelopesApi = new EnvelopesApi(configuration); Log.Info("DocuSign webhook get stream: " + documentId); using (var stream = envelopesApi.GetDocument(account.AccountId, envelopeId, documentId)) { file.ContentLength = stream.Length; file = fileDao.SaveFile(file, stream); } FilesMessageService.Send(file, MessageInitiator.ThirdPartyProvider, MessageAction.DocumentSignComplete, "DocuSign", file.Title); FileMarker.MarkAsNew(file); return file; } [DataContract] [DebuggerDisplay("{AccountId} {BaseUri}")] private class DocuSignAccount { [DataMember(Name = "account_id", EmitDefaultValue = false)] public string AccountId { get; set; } [DataMember(Name = "base_uri", EmitDefaultValue = false)] public string BaseUri { get; set; } } [DataContract] private class DocuSignUserInfo { [DataMember(Name = "accounts", EmitDefaultValue = false)] public List Accounts { get; set; } } } [DataContract(Name = "docusign_data", Namespace = "")] [DebuggerDisplay("{Name}")] public class DocuSignData { [DataMember(Name = "folderId")] public string FolderId { get; set; } [DataMember(Name = "message")] public string Message { get; set; } [DataMember(Name = "name", IsRequired = true, EmitDefaultValue = false)] public string Name { get; set; } [DataMember(Name = "users")] public ItemList Users { get; set; } } public enum DocuSignStatus { Draft, Sent, Delivered, Completed, Declined, Voided, } public static class DocuSignHelperExtension { public static DIHelper AddDocuSignTokenService(this DIHelper services) { services.TryAddScoped(); return services .AddAuthContextService() .AddDocuSignLoginProviderService() .AddTokenHelperService(); } public static DIHelper AddDocuSignHelperService(this DIHelper services) { services.TryAddScoped(); return services .AddDocuSignLoginProviderService() .AddFileSecurityService() .AddDaoFactoryService() .AddBaseCommonLinkUtilityService() .AddUserManagerService() .AddAuthContextService() .AddDisplayUserSettingsService() .AddFileMarkerService() .AddGlobalFolderHelperService() .AddFilesMessageService() .AddDocuSignTokenService() .AddFilesLinkUtilityService() ; } } }