From b995b357d0197a16d961da1e31d8e4820fb32376 Mon Sep 17 00:00:00 2001 From: Daniel Pastoor Date: Sun, 1 Sep 2024 17:26:41 +0200 Subject: [PATCH 1/3] Changed BinaryBody property to Content so the type is HttpContent in the ApiCall class so it is possible to add a content-type to the ByteArrayContent. This is needed for uploading images or other files to the graph api --- .../Model/SharePoint/Core/Internal/AppManager.cs | 2 +- .../Core/Internal/AttachmentCollection.cs | 3 ++- .../Core/Internal/FieldThumbnailValue.cs | 2 +- .../SharePoint/Core/Internal/FileCollection.cs | 8 ++++---- .../Model/SharePoint/Core/Internal/UserProfile.cs | 2 +- src/sdk/PnP.Core/Services/Core/ApiCall.cs | 14 ++++++++------ src/sdk/PnP.Core/Services/Core/Batch.cs | 2 +- src/sdk/PnP.Core/Services/Core/BatchClient.cs | 10 ++++------ 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/sdk/PnP.Core.Admin/Model/SharePoint/Core/Internal/AppManager.cs b/src/sdk/PnP.Core.Admin/Model/SharePoint/Core/Internal/AppManager.cs index a782d32a6d..7c73bd9e80 100644 --- a/src/sdk/PnP.Core.Admin/Model/SharePoint/Core/Internal/AppManager.cs +++ b/src/sdk/PnP.Core.Admin/Model/SharePoint/Core/Internal/AppManager.cs @@ -195,7 +195,7 @@ public async Task AddAsync(byte[] fileBytes, string fileName, bool overwrite var apiCall = new ApiCall($"_api/web/{Scope}appcatalog/Add(overwrite={overwrite.ToString().ToLower()},url='{fileName}')", ApiType.SPORest) { Interactive = true, - BinaryBody = fileBytes, + Content = new ByteArrayContent(fileBytes), Headers = new Dictionary() }; diff --git a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/AttachmentCollection.cs b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/AttachmentCollection.cs index b4add5b499..c1ec95511e 100644 --- a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/AttachmentCollection.cs +++ b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/AttachmentCollection.cs @@ -44,8 +44,9 @@ private static async Task FileUpload(Attachment newFile, Stream cont var api = new ApiCall(fileCreateRequest, ApiType.SPORest) { Interactive = true, - BinaryBody = ToByteArray(content) + Content = new ByteArrayContent(ToByteArray(content)) }; + await newFile.RequestAsync(api, HttpMethod.Post).ConfigureAwait(false); return newFile; } diff --git a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FieldThumbnailValue.cs b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FieldThumbnailValue.cs index 92536fb376..7cfdc494aa 100644 --- a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FieldThumbnailValue.cs +++ b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FieldThumbnailValue.cs @@ -121,7 +121,7 @@ public async Task UploadImageAsync(IListItem item, string name, Stream content) var api = new ApiCall(fileCreateRequest, ApiType.SPORest) { Interactive = true, - BinaryBody = ToByteArray(content), + Content = new ByteArrayContent(ToByteArray(content)) }; var response = await (item as ListItem).RawRequestAsync(api, HttpMethod.Post).ConfigureAwait(false); diff --git a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FileCollection.cs b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FileCollection.cs index 22f882f237..14b66d8795 100644 --- a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FileCollection.cs +++ b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/FileCollection.cs @@ -78,7 +78,7 @@ private static async Task FileUpload(File newFile, Stream content, bool ov var api = new ApiCall(fileCreateRequest, ApiType.SPORest) { Interactive = true, - BinaryBody = ToByteArray(content) + Content = new ByteArrayContent(ToByteArray(content)) }; await newFile.RequestAsync(api, HttpMethod.Post).ConfigureAwait(false); return newFile; @@ -122,7 +122,7 @@ private static async Task ChunkedFileUpload(File newFile, Stream content, api = new ApiCall(endpointUrl, ApiType.SPORest) { Interactive = true, - BinaryBody = chunk + Content = new ByteArrayContent(chunk) }; await newFile.RequestAsync(api, HttpMethod.Post).ConfigureAwait(false); firstChunk = false; @@ -134,7 +134,7 @@ private static async Task ChunkedFileUpload(File newFile, Stream content, var api = new ApiCall(endpointUrl, ApiType.SPORest) { Interactive = true, - BinaryBody = chunk + Content = new ByteArrayContent(chunk) }; await newFile.RequestAsync(api, HttpMethod.Post).ConfigureAwait(false); @@ -146,7 +146,7 @@ private static async Task ChunkedFileUpload(File newFile, Stream content, var api = new ApiCall(endpointUrl, ApiType.SPORest) { Interactive = true, - BinaryBody = chunk + Content = new ByteArrayContent(chunk) }; await newFile.RequestAsync(api, HttpMethod.Post).ConfigureAwait(false); } diff --git a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/UserProfile.cs b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/UserProfile.cs index ba86d9e50f..92c5a6c7d4 100644 --- a/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/UserProfile.cs +++ b/src/sdk/PnP.Core/Model/SharePoint/Core/Internal/UserProfile.cs @@ -113,7 +113,7 @@ public async Task SetMyProfilePictureAsync(byte[] fileBytes) var apiCall = new ApiCall(baseUrl, ApiType.SPORest) { Interactive = true, - BinaryBody = fileBytes + Content = new ByteArrayContent(fileBytes) }; await RawRequestAsync(apiCall, HttpMethod.Post).ConfigureAwait(false); diff --git a/src/sdk/PnP.Core/Services/Core/ApiCall.cs b/src/sdk/PnP.Core/Services/Core/ApiCall.cs index 1ad2e970c6..4f8f144a38 100644 --- a/src/sdk/PnP.Core/Services/Core/ApiCall.cs +++ b/src/sdk/PnP.Core/Services/Core/ApiCall.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Net.Http; namespace PnP.Core.Services { @@ -9,7 +10,8 @@ namespace PnP.Core.Services /// internal struct ApiCall { - internal ApiCall(string request, ApiType apiType, string jsonBody = null, string receivingProperty = null, bool loadPages = false) + internal ApiCall(string request, ApiType apiType, string jsonBody = null, string receivingProperty = null, + bool loadPages = false) { Type = apiType; Request = request; @@ -22,7 +24,7 @@ internal ApiCall(string request, ApiType apiType, string jsonBody = null, string RawResultsHandler = null; Commit = false; Interactive = false; - BinaryBody = null; + Content = null; ExpectBinaryResponse = false; StreamResponse = false; RemoveFromModel = false; @@ -46,7 +48,7 @@ internal ApiCall(List> csomRequests, string RawResultsHandler = null; Commit = false; Interactive = false; - BinaryBody = null; + Content = null; ExpectBinaryResponse = false; StreamResponse = false; RemoveFromModel = false; @@ -117,9 +119,9 @@ internal ApiCall(List> csomRequests, string internal bool Interactive { get; set; } /// - /// Binary content for this API call + /// Http Content to add Binary content or other content to this API call /// - internal byte[] BinaryBody { get; set; } + internal HttpContent Content { get; set; } /// /// Indicates whether the call expects a binary response @@ -161,4 +163,4 @@ internal ApiCall(List> csomRequests, string /// internal bool AddedViaBatchMethod { get; set; } } -} +} \ No newline at end of file diff --git a/src/sdk/PnP.Core/Services/Core/Batch.cs b/src/sdk/PnP.Core/Services/Core/Batch.cs index d1d5d1c580..ddce3fcf4f 100644 --- a/src/sdk/PnP.Core/Services/Core/Batch.cs +++ b/src/sdk/PnP.Core/Services/Core/Batch.cs @@ -251,7 +251,7 @@ internal Guid PrepareLastAddedRequestForBatchProcessing(Action Date: Sun, 1 Sep 2024 17:27:23 +0200 Subject: [PATCH 2/3] Added support for application permissions to set a site logo or thumbnail when the site is connected to a group --- .../Branding/Internal/HeaderOptions.cs | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/sdk/PnP.Core/Model/SharePoint/Branding/Internal/HeaderOptions.cs b/src/sdk/PnP.Core/Model/SharePoint/Branding/Internal/HeaderOptions.cs index 026e9baa82..0503ba8d80 100644 --- a/src/sdk/PnP.Core/Model/SharePoint/Branding/Internal/HeaderOptions.cs +++ b/src/sdk/PnP.Core/Model/SharePoint/Branding/Internal/HeaderOptions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; @@ -74,29 +75,63 @@ public async Task SetSiteLogoThumbnailAsync(string fileName, Stream content, boo } else { - Dictionary headers; + var byteContent = new ByteArrayContent(ToByteArray(content)); + if (MimeTypeMap.TryGetMimeType(fileName, out string mimeType)) { - headers = new Dictionary - { - { "Content-Type", mimeType } - }; + byteContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType); } else { - throw new ClientException(ErrorType.Unsupported, PnPCoreResources.Exception_Unsupported_NotAnImageMimeType); + throw new ClientException(ErrorType.Unsupported, + PnPCoreResources.Exception_Unsupported_NotAnImageMimeType); } - // We're setting the group logo as that serves as the site logo thumbnail - var api = new ApiCall("_api/GroupService/SetGroupImage", ApiType.SPORest) + var apiCall = new ApiCall($"groups/{PnPContext.Site.GroupId}/photo/$value", ApiType.Graph) { - Interactive = true, - BinaryBody = ToByteArray(content), - Headers = headers + Interactive = true, Content = byteContent }; - // Set the uploaded file as site logo - await (PnPContext.Web as Web).RawRequestAsync(api, HttpMethod.Post, "SetGroupImage").ConfigureAwait(false); + // Upload the image and set it as group logo + await (PnPContext.Web as Web).RawRequestAsync(apiCall, HttpMethod.Put).ConfigureAwait(false); + + // get the site url and current used site logo url to check if it is set correctly + await PnPContext.Web.EnsurePropertiesAsync(x => x.SiteLogoUrl, x => x.Url).ConfigureAwait(false); + + var correctSiteLogoUrl = + $"{PnPContext.Web.Url.PathAndQuery}/_api/GroupService/GetGroupImage?id='{PnPContext.Site.GroupId}'"; + + // Use StartSWith to avoid issues with the query string that can contains &hash=xxxx + if (string.IsNullOrEmpty(PnPContext.Web.SiteLogoUrl) || + !PnPContext.Web.SiteLogoUrl.StartsWith(correctSiteLogoUrl)) + { + PnPContext.Web.SiteLogoUrl = correctSiteLogoUrl; + await PnPContext.Web.UpdateAsync().ConfigureAwait(false); + + const string cachedSiteIcon = "__siteIcon__.png"; + + try + { + // check if we have a file named "__siteIcon__.jpg" in the SiteAssets folder + var file = await PnPContext.Web + .GetFileByServerRelativeUrlAsync( + $"{PnPContext.Uri.AbsolutePath}/siteassets/{cachedSiteIcon}") + .ConfigureAwait(false); + + // delete the cached file to ensure the new logo is used https://learn.microsoft.com/en-us/sharepoint/troubleshoot/sites/error-when-changing-o365-site-logo + await file.DeleteAsync().ConfigureAwait(false); + } + catch (SharePointRestServiceException ex) + { + var error = ex.Error as SharePointRestError; + + // If the exception indicated a non existing file then ignore, else throw + if (!File.ErrorIndicatesFileDoesNotExists(error)) + { + throw; + } + } + } } } @@ -215,4 +250,4 @@ private static byte[] ToByteArray(Stream source) } } } -} +} \ No newline at end of file From 4c53edeb7cfab9acf66f419326359199286d5dcd Mon Sep 17 00:00:00 2001 From: Daniel Pastoor Date: Sun, 1 Sep 2024 17:29:26 +0200 Subject: [PATCH 3/3] Removed the part in the documentation where it said that setting site logo is not supported for group connected sites with application permission --- docs/using-the-sdk/branding-intro.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/using-the-sdk/branding-intro.md b/docs/using-the-sdk/branding-intro.md index dc6bd79bbb..aa594bcae7 100644 --- a/docs/using-the-sdk/branding-intro.md +++ b/docs/using-the-sdk/branding-intro.md @@ -114,9 +114,6 @@ await chrome.Header.ResetSiteLogoThumbnailAsync(); While above methods work for any modern SharePoint site the implementation is different for group connected sites (such as a Team site) as for those sites the logo is maintained via the connected group. For group connected sites the site logo and the site logo thumbnail are the same and as such is does not matter which set or reset method you use, both result in the same code being called. Another difference is on how the provided image is stored: for group connected sites the image is stored with the Microsoft 365 group, whereas for other sites the image is first uploaded to the site assets library. Consequently there's also a difference in reset behavior: for group connected sites a reset will try to load the `__siteIcon__.jpg` file from the site assets library and set that as group image, for other site types there's no dependency on an image in site assets library. -> [!Important] -> When you're using the `SetSiteLogo` and `SetSiteLogoThumbnail` methods on a Microsoft 365 group connected site while using application permissions then you'll get an error. Currently these methods require delegated permissions when used on a group connected sites, usage on other sites is not impacted. - ### Set and clear the header background image Whenever the site header layout is set to `Extended` you can optionally set a header background image via the `SetHeaderBackgroundImage` methods. Clearing the header background can be done using the `ClearHeaderBackgroundImage` methods.