Skip to content

Feature/199 PutUserInfo #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 22 additions & 39 deletions WOPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B692B58D
test\Directory.Packages.props = test\Directory.Packages.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost", "sample\WopiHost\WopiHost.csproj", "{21B81530-CC57-46F1-9524-08F984187DE2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Discovery", "src\WopiHost.Discovery\WopiHost.Discovery.csproj", "{84B896D2-1A87-4671-B583-92A6AF3645F0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Abstractions", "src\WopiHost.Abstractions\WopiHost.Abstractions.csproj", "{D3B3947E-94DD-4FCD-BDDC-4E579ACF9B5B}"
Expand All @@ -40,8 +38,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Url", "src\WopiHos
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.FileSystemProvider.Tests", "test\WopiHost.FileSystemProvider.Tests\WopiHost.FileSystemProvider.Tests.csproj", "{650A5A85-5956-491E-9312-5E25A27D1108}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Web", "sample\WopiHost.Web\WopiHost.Web.csproj", "{5C301182-6FB2-40C5-97ED-58B07A910BFF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Discovery.Tests", "test\WopiHost.Discovery.Tests\WopiHost.Discovery.Tests.csproj", "{4089410A-8A12-455C-862B-A4A3834C3100}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WopiHost.Url.Tests", "test\WopiHost.Url.Tests\WopiHost.Url.Tests.csproj", "{9FFB3975-2DE6-4944-92C7-7062C39BB8FA}"
Expand All @@ -59,29 +55,16 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.Validator", "sample\WopiHost.Validator\WopiHost.Validator.csproj", "{97442113-B773-4F71-8B60-EF5599AC135D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
sample\README.md = sample\README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wopi-docs", "wopi-docs", "{EA1A15F6-C3AF-4AAF-9A8B-4B7E053D6CEC}"
ProjectSection(SolutionItems) = preProject
sample\wopi-docs\test.docx = sample\wopi-docs\test.docx
sample\wopi-docs\test.html = sample\wopi-docs\test.html
sample\wopi-docs\test.pptx = sample\wopi-docs\test.pptx
sample\wopi-docs\test.wopitest = sample\wopi-docs\test.wopitest
sample\wopi-docs\test.xlsx = sample\wopi-docs\test.xlsx
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "subfolder", "subfolder", "{1CCBC8E4-5688-4F95-ADE8-31BF49064828}"
ProjectSection(SolutionItems) = preProject
sample\wopi-docs\subfolder\test2.docx = sample\wopi-docs\subfolder\test2.docx
sample\wopi-docs\subfolder\TextFile.txt = sample\wopi-docs\subfolder\TextFile.txt
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "subsubfolder", "subsubfolder", "{7EC2D8B4-42A4-40E2-AC9A-E4496DA57F4C}"
ProjectSection(SolutionItems) = preProject
sample\wopi-docs\subfolder\subsubfolder\SubSubTextFile.txt = sample\wopi-docs\subfolder\subsubfolder\SubSubTextFile.txt
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost", "sample\WopiHost\WopiHost.csproj", "{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WopiHost.Web", "sample\WopiHost.Web\WopiHost.Web.csproj", "{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -91,14 +74,6 @@ Global
Release|Mixed Platforms = Release|Mixed Platforms
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{21B81530-CC57-46F1-9524-08F984187DE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Release|Any CPU.Build.0 = Release|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{21B81530-CC57-46F1-9524-08F984187DE2}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{84B896D2-1A87-4671-B583-92A6AF3645F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{84B896D2-1A87-4671-B583-92A6AF3645F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{84B896D2-1A87-4671-B583-92A6AF3645F0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -139,14 +114,6 @@ Global
{650A5A85-5956-491E-9312-5E25A27D1108}.Release|Any CPU.Build.0 = Release|Any CPU
{650A5A85-5956-491E-9312-5E25A27D1108}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{650A5A85-5956-491E-9312-5E25A27D1108}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Release|Any CPU.Build.0 = Release|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5C301182-6FB2-40C5-97ED-58B07A910BFF}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4089410A-8A12-455C-862B-A4A3834C3100}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4089410A-8A12-455C-862B-A4A3834C3100}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4089410A-8A12-455C-862B-A4A3834C3100}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -211,15 +178,29 @@ Global
{97442113-B773-4F71-8B60-EF5599AC135D}.Release|Any CPU.Build.0 = Release|Any CPU
{97442113-B773-4F71-8B60-EF5599AC135D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{97442113-B773-4F71-8B60-EF5599AC135D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Release|Any CPU.Build.0 = Release|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Release|Any CPU.Build.0 = Release|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6}.Release|Mixed Platforms.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F66706B6-D7DD-48FB-963C-870B3D611500} = {785E1533-48CE-4B5E-8C59-D6F1FDA8C45C}
{21B81530-CC57-46F1-9524-08F984187DE2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{650A5A85-5956-491E-9312-5E25A27D1108} = {B692B58D-1720-49A8-9CB8-5562894618F6}
{5C301182-6FB2-40C5-97ED-58B07A910BFF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{4089410A-8A12-455C-862B-A4A3834C3100} = {B692B58D-1720-49A8-9CB8-5562894618F6}
{9FFB3975-2DE6-4944-92C7-7062C39BB8FA} = {B692B58D-1720-49A8-9CB8-5562894618F6}
{9CBB4AD4-EAE4-4D85-A70C-ECC465DA3D86} = {B692B58D-1720-49A8-9CB8-5562894618F6}
Expand All @@ -228,6 +209,8 @@ Global
{EA1A15F6-C3AF-4AAF-9A8B-4B7E053D6CEC} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{1CCBC8E4-5688-4F95-ADE8-31BF49064828} = {EA1A15F6-C3AF-4AAF-9A8B-4B7E053D6CEC}
{7EC2D8B4-42A4-40E2-AC9A-E4496DA57F4C} = {1CCBC8E4-5688-4F95-ADE8-31BF49064828}
{A0D7E71F-8940-4D2E-5BDE-E82A8DEAE605} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{750ECE28-6B96-8C09-67E1-CDBE48EC16A6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {91B6EE44-4505-4272-9D80-E4C73B09BF25}
Expand Down
13 changes: 6 additions & 7 deletions src/WopiHost.Abstractions/WopiFileOperations.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WopiHost.Abstractions;
namespace WopiHost.Abstractions;

/// <summary>
/// Details all WOPI file operation keywords
Expand Down Expand Up @@ -41,6 +35,11 @@ public static class WopiFileOperations
/// </summary>
public const string PutRelativeFile = "PUT_RELATIVE";

/// <summary>
/// https://learn.microsoft.com/microsoft-365/cloud-storage-partner-program/rest/files/putuserinfo
/// </summary>
public const string PutUserInfo = "PUT_USER_INFO";

/// <summary>
/// Cobalt file operations
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion src/WopiHost.Abstractions/WopiHostCapabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class WopiHostCapabilities : IWopiHostCapabilities
public bool SupportsCoauth { get; set; }

/// <inheritdoc/>
/// <remarks>set by FilesController.ctor depending on registered DI services</remarks>
public bool SupportsCobalt { get; set; }

/// <inheritdoc/>
Expand All @@ -18,9 +19,11 @@ public class WopiHostCapabilities : IWopiHostCapabilities
public bool SupportsContainers { get; set; } = true;

/// <inheritdoc/>
/// <remarks>set by FilesController.ctor depending on registered DI services</remarks>
public bool SupportsLocks { get; set; }

/// <inheritdoc/>
/// <remarks>set by FilesController.ctor depending on registered DI services</remarks>
public bool SupportsGetLock { get; set; }

/// <inheritdoc/>
Expand Down Expand Up @@ -54,5 +57,5 @@ public class WopiHostCapabilities : IWopiHostCapabilities
public bool SupportsDeleteFile { get; set; }

/// <inheritdoc/>
public bool SupportsUserInfo { get; set; }
public bool SupportsUserInfo { get; set; } = true;
}
48 changes: 46 additions & 2 deletions src/WopiHost.Core/Controllers/FilesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using WopiHost.Abstractions;
using WopiHost.Core.Extensions;
Expand All @@ -22,6 +23,7 @@
/// <param name="storageProvider">Storage provider instance for retrieving files and folders.</param>
/// <param name="securityHandler">Security handler instance for performing security-related operations.</param>
/// <param name="wopiHostOptions">WOPI Host configuration</param>
/// <param name="memoryCache">An instance of the memory cache.</param>
/// <param name="lockProvider">An instance of the lock provider.</param>
/// <param name="cobaltProcessor">An instance of a MS-FSSHTTP processor.</param>
[Authorize]
Expand All @@ -31,6 +33,7 @@
IWopiStorageProvider storageProvider,
IWopiSecurityHandler securityHandler,
IOptions<WopiHostOptions> wopiHostOptions,
IMemoryCache memoryCache,
IWopiLockProvider? lockProvider = null,
ICobaltProcessor? cobaltProcessor = null) : ControllerBase
{
Expand All @@ -42,6 +45,7 @@
SupportsCoauth = false,
SupportsUpdate = true //TODO: PutRelativeFile
};
private const string UserInfoCacheKey = "UserInfo-{0}";

/// <summary>
/// Returns the metadata about a file specified by an identifier.
Expand Down Expand Up @@ -236,6 +240,40 @@
[WopiAuthorize(WopiResourceType.File, Permission.Update)]
public Task<IActionResult> PutRelativeFile(string id) => throw new NotImplementedException($"{nameof(PutRelativeFile)} is not implemented yet.");

/// <summary>
/// The PutUserInfo operation stores some basic user information on the host.
/// M365 spec: https://learn.microsoft.com/microsoft-365/cloud-storage-partner-program/rest/files/putuserinfo
/// Example URL path: /wopi/files/(file_id)
/// </summary>
/// <param name="id">A string that specifies a file ID of a file managed by host. This string must be URL safe.</param>
/// <param name="userInfo">A string that specifies the user information to be stored on the host. This string must be URL safe.</param>
/// <returns>Returns <see cref="StatusCodes.Status200OK"/> if succeeded.</returns>
[HttpPost("{id}"), WopiOverrideHeader(WopiFileOperations.PutUserInfo)]
public IActionResult PutUserInfo(
string id,
[FromStringBody] string userInfo)
{
// Get file
var file = storageProvider.GetWopiFile(id);
if (file is null)
{
return NotFound();
}

// The UserInfo string should be associated with a particular user,
// and should be passed back to the WOPI client in subsequent CheckFileInfo responses in the UserInfo property.
// we store indefinitely in memoryCache to avoid the need for a persistence model - it's called anyway by the Wopi client on every start
memoryCache.Set(
string.Format(UserInfoCacheKey, User.GetUserId()),
userInfo,
new MemoryCacheEntryOptions
{
Priority = CacheItemPriority.NeverRemove,
});

return Ok();
}

/// <summary>
/// Changes the contents of the file in accordance with [MS-FSSHTTP].
/// MS-FSSHTTP Specification: https://learn.microsoft.com/openspecs/sharepoint_protocols/ms-fsshttp/05fa7efd-48ed-48d5-8d85-77995e17cc81
Expand Down Expand Up @@ -287,8 +325,7 @@

if (User?.Identity?.IsAuthenticated == true)
{
checkFileInfo.UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value.ToSafeIdentity()
?? throw new InvalidOperationException("Could not find NameIdentifier claim");
checkFileInfo.UserId = User.GetUserId();
checkFileInfo.HostAuthenticationId = checkFileInfo.UserId;
checkFileInfo.UserFriendlyName = User.FindFirst(ClaimTypes.Name)?.Value;
checkFileInfo.UserPrincipalName = User.FindFirst(ClaimTypes.Upn)?.Value ?? string.Empty;
Expand All @@ -303,6 +340,13 @@
checkFileInfo.UserCanRename = permissions.HasFlag(WopiUserPermissions.UserCanRename);
checkFileInfo.UserCanWrite = permissions.HasFlag(WopiUserPermissions.UserCanWrite);
checkFileInfo.WebEditingDisabled = permissions.HasFlag(WopiUserPermissions.WebEditingDisabled);

// The UserInfo ... should be passed back to the WOPI client in subsequent CheckFileInfo responses in the UserInfo property.
if (memoryCache.TryGetValue(string.Format(UserInfoCacheKey, checkFileInfo.UserId), out string? userInfo) &&
userInfo is not null)
{
checkFileInfo.UserInfo = userInfo;
}
}
else
{
Expand Down Expand Up @@ -332,161 +376,161 @@
WopiFileOperations.RefreshLock,
WopiFileOperations.GetLock)]
[WopiAuthorize(WopiResourceType.File, Permission.Update)]
public IActionResult ProcessLock(
string id,
[FromHeader(Name = WopiHeaders.WOPI_OVERRIDE)] string? wopiOverrideHeader = null,
[FromHeader(Name = WopiHeaders.OLD_LOCK)] string? oldLockIdentifier = null,
[FromHeader(Name = WopiHeaders.LOCK)] string? newLockIdentifier = null)
{
if (lockProvider is null)
{
return new LockMismatchResult(Response, reason: "Locking is not supported");
}

var lockAcquired = lockProvider.TryGetLock(id, out var existingLock);
switch (wopiOverrideHeader)
{
case WopiFileOperations.GetLock:
if (lockAcquired)
{
if (existingLock is null)
{
return new LockMismatchResult(Response, reason: "Missing existing lock");
}
Response.Headers[WopiHeaders.LOCK] = existingLock.LockId;
}
else
{
// File is not locked (or lock expired)... return empty X-WOPI-Lock header
Response.Headers[WopiHeaders.LOCK] = string.Empty;
}
return Ok();

case WopiFileOperations.Lock:
case WopiFileOperations.Put:
if (oldLockIdentifier is null)
{
if (string.IsNullOrWhiteSpace(newLockIdentifier))
{
return new LockMismatchResult(Response, reason: "Missing new lock identifier");
}

// Lock / put
if (lockAcquired)
{
if (existingLock is null)
{
return new LockMismatchResult(Response, reason: "Missing existing lock");
}
return LockOrRefresh(newLockIdentifier, existingLock);
}
else
{
// The file is not currently locked, create and store new lock information
if (lockProvider.AddLock(id, newLockIdentifier) != null)
{
return Ok();
}
else
{
return new LockMismatchResult(Response, "Could not create lock");
}
}
}
else
{
// Unlock and re-lock (https://learn.microsoft.com/microsoft-365/cloud-storage-partner-program/rest/files/unlockandrelock)
if (lockAcquired)
{
if (existingLock is null)
{
return new LockMismatchResult(Response, reason: "Missing existing lock");
}
if (existingLock.LockId == oldLockIdentifier)
{
if (string.IsNullOrWhiteSpace(newLockIdentifier))
{
return new LockMismatchResult(Response, reason: "Missing new lock identifier");
}

// Replace the existing lock with the new one
if (lockProvider.RefreshLock(id, newLockIdentifier))
{
return Ok();
}
else
{
return new LockMismatchResult(Response, "Could not create lock");
}
}
else
{
// The existing lock doesn't match the requested one. Return a lock mismatch error along with the current lock
return new LockMismatchResult(Response, existingLock.LockId);
}
}
else
{
// The requested lock does not exist which should result in a lock mismatch error.
return new LockMismatchResult(Response, reason: "File not locked");
}
}

case WopiFileOperations.Unlock:
if (lockAcquired)
{
if (existingLock is null)
{
return new LockMismatchResult(Response, reason: "Missing existing lock");
}
if (existingLock.LockId == newLockIdentifier)
{
// Remove valid lock
if (lockProvider.RemoveLock(id))
{
return Ok();
}
else
{
return new LockMismatchResult(Response, "Could not remove lock");
}
}
else
{
// The existing lock doesn't match the requested one. Return a lock mismatch error along with the current lock
return new LockMismatchResult(Response, existingLock.LockId);
}
}
else
{
// The requested lock does not exist.
return new LockMismatchResult(Response, reason: "File not locked");
}

case WopiFileOperations.RefreshLock:
if (lockAcquired)
{
if (existingLock is null)
{
return new LockMismatchResult(Response, reason: "Missing existing lock");
}
if (string.IsNullOrWhiteSpace(newLockIdentifier))
{
return new LockMismatchResult(Response, reason: "Missing new lock identifier");
}
return LockOrRefresh(newLockIdentifier, existingLock);
}
else
{
// The requested lock does not exist. That's also a lock mismatch error.
return new LockMismatchResult(Response, reason: "File not locked");
}

default:
return new NotImplementedResult();
}
}

Check notice on line 533 in src/WopiHost.Core/Controllers/FilesController.cs

View check run for this annotation

codefactor.io / CodeFactor

src/WopiHost.Core/Controllers/FilesController.cs#L379-L533

Complex Method
private IActionResult LockOrRefresh(string newLock, WopiLockInfo existingLock)
{
ArgumentNullException.ThrowIfNull(lockProvider);
Expand Down
14 changes: 14 additions & 0 deletions src/WopiHost.Core/Extensions/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -42,6 +43,19 @@ public static long ToUnixTimestamp(this DateTime dateTime)
return dto.ToUnixTimeSeconds();
}

/// <summary>
/// Returns the User NameIdentifier claim value.
/// </summary>
/// <param name="principal">the current user</param>
/// <returns>nameIdentifier</returns>
/// <exception cref="InvalidOperationException">if such a claim does not exist</exception>
public static string GetUserId(this ClaimsPrincipal principal)
{
ArgumentNullException.ThrowIfNull(principal);
return principal.FindFirst(ClaimTypes.NameIdentifier)?.Value.ToSafeIdentity()
?? throw new InvalidOperationException("Could not find NameIdentifier claim");
}

/// <summary>
/// Replaces forbidden characters in identity properties with an underscore.
/// Accordingly to: https://learn.microsoft.com/microsoft-365/cloud-storage-partner-program/rest/files/checkfileinfo/checkfileinfo-response#requirements-for-user-identity-properties
Expand Down
17 changes: 17 additions & 0 deletions src/WopiHost.Core/Infrastructure/FromStringBodyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Mvc;

namespace WopiHost.Core.Infrastructure;

/// <summary>
/// Attribute for binding request body to a string (.NET FromBody binding only accepts json)
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromStringBodyAttribute : ModelBinderAttribute
{
/// <summary>
/// Creates a new instance of the <see cref="FromStringBodyAttribute"/> using our <see cref="FromStringBodyModelBinder"/>
/// </summary>
public FromStringBodyAttribute() : base(typeof(FromStringBodyModelBinder))
{
}
}
42 changes: 42 additions & 0 deletions src/WopiHost.Core/Infrastructure/FromStringBodyModelBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace WopiHost.Core.Infrastructure;

/// <summary>
/// Model binder for binding request body to a string.
/// </summary>
public sealed class FromStringBodyModelBinder : IModelBinder
{
/// <inheritdoc/>
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
ArgumentNullException.ThrowIfNull(bindingContext);

var request = bindingContext.HttpContext.Request;

try
{
if (!request.Body.CanSeek)
{
request.EnableBuffering();
}

if (request.ContentLength is not null && request.ContentLength > 0)
{
request.Body.Position = 0;
using var reader = new StreamReader(request.Body);
var body = await reader.ReadToEndAsync();

bindingContext.Result = ModelBindingResult.Success(body ?? string.Empty);

request.Body.Position = 0;
}
}
catch (Exception ex)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex, bindingContext.ModelMetadata);
bindingContext.Result = ModelBindingResult.Failed();
}
}
}
Loading