diff --git a/WOPI.sln b/WOPI.sln index 9df5073..4cb5e30 100644 --- a/WOPI.sln +++ b/WOPI.sln @@ -4,6 +4,7 @@ VisualStudioVersion = 16.0.29319.158 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{785E1533-48CE-4B5E-8C59-D6F1FDA8C45C}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .gitignore = .gitignore appveyor.yml = appveyor.yml coverage.ps1 = coverage.ps1 diff --git a/WopiHost.Abstractions/WopiClaimTypes.cs b/WopiHost.Abstractions/WopiClaimTypes.cs index 265963b..240b44b 100644 --- a/WopiHost.Abstractions/WopiClaimTypes.cs +++ b/WopiHost.Abstractions/WopiClaimTypes.cs @@ -2,6 +2,6 @@ { public class WopiClaimTypes { - public const string UserPermissions = nameof(UserPermissions); + public const string USER_PERMISSIONS = nameof(USER_PERMISSIONS); } } diff --git a/WopiHost.Cobalt/CobaltHostLockingStore.cs b/WopiHost.Cobalt/CobaltHostLockingStore.cs index a9cc90f..a30c859 100644 --- a/WopiHost.Cobalt/CobaltHostLockingStore.cs +++ b/WopiHost.Cobalt/CobaltHostLockingStore.cs @@ -16,7 +16,7 @@ public CobaltHostLockingStore(ClaimsPrincipal principal) public override WhoAmIRequest.OutputType HandleWhoAmI(WhoAmIRequest.InputType input) { - WhoAmIRequest.OutputType result = new WhoAmIRequest.OutputType + var result = new WhoAmIRequest.OutputType { UserEmailAddress = _principal?.FindFirst(ClaimTypes.Email).Value, UserIsAnonymous = string.IsNullOrEmpty(_principal?.FindFirst(ClaimTypes.NameIdentifier).Value), @@ -29,14 +29,14 @@ public override WhoAmIRequest.OutputType HandleWhoAmI(WhoAmIRequest.InputType in public override ServerTimeRequest.OutputType HandleServerTime(ServerTimeRequest.InputType input) { - ServerTimeRequest.OutputType result = new ServerTimeRequest.OutputType { ServerTime = DateTime.UtcNow }; + var result = new ServerTimeRequest.OutputType { ServerTime = DateTime.UtcNow }; return result; } public override LockAndCheckOutStatusRequest.OutputType HandleLockAndCheckOutStatus(LockAndCheckOutStatusRequest.InputType input) { - LockAndCheckOutStatusRequest.OutputType result = new LockAndCheckOutStatusRequest.OutputType + var result = new LockAndCheckOutStatusRequest.OutputType { LockType = 1U, CheckOutType = 0U @@ -47,84 +47,84 @@ public override LockAndCheckOutStatusRequest.OutputType HandleLockAndCheckOutSta public override GetExclusiveLockRequest.OutputType HandleGetExclusiveLock(GetExclusiveLockRequest.InputType input) { - GetExclusiveLockRequest.OutputType result = new GetExclusiveLockRequest.OutputType(); + var result = new GetExclusiveLockRequest.OutputType(); return result; } public override RefreshExclusiveLockRequest.OutputType HandleRefreshExclusiveLock(RefreshExclusiveLockRequest.InputType input) { - RefreshExclusiveLockRequest.OutputType result = new RefreshExclusiveLockRequest.OutputType(); + var result = new RefreshExclusiveLockRequest.OutputType(); return result; } public override CheckExclusiveLockAvailabilityRequest.OutputType HandleCheckExclusiveLockAvailability(CheckExclusiveLockAvailabilityRequest.InputType input) { - CheckExclusiveLockAvailabilityRequest.OutputType result = new CheckExclusiveLockAvailabilityRequest.OutputType(); + var result = new CheckExclusiveLockAvailabilityRequest.OutputType(); return result; } public override ConvertExclusiveLockToSchemaLockRequest.OutputType HandleConvertExclusiveLockToSchemaLock(ConvertExclusiveLockToSchemaLockRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - ConvertExclusiveLockToSchemaLockRequest.OutputType result = new ConvertExclusiveLockToSchemaLockRequest.OutputType(); + var result = new ConvertExclusiveLockToSchemaLockRequest.OutputType(); return result; } public override ConvertExclusiveLockWithCoauthTransitionRequest.OutputType HandleConvertExclusiveLockWithCoauthTransition(ConvertExclusiveLockWithCoauthTransitionRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - ConvertExclusiveLockWithCoauthTransitionRequest.OutputType result = new ConvertExclusiveLockWithCoauthTransitionRequest.OutputType(); + var result = new ConvertExclusiveLockWithCoauthTransitionRequest.OutputType(); return result; } public override GetSchemaLockRequest.OutputType HandleGetSchemaLock(GetSchemaLockRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - GetSchemaLockRequest.OutputType result = new GetSchemaLockRequest.OutputType(); + var result = new GetSchemaLockRequest.OutputType(); return result; } public override ReleaseExclusiveLockRequest.OutputType HandleReleaseExclusiveLock(ReleaseExclusiveLockRequest.InputType input) { - ReleaseExclusiveLockRequest.OutputType result = new ReleaseExclusiveLockRequest.OutputType(); + var result = new ReleaseExclusiveLockRequest.OutputType(); return result; } public override ReleaseSchemaLockRequest.OutputType HandleReleaseSchemaLock(ReleaseSchemaLockRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - ReleaseSchemaLockRequest.OutputType result = new ReleaseSchemaLockRequest.OutputType(); + var result = new ReleaseSchemaLockRequest.OutputType(); return result; } public override RefreshSchemaLockRequest.OutputType HandleRefreshSchemaLock(RefreshSchemaLockRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - RefreshSchemaLockRequest.OutputType result = new RefreshSchemaLockRequest.OutputType { Lock = LockType.SchemaLock }; + var result = new RefreshSchemaLockRequest.OutputType { Lock = LockType.SchemaLock }; return result; } public override ConvertSchemaLockToExclusiveLockRequest.OutputType HandleConvertSchemaLockToExclusiveLock(ConvertSchemaLockToExclusiveLockRequest.InputType input) { - ConvertSchemaLockToExclusiveLockRequest.OutputType result = new ConvertSchemaLockToExclusiveLockRequest.OutputType(); + var result = new ConvertSchemaLockToExclusiveLockRequest.OutputType(); return result; } public override CheckSchemaLockAvailabilityRequest.OutputType HandleCheckSchemaLockAvailability(CheckSchemaLockAvailabilityRequest.InputType input) { - CheckSchemaLockAvailabilityRequest.OutputType result = new CheckSchemaLockAvailabilityRequest.OutputType(); + var result = new CheckSchemaLockAvailabilityRequest.OutputType(); return result; } public override JoinCoauthoringRequest.OutputType HandleJoinCoauthoring(JoinCoauthoringRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - JoinCoauthoringRequest.OutputType result = new JoinCoauthoringRequest.OutputType + var result = new JoinCoauthoringRequest.OutputType { Lock = LockType.SchemaLock, CoauthStatus = CoauthStatusType.Alone, @@ -135,14 +135,14 @@ public override JoinCoauthoringRequest.OutputType HandleJoinCoauthoring(JoinCoau public override ExitCoauthoringRequest.OutputType HandleExitCoauthoring(ExitCoauthoringRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - ExitCoauthoringRequest.OutputType result = new ExitCoauthoringRequest.OutputType(); + var result = new ExitCoauthoringRequest.OutputType(); return result; } public override RefreshCoauthoringSessionRequest.OutputType HandleRefreshCoauthoring(RefreshCoauthoringSessionRequest.InputType input, int protocolMajorVersion, int protocolMinorVersion) { - RefreshCoauthoringSessionRequest.OutputType result = new RefreshCoauthoringSessionRequest.OutputType + var result = new RefreshCoauthoringSessionRequest.OutputType { Lock = LockType.SchemaLock, CoauthStatus = CoauthStatusType.Alone @@ -153,28 +153,28 @@ public override RefreshCoauthoringSessionRequest.OutputType HandleRefreshCoautho public override ConvertCoauthLockToExclusiveLockRequest.OutputType HandleConvertCoauthLockToExclusiveLock(ConvertCoauthLockToExclusiveLockRequest.InputType input) { - ConvertCoauthLockToExclusiveLockRequest.OutputType result = new ConvertCoauthLockToExclusiveLockRequest.OutputType(); + var result = new ConvertCoauthLockToExclusiveLockRequest.OutputType(); return result; } public override CheckCoauthLockAvailabilityRequest.OutputType HandleCheckCoauthLockAvailability(CheckCoauthLockAvailabilityRequest.InputType input) { - CheckCoauthLockAvailabilityRequest.OutputType result = new CheckCoauthLockAvailabilityRequest.OutputType(); + var result = new CheckCoauthLockAvailabilityRequest.OutputType(); return result; } public override MarkCoauthTransitionCompleteRequest.OutputType HandleMarkCoauthTransitionComplete(MarkCoauthTransitionCompleteRequest.InputType input) { - MarkCoauthTransitionCompleteRequest.OutputType result = new MarkCoauthTransitionCompleteRequest.OutputType(); + var result = new MarkCoauthTransitionCompleteRequest.OutputType(); return result; } public override GetCoauthoringStatusRequest.OutputType HandleGetCoauthoringStatus(GetCoauthoringStatusRequest.InputType input) { - GetCoauthoringStatusRequest.OutputType result = new GetCoauthoringStatusRequest.OutputType + var result = new GetCoauthoringStatusRequest.OutputType { CoauthStatus = CoauthStatusType.Alone }; @@ -189,35 +189,35 @@ public override Dictionary QueryEditorsTable() public override JoinEditingSessionRequest.OutputType HandleJoinEditingSession(JoinEditingSessionRequest.InputType input) { - JoinEditingSessionRequest.OutputType result = new JoinEditingSessionRequest.OutputType(); + var result = new JoinEditingSessionRequest.OutputType(); return result; } public override RefreshEditingSessionRequest.OutputType HandleRefreshEditingSession(RefreshEditingSessionRequest.InputType input) { - RefreshEditingSessionRequest.OutputType result = new RefreshEditingSessionRequest.OutputType(); + var result = new RefreshEditingSessionRequest.OutputType(); return result; } public override LeaveEditingSessionRequest.OutputType HandleLeaveEditingSession(LeaveEditingSessionRequest.InputType input) { - LeaveEditingSessionRequest.OutputType result = new LeaveEditingSessionRequest.OutputType(); + var result = new LeaveEditingSessionRequest.OutputType(); return result; } public override UpdateEditorMetadataRequest.OutputType HandleUpdateEditorMetadata(UpdateEditorMetadataRequest.InputType input) { - UpdateEditorMetadataRequest.OutputType result = new UpdateEditorMetadataRequest.OutputType(); + var result = new UpdateEditorMetadataRequest.OutputType(); return result; } public override RemoveEditorMetadataRequest.OutputType HandleRemoveEditorMetadata(RemoveEditorMetadataRequest.InputType input) { - RemoveEditorMetadataRequest.OutputType result = new RemoveEditorMetadataRequest.OutputType(); + var result = new RemoveEditorMetadataRequest.OutputType(); return result; } @@ -229,21 +229,21 @@ public override ulong GetEditorsTableWaterline() public override AmIAloneRequest.OutputType HandleAmIAlone(AmIAloneRequest.InputType input) { - AmIAloneRequest.OutputType result = new AmIAloneRequest.OutputType { AmIAlone = true }; + var result = new AmIAloneRequest.OutputType { AmIAlone = true }; return result; } public override DocMetaInfoRequest.OutputType HandleDocMetaInfo(DocMetaInfoRequest.InputType input) { - DocMetaInfoRequest.OutputType result = new DocMetaInfoRequest.OutputType(); + var result = new DocMetaInfoRequest.OutputType(); return result; } public override VersionsRequest.OutputType HandleVersions(VersionsRequest.InputType input) { - VersionsRequest.OutputType result = new VersionsRequest.OutputType { Enabled = false }; + var result = new VersionsRequest.OutputType { Enabled = false }; return result; } diff --git a/WopiHost.Cobalt/CobaltSession.cs b/WopiHost.Cobalt/CobaltSession.cs index 8cb4845..3b23fcc 100644 --- a/WopiHost.Cobalt/CobaltSession.cs +++ b/WopiHost.Cobalt/CobaltSession.cs @@ -13,7 +13,7 @@ public class CobaltProcessor : ICobaltProcessor private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) { var disposal = new DisposalEscrow(file.Owner); - CobaltFilePartitionConfig content = new CobaltFilePartitionConfig + var content = new CobaltFilePartitionConfig { IsNewFile = true, HostBlobStore = new TemporaryHostBlobStore(new TemporaryHostBlobStore.Config(), disposal, file.Identifier + @".Content"), @@ -23,7 +23,7 @@ private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) PartitionId = FilePartitionId.Content }; - CobaltFilePartitionConfig coauth = new CobaltFilePartitionConfig + var coauth = new CobaltFilePartitionConfig { IsNewFile = true, HostBlobStore = new TemporaryHostBlobStore(new TemporaryHostBlobStore.Config(), disposal, file.Identifier + @".CoauthMetadata"), @@ -33,7 +33,7 @@ private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) PartitionId = FilePartitionId.CoauthMetadata }; - CobaltFilePartitionConfig wacupdate = new CobaltFilePartitionConfig + var wacupdate = new CobaltFilePartitionConfig { IsNewFile = true, HostBlobStore = new TemporaryHostBlobStore(new TemporaryHostBlobStore.Config(), disposal, file.Identifier + @".WordWacUpdate"), @@ -43,7 +43,7 @@ private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) PartitionId = FilePartitionId.WordWacUpdate }; - Dictionary partitionConfigs = new Dictionary { { FilePartitionId.Content, content }, { FilePartitionId.WordWacUpdate, wacupdate }, { FilePartitionId.CoauthMetadata, coauth } }; + var partitionConfigs = new Dictionary { { FilePartitionId.Content, content }, { FilePartitionId.WordWacUpdate, wacupdate }, { FilePartitionId.CoauthMetadata, coauth } }; var tempCobaltFile = new CobaltFile(disposal, partitionConfigs, new CobaltHostLockingStore(principal), null); @@ -53,7 +53,7 @@ private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) using (var stream = file.GetReadStream()) { var srcAtom = new AtomFromStream(stream); - tempCobaltFile.GetCobaltFilePartition(FilePartitionId.Content).SetStream(RootId.Default.Value, srcAtom, out Metrics o1); + tempCobaltFile.GetCobaltFilePartition(FilePartitionId.Content).SetStream(RootId.Default.Value, srcAtom, out var o1); tempCobaltFile.GetCobaltFilePartition(FilePartitionId.Content).GetStream(RootId.Default.Value).Flush(); } } @@ -65,7 +65,7 @@ private CobaltFile GetCobaltFile(IWopiFile file, ClaimsPrincipal principal) public Stream GetFileStream(IWopiFile file, ClaimsPrincipal principal) { //TODO: use in filescontroller - using (MemoryStream ms = new MemoryStream()) + using (var ms = new MemoryStream()) { new GenericFda(GetCobaltFile(file, principal).CobaltEndpoint).GetContentStream().CopyTo(ms); return ms; @@ -76,11 +76,11 @@ public Stream GetFileStream(IWopiFile file, ClaimsPrincipal principal) public Action ProcessCobalt(IWopiFile file, ClaimsPrincipal principal, byte[] newContent) { // Refactoring tip: there are more ways of initializing Atom - AtomFromByteArray atomRequest = new AtomFromByteArray(newContent); - RequestBatch requestBatch = new RequestBatch(); + var atomRequest = new AtomFromByteArray(newContent); + var requestBatch = new RequestBatch(); - requestBatch.DeserializeInputFromProtocol(atomRequest, out object ctx, out ProtocolVersion protocolVersion); + requestBatch.DeserializeInputFromProtocol(atomRequest, out var ctx, out var protocolVersion); var cobaltFile = GetCobaltFile(file, principal); cobaltFile.CobaltEndpoint.ExecuteRequestBatch(requestBatch); diff --git a/WopiHost.Core/Controllers/ContainersController.cs b/WopiHost.Core/Controllers/ContainersController.cs index 16ee168..feff4f1 100644 --- a/WopiHost.Core/Controllers/ContainersController.cs +++ b/WopiHost.Core/Controllers/ContainersController.cs @@ -48,11 +48,11 @@ public CheckContainerInfo GetCheckContainerInfo(string id) [Produces("application/json")] public Container EnumerateChildren(string id) { - Container container = new Container(); + var container = new Container(); var files = new List(); var containers = new List(); - foreach (IWopiFile wopiFile in StorageProvider.GetWopiFiles(id)) + foreach (var wopiFile in StorageProvider.GetWopiFiles(id)) { files.Add(new ChildFile { @@ -64,7 +64,7 @@ public Container EnumerateChildren(string id) }); } - foreach (IWopiFolder wopiContainer in StorageProvider.GetWopiContainers(id)) + foreach (var wopiContainer in StorageProvider.GetWopiContainers(id)) { containers.Add(new ChildContainer { diff --git a/WopiHost.Core/Controllers/EcosystemController.cs b/WopiHost.Core/Controllers/EcosystemController.cs index ca41e76..0c9216a 100644 --- a/WopiHost.Core/Controllers/EcosystemController.cs +++ b/WopiHost.Core/Controllers/EcosystemController.cs @@ -27,7 +27,7 @@ public EcosystemController(IWopiStorageProvider fileProvider, IWopiSecurityHandl public RootContainerInfo GetRootContainer() { var root = StorageProvider.GetWopiContainer(@".\"); - RootContainerInfo rc = new RootContainerInfo + var rc = new RootContainerInfo { ContainerPointer = new ChildContainer { diff --git a/WopiHost.Core/Controllers/FilesController.cs b/WopiHost.Core/Controllers/FilesController.cs index 4d12680..1748f44 100644 --- a/WopiHost.Core/Controllers/FilesController.cs +++ b/WopiHost.Core/Controllers/FilesController.cs @@ -38,14 +38,14 @@ public class FilesController : WopiControllerBase /// /// Collection holding information about locks. Should be persistent. /// - private static IDictionary LockStorage; + private static IDictionary _lockStorage; - private string WopiOverrideHeader => HttpContext.Request.Headers[WopiHeaders.WopiOverride]; + private string WopiOverrideHeader => HttpContext.Request.Headers[WopiHeaders.WOPI_OVERRIDE]; public FilesController(IWopiStorageProvider storageProvider, IWopiSecurityHandler securityHandler, IOptionsSnapshot wopiHostOptions, IAuthorizationService authorizationService, IDictionary lockStorage, ICobaltProcessor cobaltProcessor = null) : base(storageProvider, securityHandler, wopiHostOptions) { _authorizationService = authorizationService; - LockStorage = lockStorage; + _lockStorage = lockStorage; CobaltProcessor = cobaltProcessor; } @@ -86,7 +86,7 @@ public async Task GetFile(string id) var file = StorageProvider.GetWopiFile(id); // Check expected size - int? maximumExpectedSize = HttpContext.Request.Headers[WopiHeaders.MaxExpectedSize].ToString().ToNullableInt(); + var maximumExpectedSize = HttpContext.Request.Headers[WopiHeaders.MAX_EXPECTED_SIZE].ToString().ToNullableInt(); if (maximumExpectedSize != null && file.GetCheckFileInfo(User, HostCapabilities).Size > maximumExpectedSize.Value) { return new PreconditionFailedResult(); @@ -125,9 +125,9 @@ public async Task PutFile(string id) // Save file contents var newContent = await HttpContext.Request.Body.ReadBytesAsync(); - using (var stream = file.GetWriteStream()) + await using (var stream = file.GetWriteStream()) { - stream.Write(newContent, 0, newContent.Length); + await stream.WriteAsync(newContent, 0, newContent.Length); } return new OkResult(); @@ -160,15 +160,15 @@ public async Task ProcessCobalt(string id) var file = StorageProvider.GetWopiFile(id); // TODO: remove workaround https://github.com/aspnet/Announcements/issues/342 (use FileBufferingWriteStream) - var syncIOFeature = HttpContext.Features.Get(); - if (syncIOFeature != null) + var syncIoFeature = HttpContext.Features.Get(); + if (syncIoFeature != null) { - syncIOFeature.AllowSynchronousIO = true; + syncIoFeature.AllowSynchronousIO = true; } var responseAction = CobaltProcessor.ProcessCobalt(file, User, await HttpContext.Request.Body.ReadBytesAsync()); - HttpContext.Response.Headers.Add(WopiHeaders.CorrelationId, HttpContext.Request.Headers[WopiHeaders.CorrelationId]); - HttpContext.Response.Headers.Add("request-id", HttpContext.Request.Headers[WopiHeaders.CorrelationId]); + HttpContext.Response.Headers.Add(WopiHeaders.CORRELATION_ID, HttpContext.Request.Headers[WopiHeaders.CORRELATION_ID]); + HttpContext.Response.Headers.Add("request-id", HttpContext.Request.Headers[WopiHeaders.CORRELATION_ID]); return new Results.FileResult(responseAction, "application/octet-stream"); } @@ -183,18 +183,18 @@ public async Task ProcessCobalt(string id) [HttpPost("{id}"), WopiOverrideHeader(new[] { "LOCK", "UNLOCK", "REFRESH_LOCK", "GET_LOCK" })] public IActionResult ProcessLock(string id) { - string oldLock = Request.Headers[WopiHeaders.OldLock]; - string newLock = Request.Headers[WopiHeaders.Lock]; + string oldLock = Request.Headers[WopiHeaders.OLD_LOCK]; + string newLock = Request.Headers[WopiHeaders.LOCK]; - lock (LockStorage) + lock (_lockStorage) { - bool lockAcquired = TryGetLock(id, out var existingLock); + var lockAcquired = TryGetLock(id, out var existingLock); switch (WopiOverrideHeader) { case "GET_LOCK": if (lockAcquired) { - Response.Headers[WopiHeaders.Lock] = existingLock.Lock; + Response.Headers[WopiHeaders.LOCK] = existingLock.Lock; } return new OkResult(); @@ -220,7 +220,7 @@ public IActionResult ProcessLock(string id) else { // The file is not currently locked, create and store new lock information - LockStorage[id] = new LockInfo { DateCreated = DateTime.UtcNow, Lock = newLock }; + _lockStorage[id] = new LockInfo { DateCreated = DateTime.UtcNow, Lock = newLock }; return new OkResult(); } } @@ -232,7 +232,7 @@ public IActionResult ProcessLock(string id) if (existingLock.Lock == oldLock) { // Replace the existing lock with the new one - LockStorage[id] = new LockInfo { DateCreated = DateTime.UtcNow, Lock = newLock }; + _lockStorage[id] = new LockInfo { DateCreated = DateTime.UtcNow, Lock = newLock }; return new OkResult(); } else @@ -254,7 +254,7 @@ public IActionResult ProcessLock(string id) if (existingLock.Lock == newLock) { // Remove valid lock - LockStorage.Remove(id); + _lockStorage.Remove(id); return new OkResult(); } else @@ -297,11 +297,11 @@ public IActionResult ProcessLock(string id) private bool TryGetLock(string fileId, out LockInfo lockInfo) { - if (LockStorage.TryGetValue(fileId, out lockInfo)) + if (_lockStorage.TryGetValue(fileId, out lockInfo)) { if (lockInfo.Expired) { - LockStorage.Remove(fileId); + _lockStorage.Remove(fileId); return false; } return true; @@ -312,10 +312,10 @@ private bool TryGetLock(string fileId, out LockInfo lockInfo) private StatusCodeResult ReturnLockMismatch(HttpResponse response, string existingLock = null, string reason = null) { - response.Headers[WopiHeaders.Lock] = existingLock ?? string.Empty; + response.Headers[WopiHeaders.LOCK] = existingLock ?? string.Empty; if (!string.IsNullOrEmpty(reason)) { - response.Headers[WopiHeaders.LockFailureReason] = reason; + response.Headers[WopiHeaders.LOCK_FAILURE_REASON] = reason; } return new ConflictResult(); } diff --git a/WopiHost.Core/Controllers/WopiBootstrapperController.cs b/WopiHost.Core/Controllers/WopiBootstrapperController.cs index addec94..c010053 100644 --- a/WopiHost.Core/Controllers/WopiBootstrapperController.cs +++ b/WopiHost.Core/Controllers/WopiBootstrapperController.cs @@ -23,8 +23,8 @@ public WopiBootstrapperController(IWopiStorageProvider fileProvider, IWopiSecuri public IActionResult GetRootContainer() { var authorizationHeader = HttpContext.Request.Headers["Authorization"]; - var ecosystemOperation = HttpContext.Request.Headers[WopiHeaders.EcosystemOperation]; - string wopiSrc = HttpContext.Request.Headers[WopiHeaders.WopiSrc].FirstOrDefault(); + var ecosystemOperation = HttpContext.Request.Headers[WopiHeaders.ECOSYSTEM_OPERATION]; + var wopiSrc = HttpContext.Request.Headers[WopiHeaders.WOPI_SRC].FirstOrDefault(); if (ValidateAuthorizationHeader(authorizationHeader)) { @@ -32,7 +32,7 @@ public IActionResult GetRootContainer() var user = "Anonymous"; //TODO: implement bootstrap - BootstrapRootContainerInfo bootstrapRoot = new BootstrapRootContainerInfo + var bootstrapRoot = new BootstrapRootContainerInfo { Bootstrap = new BootstrapInfo { @@ -75,10 +75,10 @@ public IActionResult GetRootContainer() else { //TODO: implement WWW-authentication header https://wopirest.readthedocs.io/en/latest/bootstrapper/Bootstrap.html#www-authenticate-header - string authorizationUri = "https://contoso.com/api/oauth2/authorize"; - string tokenIssuanceUri = "https://contoso.com/api/oauth2/token"; - string providerId = "tp_contoso"; - string urlSchemes = Uri.EscapeDataString("{\"iOS\" : [\"contoso\",\"contoso - EMM\"], \"Android\" : [\"contoso\",\"contoso - EMM\"], \"UWP\": [\"contoso\",\"contoso - EMM\"]}"); + var authorizationUri = "https://contoso.com/api/oauth2/authorize"; + var tokenIssuanceUri = "https://contoso.com/api/oauth2/token"; + var providerId = "tp_contoso"; + var urlSchemes = Uri.EscapeDataString("{\"iOS\" : [\"contoso\",\"contoso - EMM\"], \"Android\" : [\"contoso\",\"contoso - EMM\"], \"UWP\": [\"contoso\",\"contoso - EMM\"]}"); Response.Headers.Add("WWW-Authenticate", $"Bearer authorization_uri=\"{authorizationUri}\",tokenIssuance_uri=\"{tokenIssuanceUri}\",providerId=\"{providerId}\", UrlSchemes=\"{urlSchemes}\""); return new UnauthorizedResult(); } @@ -87,7 +87,7 @@ public IActionResult GetRootContainer() private string GetIdFromUrl(string resourceUrl) { - string resourceId = resourceUrl.Substring(resourceUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); + var resourceId = resourceUrl.Substring(resourceUrl.LastIndexOf("/", StringComparison.Ordinal) + 1); var queryIndex = resourceId.IndexOf("?", StringComparison.Ordinal); if (queryIndex > -1) { diff --git a/WopiHost.Core/Controllers/WopiControllerBase.cs b/WopiHost.Core/Controllers/WopiControllerBase.cs index a1acdd0..92ff140 100644 --- a/WopiHost.Core/Controllers/WopiControllerBase.cs +++ b/WopiHost.Core/Controllers/WopiControllerBase.cs @@ -23,8 +23,8 @@ protected string AccessToken get { //TODO: an alternative would be HttpContext.GetTokenAsync(AccessTokenDefaults.AuthenticationScheme, AccessTokenDefaults.AccessTokenQueryName).Result (if the code below doesn't work) - var authenticateInfo = HttpContext.AuthenticateAsync(AccessTokenDefaults.AuthenticationScheme).Result; - return authenticateInfo?.Properties?.GetTokenValue(AccessTokenDefaults.AccessTokenQueryName); + var authenticateInfo = HttpContext.AuthenticateAsync(AccessTokenDefaults.AUTHENTICATION_SCHEME).Result; + return authenticateInfo?.Properties?.GetTokenValue(AccessTokenDefaults.ACCESS_TOKEN_QUERY_NAME); } } diff --git a/WopiHost.Core/Extensions.cs b/WopiHost.Core/Extensions.cs index e37d1e4..614eb76 100644 --- a/WopiHost.Core/Extensions.cs +++ b/WopiHost.Core/Extensions.cs @@ -14,7 +14,7 @@ internal static class Extensions /// Byte array copy of a stream public static async Task ReadBytesAsync(this Stream input) { - using (MemoryStream ms = new MemoryStream()) + using (var ms = new MemoryStream()) { await input.CopyToAsync(ms); return ms.ToArray(); @@ -28,7 +28,7 @@ public static async Task ReadBytesAsync(this Stream input) /// Integer parsed from public static int? ToNullableInt(this string s) { - if (int.TryParse(s, out int i)) return i; + if (int.TryParse(s, out var i)) return i; return null; } @@ -37,8 +37,8 @@ public static async Task ReadBytesAsync(this Stream input) /// public static long ToUnixTimestamp(this DateTime dateTime) { - DateTime unixStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - long unixTimeStampInTicks = (dateTime.ToUniversalTime() - unixStart).Ticks; + var unixStart = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var unixTimeStampInTicks = (dateTime.ToUniversalTime() - unixStart).Ticks; return unixTimeStampInTicks / TimeSpan.TicksPerSecond; } diff --git a/WopiHost.Core/FileExtensions.cs b/WopiHost.Core/FileExtensions.cs index f5d570e..15d12f3 100644 --- a/WopiHost.Core/FileExtensions.cs +++ b/WopiHost.Core/FileExtensions.cs @@ -9,7 +9,7 @@ namespace WopiHost.Core { public static class FileExtensions { - private static readonly SHA256 SHA = SHA256.Create(); + private static readonly SHA256 Sha = SHA256.Create(); public static CheckFileInfo GetCheckFileInfo(this IWopiFile file, ClaimsPrincipal principal, HostCapabilities capabilities) { @@ -23,13 +23,13 @@ public static CheckFileInfo GetCheckFileInfo(this IWopiFile file, ClaimsPrincipa throw new ArgumentNullException(nameof(capabilities)); } - CheckFileInfo checkFileInfo = new CheckFileInfo(); + var checkFileInfo = new CheckFileInfo(); if (principal != null) { checkFileInfo.UserId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value.ToSafeIdentity(); checkFileInfo.UserFriendlyName = principal.FindFirst(ClaimTypes.Name)?.Value; - WopiUserPermissions permissions = (WopiUserPermissions)Enum.Parse(typeof(WopiUserPermissions), principal.FindFirst(WopiClaimTypes.UserPermissions).Value); + var permissions = (WopiUserPermissions)Enum.Parse(typeof(WopiUserPermissions), principal.FindFirst(WopiClaimTypes.USER_PERMISSIONS).Value); checkFileInfo.ReadOnly = permissions.HasFlag(WopiUserPermissions.ReadOnly); checkFileInfo.RestrictedWebViewOnly = permissions.HasFlag(WopiUserPermissions.RestrictedWebViewOnly); @@ -67,8 +67,8 @@ public static CheckFileInfo GetCheckFileInfo(this IWopiFile file, ClaimsPrincipa using (var stream = file.GetReadStream()) { - byte[] checksum = SHA.ComputeHash(stream); - checkFileInfo.SHA256 = Convert.ToBase64String(checksum); + var checksum = Sha.ComputeHash(stream); + checkFileInfo.Sha256 = Convert.ToBase64String(checksum); } checkFileInfo.BaseFileName = file.Name; checkFileInfo.FileExtension = "." + file.Extension.TrimStart('.'); diff --git a/WopiHost.Core/HttpHeaderAttribute.cs b/WopiHost.Core/HttpHeaderAttribute.cs index 8c638cb..78e5b36 100644 --- a/WopiHost.Core/HttpHeaderAttribute.cs +++ b/WopiHost.Core/HttpHeaderAttribute.cs @@ -17,9 +17,7 @@ public HttpHeaderAttribute(string header, params string[] values) public bool Accept(ActionConstraintContext context) { - return context == null - ? false - : context.RouteContext.HttpContext.Request.Headers.TryGetValue(Header, out var value) ? Values.Contains(value[0]) : false; + return context != null && (context.RouteContext.HttpContext.Request.Headers.TryGetValue(Header, out var value) && Values.Contains(value[0])); } public int Order => 0; diff --git a/WopiHost.Core/Models/CheckFileInfo.cs b/WopiHost.Core/Models/CheckFileInfo.cs index 82fae83..8c84c3a 100644 --- a/WopiHost.Core/Models/CheckFileInfo.cs +++ b/WopiHost.Core/Models/CheckFileInfo.cs @@ -96,7 +96,7 @@ public class CheckFileInfo public bool RestrictedWebViewOnly { get; set; } - public string SHA256 { get; set; } + public string Sha256 { get; set; } public string UniqueContentId { get; set; } @@ -106,7 +106,11 @@ public class CheckFileInfo public bool SupportsCobalt { get; set; } - public bool SupportsFolders { get { return SupportsContainers; } set { SupportsContainers = value; } } + public bool SupportsFolders + { + get => SupportsContainers; + set => SupportsContainers = value; + } public bool SupportsContainers { get; set; } diff --git a/WopiHost.Core/Security/Authentication/AccessTokenDefaults.cs b/WopiHost.Core/Security/Authentication/AccessTokenDefaults.cs index b0bb12c..c6e4e8f 100644 --- a/WopiHost.Core/Security/Authentication/AccessTokenDefaults.cs +++ b/WopiHost.Core/Security/Authentication/AccessTokenDefaults.cs @@ -5,11 +5,11 @@ public static class AccessTokenDefaults /// /// Default value for AuthenticationScheme property in the AccessTokenAuthenticationOptions /// - public const string AuthenticationScheme = "AccessToken"; + public const string AUTHENTICATION_SCHEME = "AccessToken"; /// /// Default query string name used for the access token. /// - public const string AccessTokenQueryName = "access_token"; + public const string ACCESS_TOKEN_QUERY_NAME = "access_token"; } } \ No newline at end of file diff --git a/WopiHost.Core/Security/Authentication/AccessTokenHandler.cs b/WopiHost.Core/Security/Authentication/AccessTokenHandler.cs index 1994ec1..03af491 100644 --- a/WopiHost.Core/Security/Authentication/AccessTokenHandler.cs +++ b/WopiHost.Core/Security/Authentication/AccessTokenHandler.cs @@ -16,7 +16,7 @@ protected override Task HandleAuthenticateAsync() { //TODO: implement access_token_ttl https://msdn.microsoft.com/en-us/library/hh695362(v=office.12).aspx - var token = Context.Request.Query[AccessTokenDefaults.AccessTokenQueryName]; + var token = Context.Request.Query[AccessTokenDefaults.ACCESS_TOKEN_QUERY_NAME]; if (Context.Request.Path.Value == "/wopibootstrapper") { @@ -33,7 +33,7 @@ protected override Task HandleAuthenticateAsync() { ticket.Properties.StoreTokens(new[] { - new AuthenticationToken { Name = AccessTokenDefaults.AccessTokenQueryName, Value = token } + new AuthenticationToken { Name = AccessTokenDefaults.ACCESS_TOKEN_QUERY_NAME, Value = token } }); } return Task.FromResult(AuthenticateResult.Success(ticket)); diff --git a/WopiHost.Core/WopiCoreBuilderExtensions.cs b/WopiHost.Core/WopiCoreBuilderExtensions.cs index 50e67af..4abac77 100644 --- a/WopiHost.Core/WopiCoreBuilderExtensions.cs +++ b/WopiHost.Core/WopiCoreBuilderExtensions.cs @@ -20,8 +20,8 @@ public static void AddWopi(this IServiceCollection services, IWopiSecurityHandle .AddApplicationPart(typeof(WopiCoreBuilderExtensions).GetTypeInfo().Assembly) // Add controllers from this assembly .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = null); // Ensure PascalCase property name-style - services.AddAuthentication(o => { o.DefaultScheme = AccessTokenDefaults.AuthenticationScheme; }) - .AddTokenAuthentication(AccessTokenDefaults.AuthenticationScheme, AccessTokenDefaults.AuthenticationScheme, options => { options.SecurityHandler = securityHandler; }); + services.AddAuthentication(o => { o.DefaultScheme = AccessTokenDefaults.AUTHENTICATION_SCHEME; }) + .AddTokenAuthentication(AccessTokenDefaults.AUTHENTICATION_SCHEME, AccessTokenDefaults.AUTHENTICATION_SCHEME, options => { options.SecurityHandler = securityHandler; }); } } } diff --git a/WopiHost.Core/WopiHeaders.cs b/WopiHost.Core/WopiHeaders.cs index c2ad7c8..614a6f9 100644 --- a/WopiHost.Core/WopiHeaders.cs +++ b/WopiHost.Core/WopiHeaders.cs @@ -3,26 +3,26 @@ public class WopiHeaders { - public const string WopiOverride = "X-WOPI-Override"; + public const string WOPI_OVERRIDE = "X-WOPI-Override"; /// /// A string value identifying the current lock on the file. This header must always be included when responding to the request with 409 Conflict. It should not be included when responding to the request with 200 OK. /// - public const string Lock = "X-WOPI-Lock"; - public const string OldLock = "X-WOPI-OldLock"; - public const string LockFailureReason = "X-WOPI-LockFailureReason"; - public const string LockedByOtherInterface = "X-WOPI-LockedByOtherInterface"; + public const string LOCK = "X-WOPI-Lock"; + public const string OLD_LOCK = "X-WOPI-OldLock"; + public const string LOCK_FAILURE_REASON = "X-WOPI-LockFailureReason"; + public const string LOCKED_BY_OTHER_INTERFACE = "X-WOPI-LockedByOtherInterface"; - public const string SuggestedTarget = "X-WOPI-SuggestedTarget"; - public const string RelativeTarget = "X-WOPI-RelativeTarget"; - public const string OverwriteRelativeTarget = "X-WOPI-OverwriteRelativeTarget"; + public const string SUGGESTED_TARGET = "X-WOPI-SuggestedTarget"; + public const string RELATIVE_TARGET = "X-WOPI-RelativeTarget"; + public const string OVERWRITE_RELATIVE_TARGET = "X-WOPI-OverwriteRelativeTarget"; - public const string CorrelationId = "X-WOPI-CorrelationID"; + public const string CORRELATION_ID = "X-WOPI-CorrelationID"; - public const string MaxExpectedSize = "X-WOPI-MaxExpectedSize"; + public const string MAX_EXPECTED_SIZE = "X-WOPI-MaxExpectedSize"; - public const string WopiSrc = "X-WOPI-WopiSrc"; + public const string WOPI_SRC = "X-WOPI-WopiSrc"; - public const string EcosystemOperation = "X-WOPI-EcosystemOperation"; + public const string ECOSYSTEM_OPERATION = "X-WOPI-EcosystemOperation"; } } diff --git a/WopiHost.Core/WopiOverrideHeaderAttribute.cs b/WopiHost.Core/WopiOverrideHeaderAttribute.cs index e6219a6..2282501 100644 --- a/WopiHost.Core/WopiOverrideHeaderAttribute.cs +++ b/WopiHost.Core/WopiOverrideHeaderAttribute.cs @@ -2,7 +2,7 @@ { public class WopiOverrideHeaderAttribute : HttpHeaderAttribute { - public WopiOverrideHeaderAttribute(string[] values) : base(WopiHeaders.WopiOverride, values) + public WopiOverrideHeaderAttribute(string[] values) : base(WopiHeaders.WOPI_OVERRIDE, values) { } } diff --git a/WopiHost.Discovery.Tests/WopiDiscovererTests.cs b/WopiHost.Discovery.Tests/WopiDiscovererTests.cs index 87d55f9..9d0b26d 100644 --- a/WopiHost.Discovery.Tests/WopiDiscovererTests.cs +++ b/WopiHost.Discovery.Tests/WopiDiscovererTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; using System.IO; using WopiHost.Discovery.Enumerations; using Xunit; @@ -8,9 +8,9 @@ namespace WopiHost.Discovery.Tests public class WopiDiscovererTests { private WopiDiscoverer _wopiDiscoverer; - private const string XML_OOS_2016 = "OOS2016_discovery.xml"; - private const string XML_OWA_2013 = "OWA2013_discovery.xml"; - private const string XML_OO_2019 = "OO2019_discovery.xml"; + private const string XmlOos2016 = "OOS2016_discovery.xml"; + private const string XmlOwa2013 = "OWA2013_discovery.xml"; + private const string XmlOo2019 = "OO2019_discovery.xml"; public WopiDiscovererTests() { @@ -23,9 +23,9 @@ private void InitDiscoverer(string fileName, NetZoneEnum netZone = NetZoneEnum.A } [Theory] - [InlineData(NetZoneEnum.ExternalHttps, "xlsm", WopiActionEnum.LegacyWebService, "https://excel.officeapps.live.com/x/_vti_bin/excelserviceinternal.asmx?", XML_OO_2019)] - [InlineData(NetZoneEnum.InternalHttp, "xlsx", WopiActionEnum.MobileView, "http://owaserver/x/_layouts/xlviewerinternal.aspx?", XML_OO_2019)] - [InlineData(NetZoneEnum.InternalHttp, "ods", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XML_OWA_2013)] + [InlineData(NetZoneEnum.ExternalHttps, "xlsm", WopiActionEnum.LegacyWebService, "https://excel.officeapps.live.com/x/_vti_bin/excelserviceinternal.asmx?", XmlOo2019)] + [InlineData(NetZoneEnum.InternalHttp, "xlsx", WopiActionEnum.MobileView, "http://owaserver/x/_layouts/xlviewerinternal.aspx?", XmlOo2019)] + [InlineData(NetZoneEnum.InternalHttp, "ods", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XmlOwa2013)] public async void NetZoneTests(NetZoneEnum netZone, string extension, WopiActionEnum action, string expectedValue, string fileName) { // Arrange @@ -39,8 +39,8 @@ public async void NetZoneTests(NetZoneEnum netZone, string extension, WopiAction } [Theory] - [InlineData("xlsx", XML_OOS_2016)] - [InlineData("docx", XML_OOS_2016)] + [InlineData("xlsx", XmlOos2016)] + [InlineData("docx", XmlOos2016)] public async void SupportedExtension(string extension, string fileName) { // Arrange @@ -54,8 +54,8 @@ public async void SupportedExtension(string extension, string fileName) } [Theory] - [InlineData("html", XML_OOS_2016)] - [InlineData("txt", XML_OOS_2016)] + [InlineData("html", XmlOos2016)] + [InlineData("txt", XmlOos2016)] public async void NonSupportedExtension(string extension, string fileName) { // Arrange @@ -69,8 +69,8 @@ public async void NonSupportedExtension(string extension, string fileName) } [Theory] - [InlineData("html", XML_OOS_2016)] - [InlineData("txt", XML_OOS_2016)] + [InlineData("html", XmlOos2016)] + [InlineData("txt", XmlOos2016)] public async void NonSupportedExtensionWithAction(string extension, string fileName) { // Arrange @@ -84,8 +84,8 @@ public async void NonSupportedExtensionWithAction(string extension, string fileN } [Theory] - [InlineData("pptx", XML_OOS_2016)] - [InlineData("docx", XML_OOS_2016)] + [InlineData("pptx", XmlOos2016)] + [InlineData("docx", XmlOos2016)] public async void SupportedExtensionWithAction(string extension, string fileName) { // Arrange @@ -99,8 +99,8 @@ public async void SupportedExtensionWithAction(string extension, string fileName } [Theory] - [InlineData("html", XML_OOS_2016)] - [InlineData("txt", XML_OOS_2016)] + [InlineData("html", XmlOos2016)] + [InlineData("txt", XmlOos2016)] public async void NonSupportedExtensionCobalt(string extension, string fileName) { // Arrange @@ -114,8 +114,8 @@ public async void NonSupportedExtensionCobalt(string extension, string fileName) } [Theory] - [InlineData("docx", XML_OWA_2013, true)] - [InlineData("docx", XML_OOS_2016, false)] + [InlineData("docx", XmlOwa2013, true)] + [InlineData("docx", XmlOos2016, false)] public async void SupportedExtensionCobalt(string extension, string fileName, bool expected) { // Arrange @@ -129,10 +129,10 @@ public async void SupportedExtensionCobalt(string extension, string fileName, bo } [Theory] - [InlineData("xlsx", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XML_OWA_2013)] - [InlineData("docx", WopiActionEnum.Edit, "http://owaserver/we/wordeditorframe.aspx?", XML_OWA_2013)] - [InlineData("html", WopiActionEnum.Edit, null, XML_OWA_2013)] - [InlineData("txt", WopiActionEnum.Edit, null, XML_OWA_2013)] + [InlineData("xlsx", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XmlOwa2013)] + [InlineData("docx", WopiActionEnum.Edit, "http://owaserver/we/wordeditorframe.aspx?", XmlOwa2013)] + [InlineData("html", WopiActionEnum.Edit, null, XmlOwa2013)] + [InlineData("txt", WopiActionEnum.Edit, null, XmlOwa2013)] public async void UrlTemplateTests(string extension, WopiActionEnum action, string expectedValue, string fileName) { // Arrange @@ -146,10 +146,10 @@ public async void UrlTemplateTests(string extension, WopiActionEnum action, stri } [Theory] - [InlineData("xlsx", "Excel", XML_OOS_2016)] - [InlineData("docx", "Word", XML_OOS_2016)] - [InlineData("html", null, XML_OOS_2016)] - [InlineData("txt", null, XML_OOS_2016)] + [InlineData("xlsx", "Excel", XmlOos2016)] + [InlineData("docx", "Word", XmlOos2016)] + [InlineData("html", null, XmlOos2016)] + [InlineData("txt", null, XmlOos2016)] public async void AppNameTests(string extension, string expectedValue, string fileName) { // Arrange @@ -163,10 +163,10 @@ public async void AppNameTests(string extension, string expectedValue, string fi } [Theory] - [InlineData("xlsx", "http://owaserver/x/_layouts/resources/FavIcon_Excel.ico", XML_OOS_2016)] - [InlineData("docx", "http://owaserver/wv/resources/1033/FavIcon_Word.ico", XML_OOS_2016)] - [InlineData("html", null, XML_OOS_2016)] - [InlineData("txt", null, XML_OOS_2016)] + [InlineData("xlsx", "http://owaserver/x/_layouts/resources/FavIcon_Excel.ico", XmlOos2016)] + [InlineData("docx", "http://owaserver/wv/resources/1033/FavIcon_Word.ico", XmlOos2016)] + [InlineData("html", null, XmlOos2016)] + [InlineData("txt", null, XmlOos2016)] public async void FavIconTests(string extension, string expectedValue, string fileName) { // Arrange @@ -176,39 +176,39 @@ public async void FavIconTests(string extension, string expectedValue, string fi var result = await _wopiDiscoverer.GetApplicationFavIconAsync(extension); // Assert - Assert.Equal(expectedValue, result); + Assert.Equal(expectedValue != null ? new Uri(expectedValue) : null, result); } [Theory] - [InlineData("xlsx", WopiActionEnum.Edit, "update", XML_OWA_2013)] - [InlineData("docx", WopiActionEnum.Edit, "locks", XML_OWA_2013)] - [InlineData("docx", WopiActionEnum.Edit, "cobalt", XML_OWA_2013)] - [InlineData("docx", WopiActionEnum.Edit, "update", XML_OOS_2016)] - [InlineData("one", WopiActionEnum.View, "containers", XML_OWA_2013)] + [InlineData("xlsx", WopiActionEnum.Edit, "update", XmlOwa2013)] + [InlineData("docx", WopiActionEnum.Edit, "locks", XmlOwa2013)] + [InlineData("docx", WopiActionEnum.Edit, "cobalt", XmlOwa2013)] + [InlineData("docx", WopiActionEnum.Edit, "update", XmlOos2016)] + [InlineData("one", WopiActionEnum.View, "containers", XmlOwa2013)] public async void ActionRequirementsTests(string extension, WopiActionEnum action, string expectedValue, string fileName) { // Arrange InitDiscoverer(fileName); // Act - IEnumerable result = await _wopiDiscoverer.GetActionRequirementsAsync(extension, action); + var result = await _wopiDiscoverer.GetActionRequirementsAsync(extension, action); // Assert Assert.Contains(expectedValue, result); } [Theory] - [InlineData("xlsx", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XML_OWA_2013)] - [InlineData("one", WopiActionEnum.Edit, "locks", XML_OWA_2013)] - [InlineData("xlsx", WopiActionEnum.Edit, "locks", XML_OWA_2013)] - [InlineData("xlsx", WopiActionEnum.Edit, "cobalt", XML_OOS_2016)] + [InlineData("xlsx", WopiActionEnum.Edit, "http://owaserver/x/_layouts/xlviewerinternal.aspx?edit=1&", XmlOwa2013)] + [InlineData("one", WopiActionEnum.Edit, "locks", XmlOwa2013)] + [InlineData("xlsx", WopiActionEnum.Edit, "locks", XmlOwa2013)] + [InlineData("xlsx", WopiActionEnum.Edit, "cobalt", XmlOos2016)] public async void ActionRequirementsNegativeTests(string extension, WopiActionEnum action, string expectedValue, string fileName) { // Arrange InitDiscoverer(fileName); // Act - IEnumerable result = await _wopiDiscoverer.GetActionRequirementsAsync(extension, action); + var result = await _wopiDiscoverer.GetActionRequirementsAsync(extension, action); // Assert Assert.DoesNotContain(expectedValue, result); diff --git a/WopiHost.Discovery/HttpDiscoveryFileProvider.cs b/WopiHost.Discovery/HttpDiscoveryFileProvider.cs index b652fa5..cdb9c5c 100644 --- a/WopiHost.Discovery/HttpDiscoveryFileProvider.cs +++ b/WopiHost.Discovery/HttpDiscoveryFileProvider.cs @@ -1,36 +1,39 @@ -using System.IO; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; using System.Xml.Linq; namespace WopiHost.Discovery { + /// + /// A discovery file provider that loads the discovery file from a WOPI client over HTTP. + /// public class HttpDiscoveryFileProvider : IDiscoveryFileProvider { + private readonly HttpClient _httpClient; private XElement _discoveryXml; - private readonly string _wopiClientUrl; - public HttpDiscoveryFileProvider(string wopiClientUrl) + /// + /// Creates an instance of a discovery file provider that loads the discovery file from a WOPI client over HTTP. + /// + /// An HTTP client with a configured to point to a WOPI client. + public HttpDiscoveryFileProvider(HttpClient httpClient) { - _wopiClientUrl = wopiClientUrl; + _httpClient = httpClient; } + /// public async Task GetDiscoveryXmlAsync() { if (_discoveryXml == null) { try { - Stream stream; - using (HttpClient client = new HttpClient()) - { - stream = await client.GetStreamAsync(_wopiClientUrl + "/hosting/discovery"); - } + var stream = await _httpClient.GetStreamAsync("/hosting/discovery"); _discoveryXml = XElement.Load(stream); } catch (HttpRequestException e) { - throw new DiscoveryException($"There was a problem retrieving the discovery file. Please check availability of the WOPI Client at '{_wopiClientUrl}'.", e); + throw new DiscoveryException($"There was a problem retrieving the discovery file. Please check availability of the WOPI Client at '{_httpClient.BaseAddress}'.", e); } } return _discoveryXml; diff --git a/WopiHost.Discovery/IDiscoverer.cs b/WopiHost.Discovery/IDiscoverer.cs index 984dede..e82801f 100644 --- a/WopiHost.Discovery/IDiscoverer.cs +++ b/WopiHost.Discovery/IDiscoverer.cs @@ -1,16 +1,66 @@ +using System; using System.Collections.Generic; using System.Threading.Tasks; using WopiHost.Discovery.Enumerations; namespace WopiHost.Discovery { + /// + /// Provides information about the capabilities of a WOPI client. + /// public interface IDiscoverer { + /// + /// Gets the URL template for the given file extension and action. + /// + /// File extension to get the URL for (without the leading dot). + /// Action to be performed with the file. + /// URL template with query parameter placeholders that need to be resolved. Task GetUrlTemplateAsync(string extension, WopiActionEnum action); + + /// + /// Determines whether files with the given extension are supported by the WOPI client. + /// + /// File extension to evaluate (without the leading dot). + /// True if files with the extension can be handled by the WOPI client. Task SupportsExtensionAsync(string extension); + + /// + /// Determines whether the action is supported for files with the given extension. + /// + /// File extension in question (without the leading dot). + /// Action to be evaluated. + /// True if the action is supported for the given file extension. Task SupportsActionAsync(string extension, WopiActionEnum action); + + /// + /// Gets WOPI host requirements for the combination of action and file extension. + /// + /// File extension to evaluate (without the leading dot). + /// WOPI action to evaluate. + /// A collection of requirements as strings. Task> GetActionRequirementsAsync(string extension, WopiActionEnum action); - Task RequiresCobaltAsync(string extension, WopiActionEnum action); + + /// + /// Determines if files with the given extension require MS-FSSHTTP (Cobalt) to be implemented in order to support the given action. + /// + /// File extension to consider (without the leading dot). + /// WOPI action to consider. + /// True if MS-FSSHTTP (Cobalt) is required for the combination of action and file extension. + Task RequiresCobaltAsync(string extension, WopiActionEnum action); //TODO: convert to an extension method (remove from interface) + + /// + /// Gets the name of the application that handles files with the given extension. + /// + /// File extension to get the app name for (without the leading dot). + /// Name of the app. Task GetApplicationNameAsync(string extension); - } + + /// + /// Gets the icon of the application that handles files with the given extension. + /// + /// File extension to get the icon for (without the leading dot). + /// Icon of the app. + Task GetApplicationFavIconAsync(string extension); + } } \ No newline at end of file diff --git a/WopiHost.Discovery/IDiscoveryFileProvider.cs b/WopiHost.Discovery/IDiscoveryFileProvider.cs index 9032cc9..77d0624 100644 --- a/WopiHost.Discovery/IDiscoveryFileProvider.cs +++ b/WopiHost.Discovery/IDiscoveryFileProvider.cs @@ -3,8 +3,16 @@ namespace WopiHost.Discovery { - public interface IDiscoveryFileProvider - { - Task GetDiscoveryXmlAsync(); - } + /// + /// Provides access to the WOPI discovery XML file. + /// https://wopi.readthedocs.io/en/latest/discovery.html + /// + public interface IDiscoveryFileProvider + { + /// + /// Gets an object representation of an XML file representing the capabilities of a WOPI client. + /// + /// An object representation of WOPI discovery XML file. + Task GetDiscoveryXmlAsync(); + } } \ No newline at end of file diff --git a/WopiHost.Discovery/WopiDiscoverer.cs b/WopiHost.Discovery/WopiDiscoverer.cs index 2d90e7b..f47e49f 100644 --- a/WopiHost.Discovery/WopiDiscoverer.cs +++ b/WopiHost.Discovery/WopiDiscoverer.cs @@ -7,19 +7,22 @@ namespace WopiHost.Discovery { + /// public class WopiDiscoverer : IDiscoverer { - private const string ELEMENT_NET_ZONE = "net-zone"; - private const string ELEMENT_APP = "app"; - private const string ELEMENT_ACTION = "action"; - private const string ATTR_NET_ZONE_NAME = "name"; - private const string ATTR_ACTION_EXTENSION = "ext"; - private const string ATTR_ACTION_NAME = "name"; - private const string ATTR_ACTION_URL = "urlsrc"; - private const string ATTR_ACTION_REQUIRES = "requires"; - private const string ATTR_APP_NAME = "name"; - private const string ATTR_APP_FAVICON = "favIconUrl"; - private const string ATTR_VAL_COBALT = "cobalt"; + private const string ElementNetZone = "net-zone"; + private const string ElementApp = "app"; + private const string ElementAction = "action"; + private const string AttrNetZoneName = "name"; + private const string AttrActionExtension = "ext"; + private const string AttrActionName = "name"; + private const string AttrActionUrl = "urlsrc"; + private const string AttrActionRequires = "requires"; + private const string AttrAppName = "name"; + private const string AttrAppFavicon = "favIconUrl"; + private const string AttrValCobalt = "cobalt"; + + private IEnumerable _apps; private IDiscoveryFileProvider DiscoveryFileProvider { get; } @@ -34,74 +37,86 @@ public WopiDiscoverer(IDiscoveryFileProvider discoveryFileProvider, NetZoneEnum private async Task> GetAppsAsync() { - return (await DiscoveryFileProvider.GetDiscoveryXmlAsync()) - .Elements(ELEMENT_NET_ZONE) - .Where(ValidateNetZone) - .Elements(ELEMENT_APP); + if (_apps == null) + { + _apps = (await DiscoveryFileProvider.GetDiscoveryXmlAsync()) + .Elements(ElementNetZone) + .Where(ValidateNetZone) + .Elements(ElementApp); + } + + return _apps; } private bool ValidateNetZone(XElement e) { if (NetZone != NetZoneEnum.Any) { - var netZoneString = (string)e.Attribute(ATTR_NET_ZONE_NAME); + var netZoneString = (string)e.Attribute(AttrNetZoneName); netZoneString = netZoneString.Replace("-", "", StringComparison.InvariantCulture); - bool success = Enum.TryParse(netZoneString, true, out NetZoneEnum netZone); + var success = Enum.TryParse(netZoneString, true, out NetZoneEnum netZone); return success && (netZone == NetZone); } return true; } + /// public async Task SupportsExtensionAsync(string extension) { var query = (await GetAppsAsync()).Elements() - .FirstOrDefault(e => (string)e.Attribute(ATTR_ACTION_EXTENSION) == extension); + .FirstOrDefault(e => (string)e.Attribute(AttrActionExtension) == extension); return query != null; } + /// public async Task SupportsActionAsync(string extension, WopiActionEnum action) { - string actionString = action.ToString().ToLowerInvariant(); + var actionString = action.ToString().ToLowerInvariant(); - var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(ATTR_ACTION_EXTENSION) == extension && (string)e.Attribute(ATTR_ACTION_NAME).Value.ToLowerInvariant() == actionString); + var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(AttrActionExtension) == extension && (string)e.Attribute(AttrActionName).Value.ToLowerInvariant() == actionString); return query.Any(); } + /// public async Task> GetActionRequirementsAsync(string extension, WopiActionEnum action) { - string actionString = action.ToString().ToLowerInvariant(); + var actionString = action.ToString().ToLowerInvariant(); - var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(ATTR_ACTION_EXTENSION) == extension && (string)e.Attribute(ATTR_ACTION_NAME).Value.ToLowerInvariant() == actionString).Select(e => e.Attribute(ATTR_ACTION_REQUIRES).Value.Split(',')); + var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(AttrActionExtension) == extension && (string)e.Attribute(AttrActionName).Value.ToLowerInvariant() == actionString).Select(e => e.Attribute(AttrActionRequires).Value.Split(',')); return query.FirstOrDefault(); } + /// public async Task RequiresCobaltAsync(string extension, WopiActionEnum action) { var requirements = await GetActionRequirementsAsync(extension, action); - return requirements != null && requirements.Contains(ATTR_VAL_COBALT); + return requirements != null && requirements.Contains(AttrValCobalt); } + /// public async Task GetUrlTemplateAsync(string extension, WopiActionEnum action) { - string actionString = action.ToString().ToLowerInvariant(); - var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(ATTR_ACTION_EXTENSION) == extension && e.Attribute(ATTR_ACTION_NAME).Value.ToLowerInvariant() == actionString).Select(e => e.Attribute(ATTR_ACTION_URL).Value); + var actionString = action.ToString().ToLowerInvariant(); + var query = (await GetAppsAsync()).Elements().Where(e => (string)e.Attribute(AttrActionExtension) == extension && e.Attribute(AttrActionName).Value.ToLowerInvariant() == actionString).Select(e => e.Attribute(AttrActionUrl).Value); return query.FirstOrDefault(); } + /// public async Task GetApplicationNameAsync(string extension) { - var query = (await GetAppsAsync()).Where(e => e.Descendants(ELEMENT_ACTION).Any(d => (string)d.Attribute(ATTR_ACTION_EXTENSION) == extension)).Select(e => e.Attribute(ATTR_APP_NAME).Value); + var query = (await GetAppsAsync()).Where(e => e.Descendants(ElementAction).Any(d => (string)d.Attribute(AttrActionExtension) == extension)).Select(e => e.Attribute(AttrAppName).Value); return query.FirstOrDefault(); } - public async Task GetApplicationFavIconAsync(string extension) + /// + public async Task GetApplicationFavIconAsync(string extension) { - var query = (await GetAppsAsync()).Where(e => e.Descendants(ELEMENT_ACTION).Any(d => (string)d.Attribute(ATTR_ACTION_EXTENSION) == extension)).Select(e => e.Attribute(ATTR_APP_FAVICON).Value); - - return query.FirstOrDefault(); + var query = (await GetAppsAsync()).Where(e => e.Descendants(ElementAction).Any(d => (string)d.Attribute(AttrActionExtension) == extension)).Select(e => e.Attribute(AttrAppFavicon).Value); + var result = query.FirstOrDefault(); + return result != null ? new Uri(result) : null; } } } diff --git a/WopiHost.FileSystemProvider/WopiFile.cs b/WopiHost.FileSystemProvider/WopiFile.cs index 9cb8b03..fc194ef 100644 --- a/WopiHost.FileSystemProvider/WopiFile.cs +++ b/WopiHost.FileSystemProvider/WopiFile.cs @@ -11,15 +11,15 @@ public class WopiFile : IWopiFile { public string Identifier { get; } - private FileInfo fileInfo; + private FileInfo _fileInfo; - private FileVersionInfo fileVersionInfo; + private FileVersionInfo _fileVersionInfo; protected string FilePath { get; set; } - protected FileInfo FileInfo => fileInfo ?? (fileInfo = new FileInfo(FilePath)); + protected FileInfo FileInfo => _fileInfo ??= new FileInfo(FilePath); - protected FileVersionInfo FileVersionInfo => fileVersionInfo ?? (fileVersionInfo = FileVersionInfo.GetVersionInfo(FilePath)); + protected FileVersionInfo FileVersionInfo => _fileVersionInfo ??= FileVersionInfo.GetVersionInfo(FilePath); /// public bool Exists => FileInfo.Exists; diff --git a/WopiHost.FileSystemProvider/WopiFileSystemProvider.cs b/WopiHost.FileSystemProvider/WopiFileSystemProvider.cs index 1fee906..2764c17 100644 --- a/WopiHost.FileSystemProvider/WopiFileSystemProvider.cs +++ b/WopiHost.FileSystemProvider/WopiFileSystemProvider.cs @@ -13,14 +13,14 @@ namespace WopiHost.FileSystemProvider /// public class WopiFileSystemProvider : IWopiStorageProvider { - public WopiFileSystemProviderOptions FileSystemProviderOptions { get; } + private WopiFileSystemProviderOptions FileSystemProviderOptions { get; } - private readonly string ROOT_PATH = @".\"; + private const string _rootPath = @".\"; /// /// Reference to the root container. /// - public IWopiFolder RootContainerPointer => new WopiFolder(ROOT_PATH, EncodeIdentifier(ROOT_PATH)); + public IWopiFolder RootContainerPointer => new WopiFolder(_rootPath, EncodeIdentifier(_rootPath)); protected string WopiRootPath => FileSystemProviderOptions.RootPath; @@ -33,18 +33,13 @@ public class WopiFileSystemProvider : IWopiStorageProvider public WopiFileSystemProvider(IHostEnvironment env, IConfiguration configuration) { - if (env is null) - { - throw new ArgumentNullException(nameof(env)); - } - if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } - HostEnvironment = env; - FileSystemProviderOptions = configuration.GetSection(WopiConfigurationSections.STORAGE_OPTIONS).Get(); + HostEnvironment = env ?? throw new ArgumentNullException(nameof(env)); + FileSystemProviderOptions = configuration.GetSection(WopiConfigurationSections.STORAGE_OPTIONS).Get(); //TODO: rework } /// @@ -53,7 +48,7 @@ public WopiFileSystemProvider(IHostEnvironment env, IConfiguration configuration /// A base64-encoded file path. public IWopiFile GetWopiFile(string identifier) { - string filePath = DecodeIdentifier(identifier); + var filePath = DecodeIdentifier(identifier); return new WopiFile(Path.Combine(WopiAbsolutePath, filePath), identifier); } @@ -63,7 +58,7 @@ public IWopiFile GetWopiFile(string identifier) /// A base64-encoded folder path. public IWopiFolder GetWopiContainer(string identifier = "") { - string folderPath = DecodeIdentifier(identifier); + var folderPath = DecodeIdentifier(identifier); return new WopiFolder(Path.Combine(WopiAbsolutePath, folderPath), identifier); } @@ -73,12 +68,12 @@ public IWopiFolder GetWopiContainer(string identifier = "") /// A base64-encoded folder path. public List GetWopiFiles(string identifier = "") { - string folderPath = DecodeIdentifier(identifier); - List files = new List(); - foreach (string path in Directory.GetFiles(Path.Combine(WopiAbsolutePath, folderPath))) //TODO Directory.Enumerate... + var folderPath = DecodeIdentifier(identifier); + var files = new List(); + foreach (var path in Directory.GetFiles(Path.Combine(WopiAbsolutePath, folderPath))) //TODO Directory.Enumerate... { - string filePath = Path.Combine(folderPath, Path.GetFileName(path)); - string fileId = EncodeIdentifier(filePath); + var filePath = Path.Combine(folderPath, Path.GetFileName(path)); + var fileId = EncodeIdentifier(filePath); files.Add(GetWopiFile(fileId)); } return files; @@ -90,24 +85,24 @@ public List GetWopiFiles(string identifier = "") /// A base64-encoded folder path. public List GetWopiContainers(string identifier = "") { - string folderPath = DecodeIdentifier(identifier); - List folders = new List(); - foreach (string directory in Directory.GetDirectories(Path.Combine(WopiAbsolutePath, folderPath))) + var folderPath = DecodeIdentifier(identifier); + var folders = new List(); + foreach (var directory in Directory.GetDirectories(Path.Combine(WopiAbsolutePath, folderPath))) { var subfolderPath = "." + directory.Remove(0, directory.LastIndexOf(Path.DirectorySeparatorChar)); - string folderId = EncodeIdentifier(subfolderPath); + var folderId = EncodeIdentifier(subfolderPath); folders.Add(GetWopiContainer(folderId)); } return folders; } - private string DecodeIdentifier(string identifier) + private static string DecodeIdentifier(string identifier) { var bytes = Convert.FromBase64String(identifier); return Encoding.UTF8.GetString(bytes); } - private string EncodeIdentifier(string path) + private static string EncodeIdentifier(string path) { var bytes = Encoding.UTF8.GetBytes(path); return Convert.ToBase64String(bytes); diff --git a/WopiHost.FileSystemProvider/WopiFolder.cs b/WopiHost.FileSystemProvider/WopiFolder.cs index 919c516..8dd2a53 100644 --- a/WopiHost.FileSystemProvider/WopiFolder.cs +++ b/WopiHost.FileSystemProvider/WopiFolder.cs @@ -5,11 +5,11 @@ namespace WopiHost.FileSystemProvider { public class WopiFolder : IWopiFolder { - private DirectoryInfo folderInfo; + private DirectoryInfo _folderInfo; protected string Path { get; set; } - protected DirectoryInfo FolderInfo => folderInfo ?? (folderInfo = new DirectoryInfo(Path)); + protected DirectoryInfo FolderInfo => _folderInfo ??= new DirectoryInfo(Path); public string Name => FolderInfo.Name; public string Identifier { get; } diff --git a/WopiHost.FileSystemProvider/WopiSecurityHandler.cs b/WopiHost.FileSystemProvider/WopiSecurityHandler.cs index d438d1f..4dd6d9f 100644 --- a/WopiHost.FileSystemProvider/WopiSecurityHandler.cs +++ b/WopiHost.FileSystemProvider/WopiSecurityHandler.cs @@ -11,7 +11,7 @@ namespace WopiHost.FileSystemProvider { public class WopiSecurityHandler : IWopiSecurityHandler { - private readonly JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); private SymmetricSecurityKey _key = null; private SymmetricSecurityKey Key @@ -32,7 +32,7 @@ private SymmetricSecurityKey Key } //TODO: abstract - private readonly Dictionary UserDatabase = new Dictionary + private readonly Dictionary _userDatabase = new Dictionary { {"Anonymous", new ClaimsPrincipal( new ClaimsIdentity(new List @@ -42,14 +42,14 @@ private SymmetricSecurityKey Key new Claim(ClaimTypes.Email, "anonymous@domain.tld"), //TDOO: this needs to be done per file - new Claim(WopiClaimTypes.UserPermissions, (WopiUserPermissions.UserCanWrite | WopiUserPermissions.UserCanRename | WopiUserPermissions.UserCanAttend | WopiUserPermissions.UserCanPresent).ToString()) + new Claim(WopiClaimTypes.USER_PERMISSIONS, (WopiUserPermissions.UserCanWrite | WopiUserPermissions.UserCanRename | WopiUserPermissions.UserCanAttend | WopiUserPermissions.UserCanPresent).ToString()) }) ) } }; public SecurityToken GenerateAccessToken(string userId, string resourceId) { - var user = UserDatabase[userId]; + var user = _userDatabase[userId]; var tokenDescriptor = new SecurityTokenDescriptor { @@ -58,7 +58,7 @@ public SecurityToken GenerateAccessToken(string userId, string resourceId) SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.HmacSha256) }; - return tokenHandler.CreateToken(tokenDescriptor); + return _tokenHandler.CreateToken(tokenDescriptor); } public ClaimsPrincipal GetPrincipal(string tokenString) @@ -78,7 +78,7 @@ public ClaimsPrincipal GetPrincipal(string tokenString) try { // Try to validate the token - var principal = tokenHandler.ValidateToken(tokenString, tokenValidation, out SecurityToken token); + var principal = _tokenHandler.ValidateToken(tokenString, tokenValidation, out var token); return principal; } catch (Exception ex) @@ -99,7 +99,7 @@ public bool IsAuthorized(ClaimsPrincipal principal, string resourceId, WopiAutho /// public string WriteToken(SecurityToken token) { - return tokenHandler.WriteToken(token); + return _tokenHandler.WriteToken(token); } } } diff --git a/WopiHost.Url.Tests/CollectionExtensionsTests.cs b/WopiHost.Url.Tests/CollectionExtensionsTests.cs index 8e4d3dd..b657b76 100644 --- a/WopiHost.Url.Tests/CollectionExtensionsTests.cs +++ b/WopiHost.Url.Tests/CollectionExtensionsTests.cs @@ -9,7 +9,7 @@ public class CollectionExtensionsTests public void MergeWorksWithNulls() { // Arrange - Dictionary full = new Dictionary(); + var full = new Dictionary(); Dictionary empty = null; // Act @@ -25,8 +25,8 @@ public void MergeWorksWithNulls() public void MergeTwoDictionaries() { // Arrange - Dictionary a = new Dictionary { { "A", "B"}, { "C", "D" } }; - Dictionary b = new Dictionary { { "G", "H" }, { "I", "J" } }; + var a = new Dictionary { { "A", "B"}, { "C", "D" } }; + var b = new Dictionary { { "G", "H" }, { "I", "J" } }; // Act var result = a.Merge(b); diff --git a/WopiHost.Url.Tests/WopiUrlGeneratorTests.cs b/WopiHost.Url.Tests/WopiUrlGeneratorTests.cs index 20e59d0..6aa2465 100644 --- a/WopiHost.Url.Tests/WopiUrlGeneratorTests.cs +++ b/WopiHost.Url.Tests/WopiUrlGeneratorTests.cs @@ -38,7 +38,7 @@ public async void UrlWithoutAdditionalSettings(string extension, string wopiFile public async void UrlWithAdditionalSettings(string extension, string wopiFileUrl, WopiActionEnum action, string expectedValue) { // Arrange - var settings = new WopiUrlSettings { UI_LLCC = new CultureInfo("en-US") }; + var settings = new WopiUrlSettings { UiLlcc = new CultureInfo("en-US") }; var urlGenerator = new WopiUrlBuilder(_discoverer, settings); // Act diff --git a/WopiHost.Url/WopiUrlBuilder.cs b/WopiHost.Url/WopiUrlBuilder.cs index 7008079..b71d15f 100644 --- a/WopiHost.Url/WopiUrlBuilder.cs +++ b/WopiHost.Url/WopiUrlBuilder.cs @@ -14,7 +14,7 @@ namespace WopiHost.Url /// public class WopiUrlBuilder { - private readonly IDiscoverer WopiDiscoverer; + private readonly IDiscoverer _wopiDiscoverer; public WopiUrlSettings UrlSettings { get; } @@ -26,7 +26,7 @@ public class WopiUrlBuilder /// Additional settings influencing behavior of the WOPI client. public WopiUrlBuilder(IDiscoverer discoverer, WopiUrlSettings urlSettings = null) { - WopiDiscoverer = discoverer; + _wopiDiscoverer = discoverer; UrlSettings = urlSettings; } @@ -41,7 +41,7 @@ public WopiUrlBuilder(IDiscoverer discoverer, WopiUrlSettings urlSettings = null public async Task GetFileUrlAsync(string extension, string wopiFileUrl, WopiActionEnum action, WopiUrlSettings urlSettings = null) { var combinedUrlSettings = new WopiUrlSettings(urlSettings.Merge(UrlSettings)); - var template = await WopiDiscoverer.GetUrlTemplateAsync(extension, action); + var template = await _wopiDiscoverer.GetUrlTemplateAsync(extension, action); if (!string.IsNullOrEmpty(template)) { // Resolve optional parameters @@ -56,9 +56,9 @@ public async Task GetFileUrlAsync(string extension, string wopiFileUrl, return null; } - private string ResolveOptionalParameter(string name, string value, WopiUrlSettings urlSettings) + private static string ResolveOptionalParameter(string name, string value, WopiUrlSettings urlSettings) { - if (urlSettings.TryGetValue(value, out string param)) + if (urlSettings.TryGetValue(value, out var param)) { return name + "=" + Uri.EscapeDataString(param) + "&"; } diff --git a/WopiHost.Url/WopiUrlSettings.cs b/WopiHost.Url/WopiUrlSettings.cs index a2ab197..e887f9c 100644 --- a/WopiHost.Url/WopiUrlSettings.cs +++ b/WopiHost.Url/WopiUrlSettings.cs @@ -12,101 +12,101 @@ public class WopiUrlSettings : Dictionary /// /// Indicates that the WOPI server MAY include the preferred UI language in the format described in [RFC1766]. /// - public CultureInfo UI_LLCC + public CultureInfo UiLlcc { - get { return new CultureInfo(this["UI_LLCC"]); } - set { this["UI_LLCC"] = value.Name; } - } + get => new CultureInfo(this["UI_LLCC"]); + set => this["UI_LLCC"] = value.Name; + } /// /// Indicates that the WOPI server MAY include preferred data language in the format described in [RFC1766] for cases where language can affect data calculation. /// - public CultureInfo DC_LLCC + public CultureInfo DcLlcc { - get { return new CultureInfo(this["DC_LLCC"]); } - set { this["DC_LLCC"] = value.Name; } - } + get => new CultureInfo(this["DC_LLCC"]); + set => this["DC_LLCC"] = value.Name; + } /// /// Indicates that the WOPI server MAY include the value "true" to use the output of this action embedded in a web page. /// - public bool EMBEDDED + public bool Embedded { - get { return Convert.ToBoolean(this["EMBEDDED"]); } - set { this["EMBEDDED"] = value.ToString(); } - } + get => Convert.ToBoolean(this["EMBEDDED"], CultureInfo.InvariantCulture); + set => this["EMBEDDED"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "true" to prevent the attendee from navigating a file. For example, when using the attendee action (see st_wopi-action-values in section 3.1.5.1.1.2.3.1). /// - public bool DISABLE_ASYNC + public bool DisableAsync { - get { return Convert.ToBoolean(this["DISABLE_ASYNC"]); } - set { this["DISABLE_ASYNC"] = value.ToString(); } - } + get => Convert.ToBoolean(this["DISABLE_ASYNC"], CultureInfo.InvariantCulture); + set => this["DISABLE_ASYNC"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "true" to load a view of the document that does not create or join a broadcast session. This view looks and behaves like a regular broadcast frame. /// - public bool DISABLE_BROADCAST + public bool DisableBroadcast { - get { return Convert.ToBoolean(this["DISABLE_BROADCAST"]); } - set { this["DISABLE_BROADCAST"] = value.ToString(); } - } + get => Convert.ToBoolean(this["DISABLE_BROADCAST"], CultureInfo.InvariantCulture); + set => this["DISABLE_BROADCAST"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "true" to load the file type in full-screen mode. /// - public bool FULLSCREEN + public bool Fullscreen { - get { return Convert.ToBoolean(this["FULLSCREEN"]); } - set { this["FULLSCREEN"] = value.ToString(); } - } + get => Convert.ToBoolean(this["FULLSCREEN"], CultureInfo.InvariantCulture); + set => this["FULLSCREEN"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "true" to load the file type with a minimal user interface. /// - public bool RECORDING + public bool Recording { - get { return Convert.ToBoolean(this["RECORDING"]); } - set { this["RECORDING"] = value.ToString(); } - } + get => Convert.ToBoolean(this["RECORDING"], CultureInfo.InvariantCulture); + set => this["RECORDING"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include a value to designate the theme used. Current values are "1" to indicate a light-colored theme and "2" to indicate a darker colored theme. /// - public int THEME_ID + public int ThemeId { - get { return Convert.ToInt32(this["THEME_ID"]); } - set { this["THEME_ID"] = value.ToString(); } - } + get => Convert.ToInt32(this["THEME_ID"], CultureInfo.InvariantCulture); + set => this["THEME_ID"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "1" to indicate that the user is a business user. /// - public int BUSINESS_USER + public int BusinessUser { - get { return Convert.ToInt32(this["BUSINESS_USER"]); } - set { this["BUSINESS_USER"] = value.ToString(); } - } + get => Convert.ToInt32(this["BUSINESS_USER"], CultureInfo.InvariantCulture); + set => this["BUSINESS_USER"] = value.ToString(CultureInfo.InvariantCulture); + } /// /// Indicates that the WOPI server MAY include the value "1" to load a view of the document that does not create or join a chat session. /// - public int DISABLE_CHAT + public int DisableChat { - get { return Convert.ToInt32(this["DISABLE_CHAT"]); } - set { this["DISABLE_CHAT"] = value.ToString(); } - } + get => Convert.ToInt32(this["DISABLE_CHAT"], CultureInfo.InvariantCulture); + set => this["DISABLE_CHAT"] = value.ToString(CultureInfo.InvariantCulture); + } /// - /// Sorry, this documentation hasn’t been written yet. https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation/issues/52 + /// Sorry, this documentation hasn't been written yet. https://github.com/Microsoft/Office-Online-Test-Tools-and-Documentation/issues/52 /// - public string PERFSTATS + public string Perfstats { - get { return this["PERFSTATS"]; } - set { this["PERFSTATS"] = value; } - } + get => this["PERFSTATS"]; + set => this["PERFSTATS"] = value; + } /// /// This value is used to run the WOPI Validation application in different modes. @@ -115,11 +115,11 @@ public string PERFSTATS /// OfficeOnline: activates all tests necessary for Office Online integration. /// OfficeNativeClient: activates all tests necessary for Office for iOS integration. /// - public string VALIDATOR_TEST_CATEGORY + public string ValidatorTestCategory { - get { return this["VALIDATOR_TEST_CATEGORY"]; } - set { this["VALIDATOR_TEST_CATEGORY"] = value; } - } + get => this["VALIDATOR_TEST_CATEGORY"]; + set => this["VALIDATOR_TEST_CATEGORY"] = value; + } public WopiUrlSettings() { @@ -130,7 +130,7 @@ public WopiUrlSettings(IDictionary settings) { if (settings != null) { - foreach (KeyValuePair pair in settings) + foreach (var pair in settings) { Add(pair.Key, pair.Value); } diff --git a/WopiHost.Web/Controllers/HomeController.cs b/WopiHost.Web/Controllers/HomeController.cs index d802527..d838115 100644 --- a/WopiHost.Web/Controllers/HomeController.cs +++ b/WopiHost.Web/Controllers/HomeController.cs @@ -9,6 +9,8 @@ using WopiHost.FileSystemProvider; using WopiHost.Web.Models; using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; namespace WopiHost.Web.Controllers { @@ -17,25 +19,38 @@ public class HomeController : Controller private WopiUrlBuilder _urlGenerator; private IOptionsSnapshot WopiOptions { get; } + private IWopiStorageProvider StorageProvider { get; } + private IDiscoverer Discoverer { get; } - protected IWopiStorageProvider StorageProvider { get; set; } - - public WopiDiscoverer Discoverer => new WopiDiscoverer(new HttpDiscoveryFileProvider(WopiOptions.Value.ClientUrl)); //TODO: remove test culture value and load it from configuration SECTION - public WopiUrlBuilder UrlGenerator => _urlGenerator ?? (_urlGenerator = new WopiUrlBuilder(Discoverer, new WopiUrlSettings { UI_LLCC = new CultureInfo("en-US") })); + public WopiUrlBuilder UrlGenerator => _urlGenerator ??= new WopiUrlBuilder(Discoverer, new WopiUrlSettings { UiLlcc = new CultureInfo("en-US") }); - public HomeController(IOptionsSnapshot wopiOptions, IWopiStorageProvider storageProvider) + public HomeController(IOptionsSnapshot wopiOptions, IWopiStorageProvider storageProvider, IDiscoverer discoverer) { WopiOptions = wopiOptions; StorageProvider = storageProvider; + Discoverer = discoverer; } public async Task Index() { try { - return View(StorageProvider.GetWopiFiles(StorageProvider.RootContainerPointer.Identifier)); + var files = StorageProvider.GetWopiFiles(StorageProvider.RootContainerPointer.Identifier); + var fileViewModels = new List(); + foreach (var file in files) + { + fileViewModels.Add(new FileViewModel + { + FileId = file.Identifier, + FileName = file.Name, + SupportsEdit = await Discoverer.SupportsActionAsync(file.Extension, WopiActionEnum.Edit), + SupportsView = await Discoverer.SupportsActionAsync(file.Extension, WopiActionEnum.View), + IconUri = (await Discoverer.GetApplicationFavIconAsync(file.Extension)) ?? new Uri("file.ico", UriKind.Relative) + }); + } + return View(fileViewModels); } catch (DiscoveryException ex) { @@ -47,14 +62,15 @@ public async Task Index() } } - public async Task Detail(string id) + public async Task Detail(string id, string wopiAction) { - WopiSecurityHandler securityHandler = new WopiSecurityHandler(); + var actionEnum = Enum.Parse(wopiAction); + var securityHandler = new WopiSecurityHandler(); - IWopiFile file = StorageProvider.GetWopiFile(id); + var file = StorageProvider.GetWopiFile(id); var token = securityHandler.GenerateAccessToken("Anonymous", file.Identifier); - - + + ViewData["access_token"] = securityHandler.WriteToken(token); //TODO: fix //ViewData["access_token_ttl"] = //token.ValidTo @@ -63,7 +79,7 @@ public async Task Detail(string id) var extension = file.Extension.TrimStart('.'); - ViewData["urlsrc"] = await UrlGenerator.GetFileUrlAsync(extension, $"{WopiOptions.Value.HostUrl}/wopi/files/{id}", WopiActionEnum.Edit); + ViewData["urlsrc"] = await UrlGenerator.GetFileUrlAsync(extension, $"{WopiOptions.Value.HostUrl}/wopi/files/{id}", actionEnum); ViewData["favicon"] = await Discoverer.GetApplicationFavIconAsync(extension); return View(); } diff --git a/WopiHost.Web/Models/FileViewModel.cs b/WopiHost.Web/Models/FileViewModel.cs index ec41556..5d2522b 100644 --- a/WopiHost.Web/Models/FileViewModel.cs +++ b/WopiHost.Web/Models/FileViewModel.cs @@ -1,9 +1,13 @@ -namespace WopiHost.Web.Models +using System; + +namespace WopiHost.Web.Models { public class FileViewModel { public string FileId { get; set; } public string FileName { get; set; } - + public bool SupportsView { get; set; } + public bool SupportsEdit { get; set; } + public Uri IconUri { get; set; } } } diff --git a/WopiHost.Web/Startup.cs b/WopiHost.Web/Startup.cs index a7e6628..6f317d4 100644 --- a/WopiHost.Web/Startup.cs +++ b/WopiHost.Web/Startup.cs @@ -1,4 +1,4 @@ - +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Hosting; using WopiHost.Abstractions; +using WopiHost.Discovery; using WopiHost.FileSystemProvider; using WopiHost.Web.Models; @@ -43,8 +44,12 @@ public void ConfigureServices(IServiceCollection services) services.AddOptions(); services.Configure(Configuration.GetSection(WopiConfigurationSections.WOPI_ROOT)); + services.AddHttpClient(client => + { + client.BaseAddress = new Uri(Configuration[$"{WopiConfigurationSections.WOPI_ROOT}:ClientUrl"]); + }); + services.AddSingleton(); - //services.Configure() services.AddScoped(); services.AddLogging(loggingBuilder => diff --git a/WopiHost.Web/Views/Home/Index.cshtml b/WopiHost.Web/Views/Home/Index.cshtml index 2cda4aa..5080bbb 100644 --- a/WopiHost.Web/Views/Home/Index.cshtml +++ b/WopiHost.Web/Views/Home/Index.cshtml @@ -1,13 +1,19 @@ -@using WopiHost.Abstractions -@model IEnumerable +@using WopiHost.Discovery.Enumerations + +@model IEnumerable @{ ViewData["Title"] = "List of WOPI files"; }

Documents:

    - @foreach (var file in @Model) + @foreach (var file in Model) { -
  • @file.Name
  • +
  • +   + @file.FileName  + 👁️ + 🖊️ +
  • }
\ No newline at end of file diff --git a/WopiHost.Web/Views/Shared/_Layout.cshtml b/WopiHost.Web/Views/Shared/_Layout.cshtml index c1497df..cdf5579 100644 --- a/WopiHost.Web/Views/Shared/_Layout.cshtml +++ b/WopiHost.Web/Views/Shared/_Layout.cshtml @@ -32,6 +32,6 @@ - @RenderSection("scripts", required: false) +@await RenderSectionAsync("scripts", required: false) diff --git a/WopiHost.Web/Views/Shared/_LayoutWOPI.cshtml b/WopiHost.Web/Views/Shared/_LayoutWOPI.cshtml index ff0f6c8..5acd36f 100644 --- a/WopiHost.Web/Views/Shared/_LayoutWOPI.cshtml +++ b/WopiHost.Web/Views/Shared/_LayoutWOPI.cshtml @@ -32,6 +32,6 @@ @RenderBody() -@RenderSection("scripts", required: false) +@await RenderSectionAsync("scripts", required: false) \ No newline at end of file diff --git a/WopiHost.Web/wwwroot/css/site.css b/WopiHost.Web/wwwroot/css/site.css index fbb0e17..4d89a22 100644 --- a/WopiHost.Web/wwwroot/css/site.css +++ b/WopiHost.Web/wwwroot/css/site.css @@ -18,3 +18,12 @@ nav { display: inline; padding: 16px; } + +.disabledIcon { + -webkit-filter: grayscale(100%); + -moz-filter: grayscale(100%); + filter: grayscale(100%); + cursor: default; + pointer-events: none; + text-decoration: none; +} \ No newline at end of file diff --git a/WopiHost.Web/wwwroot/file.ico b/WopiHost.Web/wwwroot/file.ico new file mode 100644 index 0000000..f36f182 Binary files /dev/null and b/WopiHost.Web/wwwroot/file.ico differ