/// <summary> /// This is here purely as an example of how a host page needs to be rendered for it to be able to first post to Collabora and then have it /// relay back to this WOPI host to serve up and manage the actual file /// </summary> /// <param name="httpContext"></param> /// <returns></returns> private static async Task GetCollaboraHostPageAsync(HttpContext httpContext) { var cancellationToken = httpContext.RequestAborted; // Try to get the discovery document from the Collabora server. This tells us what document types are supported, but more importantly // the encrption keys it is using to sign the callback requests it sends to us. We will use these keys to assure non-reupidation/tampering // If we fail to load this file, we cannot continue to build the page we are return var wopiDiscoveryDocumentFactory = httpContext.RequestServices.GetRequiredService <IWopiDiscoveryDocumentFactory>(); var wopiDiscoveryDocument = await wopiDiscoveryDocumentFactory.CreateDocumentAsync(cancellationToken); if (wopiDiscoveryDocument.IsEmpty) { throw new ApplicationException("Unable to download the WOPI Discovery Document - remote host is either unavailable or returned non-success status code"); } // Identify the file that we want to view/edit by inspecting the incoming request for an id. var fileId = httpContext.Request.Query["file_id"].FirstOrDefault()?.Trim(); if (string.IsNullOrWhiteSpace(fileId)) { fileId = File.With("DF796179-DB2F-4A06-B4D5-AD7F012CC2CC", "2021-08-09T18:15:02.4214747Z"); } var wopiConfiguration = httpContext.RequestServices.GetRequiredService <IOptionsSnapshot <WopiConfiguration> >().Value; var hostFilesUrl = wopiConfiguration.HostFilesUrl; if (string.IsNullOrWhiteSpace(hostFilesUrl)) { return; } Debug.Assert(fileId is object); var wopiHostFileEndpointUrl = new Uri(Path.Combine(hostFilesUrl, fileId), UriKind.Absolute); var fileRepository = httpContext.RequestServices.GetRequiredService <IFileRepository>(); var fileMetadata = await fileRepository.GetMetadataAsync(fileId, cancellationToken); var fileExtension = fileMetadata.Extension; if (string.IsNullOrWhiteSpace(fileExtension)) { return; // TODO - Return appropriate status code to caller } var fileAction = "view"; // edit | view | etc (see comments in discoveryDoc.GetEndpointForAsync) var collaboraOnlineEndpoint = wopiDiscoveryDocument.GetEndpointForFileExtension(fileExtension, fileAction, wopiHostFileEndpointUrl); if (collaboraOnlineEndpoint is null || !collaboraOnlineEndpoint.IsAbsoluteUri) { return; // TODO - Return appropriate status code to caller } var sb = new StringBuilder(); // https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html#term-wopisrc // When running locally in DEBUG ... // https://127.0.0.1:9980/loleaflet/4aa2794/loleaflet.html? is the Collabora endpoint for this document type, pulled out of the discovery xml file hosted by Collabora // https://127.0.0.1:44355/wopi/files/<FILE_ID> is the url Collabora uses to callback to us to get the file information and contents // In Azure, the container will be mapped to port 80 in docker run command // TODO - Generate a token with a set TTL that is specific to the current user and file combination // This token will be sent back to us by Collabora by way of it verifying the request (it will be signed so we know it // comes from them and hasn't been tampered with outside of the servers) // For now, we'll just use a Guid var accessToken = Guid.NewGuid().ToString().Replace("-", string.Empty); // TODO - This is either going to have to be generated by MVCForum or somehow injected by it after a call to our API, // but given the need for input elements, it might be more appropriate for us to just generate the token and // return both it and the collabora endpoint that needs to be used, or MVCForum gets the discovery document itself // and generates a token we can later understand httpContext.Response.StatusCode = StatusCodes.Status200OK; sb.AppendLine($"<!doctype html>"); sb.AppendLine($"<html>"); sb.AppendLine($" <body>"); sb.AppendLine($" <form action=\"{collaboraOnlineEndpoint.AbsoluteUri}\" enctype =\"multipart/form-data\" method=\"post\">"); sb.AppendLine($" <input name=\"access_token\" value=\"{ accessToken }\" type=\"hidden\">"); sb.AppendLine($" <input type=\"submit\" value=\"Load Document (Collabora)\">"); sb.AppendLine($" </form>"); sb.AppendLine($" </body>"); sb.AppendLine($"</html>"); await httpContext.Response.WriteAsync(sb.ToString()); }
public async Task HandleAsync_ResolvesAndWritesFileCorrectlyToGivenStream(string fileName) { var cancellationToken = new CancellationToken(); var httpContext = new DefaultHttpContext(); var contentRootPath = Environment.CurrentDirectory; var filePath = Path.Combine(contentRootPath, "Files", fileName); Assert.IsTrue(System.IO.File.Exists(filePath), $"Expected the {fileName} file to be accessible in the test environment"); var fileInfo = new FileInfo(filePath); var fileBuffer = await System.IO.File.ReadAllBytesAsync(filePath, cancellationToken); using var responseBodyStream = new MemoryStream(fileBuffer.Length); httpContext.Response.Body = responseBodyStream; var fileRepository = new Moq.Mock <IFileRepository>(); var fileRepositoryInvoked = false; var services = new ServiceCollection(); services.AddScoped(sp => fileRepository.Object); httpContext.RequestServices = services.BuildServiceProvider(); var fileVersion = Guid.NewGuid().ToString(); using var algo = MD5.Create(); var contentHash = algo.ComputeHash(fileBuffer); var fileMetadata = new FileMetadata("title", "description", "group-name", fileVersion, "owner", fileName, fileInfo.Extension, (ulong)fileInfo.Length, "blobName", DateTimeOffset.UtcNow, Convert.ToBase64String(contentHash), FileStatus.Verified); var fileWriteDetails = new FileWriteDetails(fileVersion, "content-type", contentHash, (ulong)fileBuffer.Length, "content-encoding", "content-language", DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, fileMetadata); fileRepository. Setup(x => x.WriteToStreamAsync(Moq.It.IsAny <FileMetadata>(), Moq.It.IsAny <Stream>(), Moq.It.IsAny <CancellationToken>())). Callback(async(FileMetadata givenFileMetadata, Stream givenStream, CancellationToken givenCancellationToken) => { Assert.IsFalse(givenFileMetadata.IsEmpty); Assert.IsNotNull(givenStream); Assert.IsFalse(givenCancellationToken.IsCancellationRequested, "Expected the cancellation token to not be cancelled"); Assert.AreSame(responseBodyStream, givenStream, "Expected the SUT to as the repository to write the file to the stream it was asked to"); Assert.AreSame(fileName, givenFileMetadata.Name, "Expected the SUT to request the file from the repository whose name it was provided with"); Assert.AreSame(fileVersion, givenFileMetadata.Version, "Expected the SUT to request the file version from the repository that it was provided with"); Assert.AreEqual(cancellationToken, givenCancellationToken, "Expected the same cancellation token to propagate between service interfaces"); await givenStream.WriteAsync(fileBuffer, cancellationToken); await givenStream.FlushAsync(cancellationToken); fileRepositoryInvoked = true; }). Returns(Task.FromResult(fileWriteDetails)); fileRepository.Setup(x => x.GetMetadataAsync(Moq.It.IsAny <File>(), Moq.It.IsAny <CancellationToken>())).Returns(Task.FromResult(fileMetadata)); var accessToken = Guid.NewGuid().ToString(); var file = File.With(fileName, fileVersion); var getFileWopiRequest = GetFileWopiRequest.With(file, accessToken); await getFileWopiRequest.HandleAsync(httpContext, cancellationToken); Assert.IsTrue(fileRepositoryInvoked, "Expected the SUT to defer to the file repository with the correct parameters"); Assert.AreEqual(fileBuffer.Length, responseBodyStream.Length, "All bytes in the file should be written to the target stream"); Assert.IsTrue(httpContext.Response.Headers.ContainsKey("X-WOPI-ItemVersion"), "Expected the X-WOPI-ItemVersion header to have been written to the response"); Assert.IsNotNull(httpContext.Response.Headers["X-WOPI-ItemVersion"], "Expected the X-WOPI-ItemVersion header in the response to not be null"); }
public void With_DoesNotThrowIfAccessTokenIsNeitherNullNorWhitespace() { var file = File.With("file-name", "file-version"); GetFileWopiRequest.With(file, accessToken: "access-token"); }
public void With_ThrowsIfAccessTokenIsWhitespace() { var file = File.With("file-name", "file-version"); GetFileWopiRequest.With(file, accessToken: " "); }
public void With_ThrowsIfAccessTokenIsEmpty() { var file = File.With("file-name", "file-version"); GetFileWopiRequest.With(file, accessToken: string.Empty); }
public void With_ThrowsIfAccessTokenIsNull() { var file = File.With("file-name", "file-version"); GetFileWopiRequest.With(file, accessToken: default); }