From 0e2e5c75b282e972db10cd4c2530e30c445fb6fe Mon Sep 17 00:00:00 2001 From: jorisvanduyse Date: Tue, 14 Jul 2026 14:04:08 +0200 Subject: [PATCH 1/2] Fix SharePoint request digest URL for sites (#8932) --- .../SharePoint/src/SharePointClientImpl.Codeunit.al | 10 +++++----- .../src/helpers/SharePointUriBuilder.Codeunit.al | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/System Application/App/SharePoint/src/SharePointClientImpl.Codeunit.al b/src/System Application/App/SharePoint/src/SharePointClientImpl.Codeunit.al index e728aa07fb5..df60679e734 100644 --- a/src/System Application/App/SharePoint/src/SharePointClientImpl.Codeunit.al +++ b/src/System Application/App/SharePoint/src/SharePointClientImpl.Codeunit.al @@ -370,7 +370,7 @@ codeunit 9101 "SharePoint Client Impl." Request.Add('Title', ListTitle); SharePointHttpContent.FromJson(Request); - SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetHost())); + SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetSiteUrl())); SharePointRequestHelper.SetAuthorization(Authorization); SharePointOperationResponse := SharePointRequestHelper.Post(SharePointUriBuilder, SharePointHttpContent); @@ -399,7 +399,7 @@ codeunit 9101 "SharePoint Client Impl." Request.Add('Title', ListItemTitle); SharePointHttpContent.FromJson(Request); - SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetHost())); + SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetSiteUrl())); SharePointRequestHelper.SetAuthorization(Authorization); SharePointOperationResponse := SharePointRequestHelper.Post(SharePointUriBuilder, SharePointHttpContent); @@ -427,7 +427,7 @@ codeunit 9101 "SharePoint Client Impl." Request.Add('Title', ListItemTitle); SharePointHttpContent.FromJson(Request); - SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetHost())); + SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetSiteUrl())); SharePointRequestHelper.SetAuthorization(Authorization); SharePointOperationResponse := SharePointRequestHelper.Post(SharePointUriBuilder, SharePointHttpContent); @@ -640,7 +640,7 @@ codeunit 9101 "SharePoint Client Impl." Request.Add('ServerRelativeUrl', ServerRelativeUrl); SharePointHttpContent.FromJson(Request); - SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetHost())); + SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetSiteUrl())); SharePointRequestHelper.SetAuthorization(Authorization); SharePointOperationResponse := SharePointRequestHelper.Post(SharePointUriBuilder, SharePointHttpContent); @@ -786,7 +786,7 @@ codeunit 9101 "SharePoint Client Impl." SharePointHttpContent.GetContent().ReadAs(Txt); - SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetHost())); + SharePointHttpContent.SetRequestDigest(GetRequestDigest(SharePointUriBuilder.GetSiteUrl())); SharePointHttpContent.SetXHttpMethod('MERGE'); SharePointHttpContent.SetIfMatch('*'); diff --git a/src/System Application/App/SharePoint/src/helpers/SharePointUriBuilder.Codeunit.al b/src/System Application/App/SharePoint/src/helpers/SharePointUriBuilder.Codeunit.al index a77e30319a2..0a3059f3abb 100644 --- a/src/System Application/App/SharePoint/src/helpers/SharePointUriBuilder.Codeunit.al +++ b/src/System Application/App/SharePoint/src/helpers/SharePointUriBuilder.Codeunit.al @@ -31,6 +31,11 @@ codeunit 9110 "SharePoint Uri Builder" exit(NewUri.GetHost()); end; + procedure GetSiteUrl(): Text + begin + exit(ServerName.TrimStart('/').TrimEnd('/')); + end; + procedure AddQueryParameter(ParameterName: Text; ParameterValue: Text) begin QueryParameters.Add(ParameterName, ParameterValue); From 9f4ae3119a7c9ed4f224b8587c507153ee0e1754 Mon Sep 17 00:00:00 2001 From: jorisvanduyse Date: Tue, 14 Jul 2026 18:21:34 +0200 Subject: [PATCH 2/2] Add regression tests for site-scoped SharePoint request digest (#8932) Adds coverage for CreateFolder, CreateList, and CreateListItem to assert that the request digest (_api/contextinfo) is requested from the site-scoped URL rather than the tenant root, so the fix keeps working under Sites.Selected permissions. --- .../src/SharePointTestLibrary.Codeunit.al | 13 +++++++ .../src/SharePointClientTest.Codeunit.al | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/System Application/Test Library/SharePoint/src/SharePointTestLibrary.Codeunit.al b/src/System Application/Test Library/SharePoint/src/SharePointTestLibrary.Codeunit.al index 951149f7e86..e87573edf93 100644 --- a/src/System Application/Test Library/SharePoint/src/SharePointTestLibrary.Codeunit.al +++ b/src/System Application/Test Library/SharePoint/src/SharePointTestLibrary.Codeunit.al @@ -12,6 +12,9 @@ codeunit 132973 "SharePoint Test Library" { EventSubscriberInstance = Manual; + var + LastContextInfoRequestUri: Text; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"SharePoint Request Helper", 'OnBeforeSendRequest', '', false, false)] local procedure RunOnBeforeSendRequest(HttpRequestMessage: HttpRequestMessage; var SharePointOperationResponse: Codeunit "SharePoint Operation Response"; var IsHandled: Boolean; Method: Text) var @@ -33,6 +36,7 @@ codeunit 132973 "SharePoint Test Library" Uri := LocalUri.UnescapeDataString(Uri); if Uri.EndsWith('/_api/contextinfo/') then begin + LastContextInfoRequestUri := Uri; GetContextDigestTestResponse(SharePointOperationResponse, BaseUrl, ParentUrl); exit; end; @@ -109,6 +113,15 @@ codeunit 132973 "SharePoint Test Library" Error('No matching test response for %1', Uri); end; + /// + /// Returns the full request URI of the last "_api/contextinfo" request digest call that was intercepted by this test library. + /// + /// The full, unescaped request URI, or an empty string if no request digest call has been made yet. + procedure GetLastContextInfoRequestUri(): Text + begin + exit(LastContextInfoRequestUri); + end; + local procedure GetContextDigestTestResponse(var SharePointOperationResponse: Codeunit "SharePoint Operation Response"; BaseUrl: Text; ParentUrl: Text) var HttpHeaders: HttpHeaders; diff --git a/src/System Application/Test/SharePoint/src/SharePointClientTest.Codeunit.al b/src/System Application/Test/SharePoint/src/SharePointClientTest.Codeunit.al index cd04a01acfb..5b3593f3510 100644 --- a/src/System Application/Test/SharePoint/src/SharePointClientTest.Codeunit.al +++ b/src/System Application/Test/SharePoint/src/SharePointClientTest.Codeunit.al @@ -399,6 +399,45 @@ codeunit 132970 "SharePoint Client Test" Assert.IsTrue(TempSharePointFolder.OdataId.EndsWith('_api/Web/GetFolderByServerRelativePath(decodedurl=''' + ParentUrl + '/Lists/Test Documents/Attachments/TestSubfolder'')'), StrSubstNo('Different %1 value expected', TempSharePointFolder.FieldCaption("OdataId"))); Assert.AreEqual('SP.Folder', TempSharePointFolder.OdataType, StrSubstNo('Different %1 value expected', TempSharePointFolder.FieldCaption("OdataType"))); Assert.IsTrue(TempSharePointFolder."Server Relative Url".EndsWith('/Lists/Test Documents/Attachments/TestSubfolder'), StrSubstNo('Different %1 value expected', TempSharePointFolder.FieldCaption("Server Relative Url"))); + + // [THEN] The request digest ("_api/contextinfo") call was made against the site-scoped URL, not the tenant root + Assert.AreEqual('https://' + BaseUrl + '/_api/contextinfo/', SharePointTestLibrary.GetLastContextInfoRequestUri(), 'Request digest should be requested from the site-scoped URL, not the tenant root.'); + end; + + [Test] + procedure TestCreateListRequestDigestUsesSiteScopedUrl() + var + TempSharePointList: Record "SharePoint List" temporary; + IsSuccess: Boolean; + begin + // [Scenario] CreateList requests the digest from the site-scoped URL, not the tenant root, so it works under Sites.Selected + Initialize(); + + // [WHEN] CreateList is called for a site-scoped account (BaseUrl contains a "/sites/" segment) + IsSuccess := SharePointClient.CreateList('Test Sample List Title', 'Test Sample List Description', TempSharePointList); + Assert.AreEqual(true, IsSuccess, 'Successfull operation expected'); + + // [THEN] The request digest ("_api/contextinfo") call was made against the site-scoped URL, not the tenant root + Assert.AreEqual('https://' + BaseUrl + '/_api/contextinfo/', SharePointTestLibrary.GetLastContextInfoRequestUri(), 'Request digest should be requested from the site-scoped URL, not the tenant root.'); + end; + + [Test] + procedure TestCreateListItemRequestDigestUsesSiteScopedUrl() + var + TempSharePointListItem: Record "SharePoint List Item" temporary; + Guid: Guid; + IsSuccess: Boolean; + begin + // [Scenario] CreateListItem requests the digest from the site-scoped URL, not the tenant root, so it works under Sites.Selected + Initialize(); + Evaluate(Guid, '{854D7F21-1C6A-43AB-A081-20404894B449}'); + + // [WHEN] CreateListItem is called for a site-scoped account (BaseUrl contains a "/sites/" segment) + IsSuccess := SharePointClient.CreateListItem(Guid, 'SP.Data.My_x0020_Test_x0020_DocumentsListItem', 'Test List Item', TempSharePointListItem); + Assert.AreEqual(true, IsSuccess, 'Successfull operation expected'); + + // [THEN] The request digest ("_api/contextinfo") call was made against the site-scoped URL, not the tenant root + Assert.AreEqual('https://' + BaseUrl + '/_api/contextinfo/', SharePointTestLibrary.GetLastContextInfoRequestUri(), 'Request digest should be requested from the site-scoped URL, not the tenant root.'); end; [Test]