diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs index d5fec7eb1..9bdd593d2 100644 --- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectPropertyBagEntry.cs @@ -30,6 +30,29 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ "DesignPreview" }); + //modern header, footer, font settings have to be set with SetChromeOptions, Set[xxxx]FontPackage (pnpContext.Web.GetBrandingManager()) in order to work as expected + var brandingSettings = new List(new[] + { + "headeroverlaycolor", + "headeroverlayopacity", + "headeroverlaygradientdirection", + "headercolorindexinlightmode", + "headercolorindexindarkmode", + "footeralignment", + "footeroverlaycolor", + "footeroverlayopacity", + "footeroverlaygradientdirection", + "footercolorindexinlightmode", + "footercolorindexindarkmode", + "fontoptionforsitetitle", + "fontoptionforsitenav", + "fontoptionforsitefootertitle", + "fontoptionforsitefooternav", + "footerbackgroundimageurl", + "footerbackgroundimagefocalpoint", + "backgroundimageurl" + }); + // Check if this is not a noscript site as we're not allowed to write to the web property bag is that one bool isNoScriptSite = web.IsNoScriptSite(); if (isNoScriptSite) @@ -44,7 +67,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ web = newContext.Web; - foreach (var propbagEntry in template.PropertyBagEntries) + foreach (var propbagEntry in template.PropertyBagEntries.Where(p=> !brandingSettings.Contains(p.Key.ToLower()))) { bool propExists = web.PropertyBagContainsKey(propbagEntry.Key); diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteFooterSettings.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteFooterSettings.cs index 5bbda8854..8b9a653bc 100644 --- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteFooterSettings.cs +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteFooterSettings.cs @@ -31,7 +31,7 @@ public override ProvisioningTemplate ExtractObjects(Web web, ProvisioningTemplat { using (var scope = new PnPMonitoredScope(this.Name)) { - web.EnsureProperties(w => w.FooterEnabled, w => w.ServerRelativeUrl, w => w.Url, w => w.Language); + web.EnsureProperties(w => w.FooterEnabled, w => w.ServerRelativeUrl, w => w.Url, w => w.Language, w => w.AllProperties); var defaultCulture = new CultureInfo((int)web.Language); var footer = new SiteFooter @@ -156,20 +156,33 @@ public override ProvisioningTemplate ExtractObjects(Web web, ProvisioningTemplat template.Footer = footer; if (creationInfo.PersistBrandingFiles) { + Uri webUri = new Uri(web.Url); + string webUrl = $"{webUri.Scheme}://{webUri.DnsSafeHost}"; + // Extract site logo if property has been set and it's not dynamic image from _api URL if (!string.IsNullOrEmpty(template.Footer.Logo) && (!template.Footer.Logo.ToLowerInvariant().Contains("_api/"))) { // Convert to server relative URL if needed (can be set to FQDN URL of a file hosted in the site (e.g. for communication sites)) - Uri webUri = new Uri(web.Url); - string webUrl = $"{webUri.Scheme}://{webUri.DnsSafeHost}"; - string footerLogoServerRelativeUrl = template.Footer.Logo.Replace(webUrl, ""); + string footerLogoServerRelativeUrl = Uri.UnescapeDataString(template.Footer.Logo).Replace(webUrl, ""); - if (PersistFile(web, creationInfo, scope, footerLogoServerRelativeUrl)) + if (Utilities.FileUtilities.PersistFile(web, creationInfo, scope, this, footerLogoServerRelativeUrl)) { template.Files.Add(GetTemplateFile(web, footerLogoServerRelativeUrl)); } } template.Footer.Logo = Tokenize(template.Footer.Logo, web.Url, web); + + var footerBackgroundImageUrl= web.GetPropertyBagValueString("FooterBackgroundImageUrl",""); + if (!string.IsNullOrWhiteSpace(footerBackgroundImageUrl)) + { + footerBackgroundImageUrl = Uri.UnescapeDataString(footerBackgroundImageUrl).Replace(webUrl, ""); + + if (Utilities.FileUtilities.PersistFile(web, creationInfo, scope, this, footerBackgroundImageUrl)) + { + template.Files.Add(GetTemplateFile(web, footerBackgroundImageUrl)); + } + } + var files = template.Files.Distinct().ToList(); template.Files.Clear(); template.Files.AddRange(files); @@ -190,6 +203,7 @@ private void ParseNodesMUI(MenuNode node, string webServerRelativeUrl, CultureIn } } } + private string TokenizeHost(Web web, string json) { // HostUrl token replacement @@ -223,99 +237,6 @@ private Model.File GetTemplateFile(Web web, string serverRelativeUrl) return templateFile; } - private bool PersistFile(Web web, ProvisioningTemplateCreationInformation creationInfo, PnPMonitoredScope scope, string serverRelativeUrl) - { - var success = false; - if (creationInfo.PersistBrandingFiles) - { - if (creationInfo.FileConnector != null) - { - if (UrlUtility.IsIisVirtualDirectory(serverRelativeUrl)) - { - scope.LogWarning("File is not located in the content database. Not retrieving {0}", serverRelativeUrl); - return success; - } - - try - { - var file = web.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(serverRelativeUrl)); - string fileName = string.Empty; - if (serverRelativeUrl.IndexOf("/") > -1) - { - fileName = serverRelativeUrl.Substring(serverRelativeUrl.LastIndexOf("/") + 1); - } - else - { - fileName = serverRelativeUrl; - } - web.Context.Load(file); - web.Context.ExecuteQueryRetry(); - ClientResult stream = file.OpenBinaryStream(); - web.Context.ExecuteQueryRetry(); - - file.EnsureProperty(f => f.ServerRelativePath); - var baseUri = new Uri(web.Url); - var fullUri = new Uri(baseUri, file.ServerRelativePath.DecodedUrl); - var folderPath = Uri.UnescapeDataString(fullUri.Segments.Take(fullUri.Segments.Length - 1).ToArray().Aggregate((i, x) => i + x).TrimEnd('/')); - - // Configure the filename to use - fileName = Uri.UnescapeDataString(fullUri.Segments[fullUri.Segments.Length - 1]); - - // Build up a site relative container URL...might end up empty as well - String container = Uri.UnescapeDataString(folderPath.Replace(web.ServerRelativeUrl, "")).Trim('/').Replace("/", "\\"); - - using (Stream memStream = new MemoryStream()) - { - CopyStream(stream.Value, memStream); - memStream.Position = 0; - if (!string.IsNullOrEmpty(container)) - { - creationInfo.FileConnector.SaveFileStream(fileName, container, memStream); - } - else - { - creationInfo.FileConnector.SaveFileStream(fileName, memStream); - } - } - success = true; - } - catch (ServerException ex1) - { - // If we are referring a file from a location outside of the current web or at a location where we cannot retrieve the file an exception is thrown. We swallow this exception. - if (ex1.ServerErrorCode != -2147024809) - { - throw; - } - else - { - scope.LogWarning("File is not necessarily located in the current web. Not retrieving {0}", serverRelativeUrl); - } - } - } - else - { - WriteMessage("No connector present to persist footer logo.", ProvisioningMessageType.Error); - scope.LogError("No connector present to persist footer logo."); - } - } - else - { - success = true; - } - return success; - } - - private void CopyStream(Stream source, Stream destination) - { - byte[] buffer = new byte[32768]; - int bytesRead; - - do - { - bytesRead = source.Read(buffer, 0, buffer.Length); - destination.Write(buffer, 0, bytesRead); - } while (bytesRead != 0); - } private SiteFooterLink ParseNodes(MenuNode node, ProvisioningTemplate template, string webServerRelativeUrl, bool persistLanguage, CultureInfo currentCulture, string parentKey, ProvisioningTemplateCreationInformation creationInfo) { var link = new SiteFooterLink(); @@ -359,61 +280,124 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ web.FooterEnabled = template.Footer.Enabled; var defaultCulture = new CultureInfo((int)web.Language); - //var jsonRequest = new - //{ - // footerEnabled = web.FooterEnabled, - // footerLayout = web.FooterLayout, - // footerEmphasis = web.FooterEmphasis - //}; - - //web.ExecutePostAsync("/_api/web/SetChromeOptions", System.Text.Json.JsonSerializer.Serialize(jsonRequest)).GetAwaiter().GetResult(); - // Move to the PnP Core SDK context using (var pnpCoreContext = PnPCoreSdk.Instance.GetPnPContext(web.Context as ClientContext)) { // Get the Chrome options - var chrome = pnpCoreContext.Web.GetBrandingManager().GetChromeOptions(); + var brandingManager = pnpCoreContext.Web.GetBrandingManager(); + var chrome = brandingManager.GetChromeOptions(); chrome.Footer.Enabled = web.FooterEnabled; chrome.Footer.DisplayName = template.Footer.DisplayName; chrome.Footer.Layout = (PnP.Core.Model.SharePoint.FooterLayoutType)Enum.Parse(typeof(PnP.Core.Model.SharePoint.FooterLayoutType), template.Footer.Layout.ToString()); - chrome.Footer.Emphasis = (PnP.Core.Model.SharePoint.FooterVariantThemeType)Enum.Parse(typeof(PnP.Core.Model.SharePoint.FooterVariantThemeType), template.Footer.BackgroundEmphasis.ToString()); + chrome.Footer.Emphasis = (PnP.Core.Model.SharePoint.FooterVariantThemeType)Enum.Parse(typeof(PnP.Core.Model.SharePoint.FooterVariantThemeType), template.Footer.BackgroundEmphasis.ToString()); - pnpCoreContext.Web.GetBrandingManager().SetChromeOptions(chrome); - } + //Modern footer settings + if (template?.PropertyBagEntries != null) + { + foreach (var entry in template.PropertyBagEntries) + { + if (string.IsNullOrWhiteSpace(entry.Value)) + { + continue; + } + switch (entry.Key.ToLower()) + { + case "footeroverlaycolor": + { + if (int.TryParse(entry.Value, out var footeroverlaycolor) && Enum.IsDefined(typeof(OverlayColorType), footeroverlaycolor)) + { + chrome.Footer.OverlayColor = (Core.Model.SharePoint.OverlayColorType)(OverlayColorType)footeroverlaycolor; + } + break; + } + case "footeroverlayopacity": + { + if (int.TryParse(entry.Value, out var footeroverlayopacity)) + { + chrome.Footer.OverlayOpacity = footeroverlayopacity; + } + break; + } + case "footeroverlaygradientdirection": + { + if (int.TryParse(entry.Value, out var footeroverlaygradientdirection) && Enum.IsDefined(typeof(Core.Model.SharePoint.OverlayGradientDirectionType), footeroverlaygradientdirection)) + { + chrome.Footer.OverlayGradientDirection = (Core.Model.SharePoint.OverlayGradientDirectionType)footeroverlaygradientdirection; + } + break; + } + case "footercolorindexinlightmode": + { + if (int.TryParse(entry.Value, out var footercolorindexinlightmode)) + { + chrome.Footer.ColorIndexInLightMode = footercolorindexinlightmode; + } + break; + } + case "footercolorindexindarkmode": + { + if (int.TryParse(entry.Value, out var footercolorindexindarkmode)) + { + chrome.Footer.ColorIndexInDarkMode = footercolorindexindarkmode; + } + break; + } + case "footeralignment": + { + if (int.TryParse(entry.Value, out var footerAlignment) && Enum.IsDefined(typeof(Core.Model.SharePoint.FooterLinkAlignment), footerAlignment)) + { + chrome.Footer.LinkAlignment = (Core.Model.SharePoint.FooterLinkAlignment)footerAlignment; + } + break; + } + case "fontoptionforsitefootertitle": + { + chrome.Font.SiteFooterTitle = System.Text.Json.JsonSerializer.Deserialize(entry.Value); + break; + } + case "fontoptionforsitefooternav": + { + chrome.Font.SiteFooterNav = System.Text.Json.JsonSerializer.Deserialize(entry.Value); + break; + } + } + } + } - //if (PnPProvisioningContext.Current != null) - //{ - // // Get an Access Token for the SetChromeOptions request - // var spoResourceUri = new Uri(web.Url).Authority; - // var accessToken = PnPProvisioningContext.Current.AcquireToken(spoResourceUri, null); - - // if (accessToken != null) - // { - // // Prepare the JSON request for SetChromeOptions - // var jsonRequest = new - // { - // footerEnabled = web.FooterEnabled, - // footerLayout = web.FooterLayout, - // footerEmphasis = web.FooterEmphasis - // }; - - // // Build the URL of the SetChromeOptions API - // var setChromeOptionsApiUrl = $"{web.Url}/_api/web/SetChromeOptions"; - - // // Make the POST request to the SetChromeOptions API - // // and fail in case of any exception - // HttpHelper.MakePostRequest(setChromeOptionsApiUrl, - // jsonRequest, - // "application/json", - // accessToken); - // } - //} - //else - //{ - // web.Update(); - // web.Context.ExecuteQueryRetry(); - //} + brandingManager.SetChromeOptions(chrome); + + //modern header background image + if (template?.PropertyBagEntries != null) + { + try + { + var footerbackgroundimageurl = template.PropertyBagEntries.FirstOrDefault(p => "footerbackgroundimageurl" == p.Key.ToLower())?.Value; + if (!string.IsNullOrWhiteSpace(footerbackgroundimageurl)) + { + var file = template.Files.FirstOrDefault(f => footerbackgroundimageurl.EndsWith(f.Src, StringComparison.InvariantCultureIgnoreCase)); + if (file != null) + { + var fileName = System.IO.Path.GetFileName(footerbackgroundimageurl); + var footerbackgroundimagefocalpoint = template.PropertyBagEntries.FirstOrDefault(p => "footerbackgroundimagefocalpoint" == p.Key.ToLower())?.Value; + if (!string.IsNullOrWhiteSpace(footerbackgroundimagefocalpoint)) + { + var focalPoint = System.Text.Json.JsonSerializer.Deserialize(footerbackgroundimagefocalpoint); + chrome.Footer.SetFooterBackgroundImage(fileName, Utilities.FileUtilities.GetFileStream(template, file), focalPoint.x, focalPoint.y, overwrite: true); + } + else + { + chrome.Footer.SetFooterBackgroundImage(fileName, Utilities.FileUtilities.GetFileStream(template, file), overwrite: true); + } + } + } + } + catch (Exception ex) + { + scope.LogError(ex, "An error occurred while setting the footer background image"); + } + } + } if (web.FooterEnabled) { @@ -570,7 +554,7 @@ public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate templ } return parser; } - + public override bool WillExtract(Web web, ProvisioningTemplate template, ProvisioningTemplateCreationInformation creationInfo) { if ((web.Context as ClientContext).Site.IsCommunicationSite()) @@ -635,5 +619,12 @@ private class MenuStateWrapper [JsonProperty("menuState")] public MenuState MenuState { get; set; } } + + private class FocalPoint + { + public double x { get; set; } + public double y { get; set; } + public string hash { get; set; } + } } } diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteHeaderSettings.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteHeaderSettings.cs index ed305c77c..462981b3d 100644 --- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteHeaderSettings.cs +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/ObjectSiteHeaderSettings.cs @@ -3,6 +3,8 @@ using PnP.Framework.Provisioning.Model; using PnP.Framework.Utilities; using System; +using System.Linq; +using System.Text.Json; namespace PnP.Framework.Provisioning.ObjectHandlers { @@ -17,58 +19,76 @@ public override string Name public override ProvisioningTemplate ExtractObjects(Web web, ProvisioningTemplate template, ProvisioningTemplateCreationInformation creationInfo) { - using (new PnPMonitoredScope(this.Name)) + using (var scope = new PnPMonitoredScope(this.Name)) { - web.EnsureProperties(w => w.HeaderEmphasis, w => w.HeaderLayout, w => w.MegaMenuEnabled); + web.EnsureProperties(w => w.AllProperties, w => w.Url); - var header = new SiteHeader + // Move to the PnP Core SDK context + using (var pnpCoreContext = PnPCoreSdk.Instance.GetPnPContext(web.Context as ClientContext)) { - MenuStyle = web.MegaMenuEnabled ? SiteHeaderMenuStyle.MegaMenu : SiteHeaderMenuStyle.Cascading - }; + // Get the Chrome options + var chrome = pnpCoreContext.Web.GetBrandingManager().GetChromeOptions(); + var header = new SiteHeader + { + ShowSiteTitle = !chrome.Header.HideTitle, + ShowSiteNavigation = chrome.Navigation.Visible, + MenuStyle = chrome.Navigation.MegaMenuEnabled ? SiteHeaderMenuStyle.MegaMenu : SiteHeaderMenuStyle.Cascading + }; - switch (web.HeaderLayout) - { - case HeaderLayoutType.Compact: - { + switch(chrome.Header.Layout) + { + case Core.Model.SharePoint.HeaderLayoutType.Compact: header.Layout = SiteHeaderLayout.Compact; break; - } - - case HeaderLayoutType.Minimal: - { + case Core.Model.SharePoint.HeaderLayoutType.Minimal: header.Layout = SiteHeaderLayout.Minimal; break; - } - - case HeaderLayoutType.Extended: - { + case Core.Model.SharePoint.HeaderLayoutType.Extended: header.Layout = SiteHeaderLayout.Extended; break; - } - - default: - { + default: header.Layout = SiteHeaderLayout.Standard; break; - } - } + } - if (Enum.TryParse(web.HeaderEmphasis.ToString(), out Emphasis backgroundEmphasis)) - { - header.BackgroundEmphasis = backgroundEmphasis; + switch(chrome.Header.Emphasis) + { + case Core.Model.SharePoint.VariantThemeType.Neutral: + header.BackgroundEmphasis = Emphasis.Neutral; + break; + case Core.Model.SharePoint.VariantThemeType.Soft: + header.BackgroundEmphasis = Emphasis.Soft; + break; + case Core.Model.SharePoint.VariantThemeType.Strong: + header.BackgroundEmphasis = Emphasis.Strong; + break; + default: + header.BackgroundEmphasis = Emphasis.None; + break; + } + template.Header = header; } - // Move to the PnP Core SDK context - using (var pnpCoreContext = PnPCoreSdk.Instance.GetPnPContext(web.Context as ClientContext)) + if (creationInfo.PersistBrandingFiles) { - // Get the Chrome options - var chrome = pnpCoreContext.Web.GetBrandingManager().GetChromeOptions(); + //Header Background Image + var backgroundImageUrl = web.GetPropertyBagValueString("BackgroundImageUrl", ""); + if (!string.IsNullOrWhiteSpace(backgroundImageUrl)) + { + Uri webUri = new Uri(web.Url); + string webUrl = $"{webUri.Scheme}://{webUri.DnsSafeHost}"; + backgroundImageUrl = backgroundImageUrl.Replace(webUrl, ""); - header.ShowSiteTitle = !chrome.Header.HideTitle; - header.ShowSiteNavigation = chrome.Navigation.Visible; - } + if (Utilities.FileUtilities.PersistFile(web, creationInfo, scope, this, backgroundImageUrl)) + { + template.Files.Add(GetTemplateFile(web, backgroundImageUrl)); + } - template.Header = header; + var files = template.Files.Distinct().ToList(); + template.Files.Clear(); + template.Files.AddRange(files); + } + } } return template; @@ -76,66 +96,149 @@ public override ProvisioningTemplate ExtractObjects(Web web, ProvisioningTemplat public override TokenParser ProvisionObjects(Web web, ProvisioningTemplate template, TokenParser parser, ProvisioningTemplateApplyingInformation applyingInformation) { - using (new PnPMonitoredScope(this.Name)) + using (var scope=new PnPMonitoredScope(this.Name)) { if (template.Header == null) { return parser; } - web.EnsureProperties(w => w.Url, w => w.HeaderLayout); + web.EnsureProperties(w => w.Url); - switch (template.Header.Layout) + // Move to the PnP Core SDK context + using (var pnpCoreContext = PnPCoreSdk.Instance.GetPnPContext(web.Context as ClientContext)) { - case SiteHeaderLayout.Compact: - { - web.HeaderLayout = HeaderLayoutType.Compact; - break; - } + // Get the Chrome options + var brandingManager = pnpCoreContext.Web.GetBrandingManager(); - case SiteHeaderLayout.Minimal: - { - web.HeaderLayout = HeaderLayoutType.Minimal; - break; - } + var chrome = brandingManager.GetChromeOptions(); - case SiteHeaderLayout.Extended: - { - web.HeaderLayout = HeaderLayoutType.Extended; + chrome.Header.HideTitle = !template.Header.ShowSiteTitle; + switch (template.Header.Layout) + { + case SiteHeaderLayout.Compact: + chrome.Header.Layout = Core.Model.SharePoint.HeaderLayoutType.Compact; break; - } + case SiteHeaderLayout.Minimal: + chrome.Header.Layout = Core.Model.SharePoint.HeaderLayoutType.Minimal; + break; + case SiteHeaderLayout.Extended: + chrome.Header.Layout = Core.Model.SharePoint.HeaderLayoutType.Extended; + break; + default: + chrome.Header.Layout = Core.Model.SharePoint.HeaderLayoutType.Standard; + break; + } - case SiteHeaderLayout.Standard: + switch (template.Header.BackgroundEmphasis) { - web.HeaderLayout = HeaderLayoutType.Standard; + case Emphasis.Neutral: + chrome.Header.Emphasis = Core.Model.SharePoint.VariantThemeType.Neutral; break; - } - } - - web.HeaderEmphasis = (SPVariantThemeType)Enum.Parse(typeof(SPVariantThemeType), template.Header.BackgroundEmphasis.ToString()); - web.MegaMenuEnabled = template.Header.MenuStyle == SiteHeaderMenuStyle.MegaMenu; - web.HideTitleInHeader = !template.Header.ShowSiteTitle; + case Emphasis.Soft: + chrome.Header.Emphasis = Core.Model.SharePoint.VariantThemeType.Soft; + break; + case Emphasis.Strong: + chrome.Header.Emphasis = Core.Model.SharePoint.VariantThemeType.Strong; + break; + default: + chrome.Header.Emphasis = Core.Model.SharePoint.VariantThemeType.None; + break; + } + chrome.Navigation.Visible = template.Header.ShowSiteNavigation; + chrome.Navigation.MegaMenuEnabled = template.Header.MenuStyle == SiteHeaderMenuStyle.MegaMenu; - var jsonRequest = new - { - headerLayout = web.HeaderLayout, - headerEmphasis = web.HeaderEmphasis, - megaMenuEnabled = web.MegaMenuEnabled, - hideTitleInHeader = web.HideTitleInHeader - }; - - web.ExecutePostAsync("/_api/web/SetChromeOptions", System.Text.Json.JsonSerializer.Serialize(jsonRequest)).GetAwaiter().GetResult(); + //Modern header settings + if(template?.PropertyBagEntries !=null) + { + foreach (var entry in template.PropertyBagEntries) + { + if (string.IsNullOrWhiteSpace(entry.Value)) + { + continue; + } + switch (entry.Key.ToLower()) + { + case "headeroverlaycolor": + { + if (int.TryParse(entry.Value, out var headerOverlayColor) && Enum.IsDefined(typeof(OverlayColorType), headerOverlayColor)) + { + chrome.Header.OverlayColor = (Core.Model.SharePoint.OverlayColorType)(OverlayColorType)headerOverlayColor; + } + break; + } + case "headeroverlayopacity": + { + if (int.TryParse(entry.Value, out var headerOverlayOpacity)) + { + chrome.Header.OverlayOpacity = headerOverlayOpacity; + } + break; + } + case "headeroverlaygradientdirection": + { + if (int.TryParse(entry.Value, out var headerOverlayGradientDirection) && Enum.IsDefined(typeof(Core.Model.SharePoint.OverlayGradientDirectionType), headerOverlayGradientDirection)) + { + chrome.Header.OverlayGradientDirection = (Core.Model.SharePoint.OverlayGradientDirectionType)headerOverlayGradientDirection; + } + break; + } + case "headercolorindexinlightmode": + { + if (int.TryParse(entry.Value, out var headercolorindexinlightmode)) + { + chrome.Header.ColorIndexInLightMode = headercolorindexinlightmode; + } + break; + } + case "headercolorindexindarkmode": + { + if (int.TryParse(entry.Value, out var headercolorindexindarkmode)) + { + chrome.Header.ColorIndexInDarkMode = headercolorindexindarkmode; + } + break; + } + case "fontoptionforsitetitle": + { + chrome.Font.SiteTitle = JsonSerializer.Deserialize(entry.Value); + break; + } + case "fontoptionforsitenav": + { + chrome.Font.SiteNav = JsonSerializer.Deserialize(entry.Value); + break; + } - // Move to the PnP Core SDK context - using (var pnpCoreContext = PnPCoreSdk.Instance.GetPnPContext(web.Context as ClientContext)) - { - // Get the Chrome options - var chrome = pnpCoreContext.Web.GetBrandingManager().GetChromeOptions(); + } + } + } - chrome.Header.HideTitle = !template.Header.ShowSiteTitle; - chrome.Navigation.Visible = template.Header.ShowSiteNavigation; + brandingManager.SetChromeOptions(chrome); - pnpCoreContext.Web.GetBrandingManager().SetChromeOptions(chrome); + //modern header background image + if (template?.PropertyBagEntries != null) + { + try + { + var backgroundImageUrl = template.PropertyBagEntries.FirstOrDefault(p => "backgroundimageurl" == p.Key.ToLower())?.Value; + if (!string.IsNullOrWhiteSpace(backgroundImageUrl)) + { + var file = template.Files.FirstOrDefault(f => backgroundImageUrl.EndsWith(f.Src, StringComparison.InvariantCultureIgnoreCase)); + if (file != null) + { + var fileName = System.IO.Path.GetFileName(backgroundImageUrl); + chrome.Header.SetHeaderBackgroundImage(fileName, Utilities.FileUtilities.GetFileStream(template, file), overwrite: true); + } + } + } + catch (Exception ex) + { + // Swallowing the exception as this is not a critical operation + // and we don't want to fail the whole provisioning + scope.LogWarning($"An error occurred while setting the header background image: {ex.Message}"); + } + } } } @@ -151,5 +254,28 @@ public override bool WillProvision(Web web, ProvisioningTemplate template, Provi { return template.Header != null; } + + private Model.File GetTemplateFile(Web web, string serverRelativeUrl) + { + + var webServerUrl = web.EnsureProperty(w => w.Url); + var serverUri = new Uri(webServerUrl); + var serverUrl = $"{serverUri.Scheme}://{serverUri.Authority}"; + var fullUri = new Uri(UrlUtility.Combine(serverUrl, serverRelativeUrl)); + + var folderPath = fullUri.Segments.Take(fullUri.Segments.Length - 1).ToArray().Aggregate((i, x) => i + x).TrimEnd('/'); + var fileName = fullUri.Segments[fullUri.Segments.Length - 1]; + + // store as site relative path + folderPath = folderPath.Replace(web.ServerRelativeUrl, "").Trim('/'); + var templateFile = new Model.File() + { + Folder = Tokenize(folderPath, web.Url), + Src = !string.IsNullOrEmpty(folderPath) ? $"{folderPath}/{fileName}" : fileName, + Overwrite = true, + }; + + return templateFile; + } } } diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FileUtilities.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FileUtilities.cs index 214192607..4a3d8522c 100644 --- a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FileUtilities.cs +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FileUtilities.cs @@ -1,4 +1,7 @@ -using PnP.Framework.Provisioning.Model; +using Microsoft.SharePoint.Client; +using PnP.Framework.Diagnostics; +using PnP.Framework.Provisioning.Model; +using PnP.Framework.Utilities; using System; using System.Collections.Generic; using System.IO; @@ -135,6 +138,100 @@ public static Dictionary> GetMetadataProperti return (result); } + + internal static bool PersistFile(Web web, ProvisioningTemplateCreationInformation creationInfo, PnPMonitoredScope scope, ObjectHandlerBase objectHandlerBase, string serverRelativeUrl) + { + var success = false; + if (creationInfo.PersistBrandingFiles) + { + if (creationInfo.FileConnector != null) + { + if (UrlUtility.IsIisVirtualDirectory(serverRelativeUrl)) + { + scope.LogWarning("File is not located in the content database. Not retrieving {0}", serverRelativeUrl); + return success; + } + + try + { + var file = web.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(serverRelativeUrl)); + string fileName = string.Empty; + if (serverRelativeUrl.IndexOf("/") > -1) + { + fileName = serverRelativeUrl.Substring(serverRelativeUrl.LastIndexOf("/") + 1); + } + else + { + fileName = serverRelativeUrl; + } + web.Context.Load(file); + web.Context.ExecuteQueryRetry(); + ClientResult stream = file.OpenBinaryStream(); + web.Context.ExecuteQueryRetry(); + + file.EnsureProperty(f => f.ServerRelativePath); + var baseUri = new Uri(web.Url); + var fullUri = new Uri(baseUri, file.ServerRelativePath.DecodedUrl); + var folderPath = Uri.UnescapeDataString(fullUri.Segments.Take(fullUri.Segments.Length - 1).ToArray().Aggregate((i, x) => i + x).TrimEnd('/')); + + // Configure the filename to use + fileName = Uri.UnescapeDataString(fullUri.Segments[fullUri.Segments.Length - 1]); + + // Build up a site relative container URL...might end up empty as well + String container = Uri.UnescapeDataString(folderPath.Replace(web.ServerRelativeUrl, "")).Trim('/').Replace("/", "\\"); + + using (Stream memStream = new MemoryStream()) + { + CopyStream(stream.Value, memStream); + memStream.Position = 0; + if (!string.IsNullOrEmpty(container)) + { + creationInfo.FileConnector.SaveFileStream(fileName, container, memStream); + } + else + { + creationInfo.FileConnector.SaveFileStream(fileName, memStream); + } + } + success = true; + } + catch (ServerException ex1) + { + // If we are referring a file from a location outside of the current web or at a location where we cannot retrieve the file an exception is thrown. We swallow this exception. + if (ex1.ServerErrorCode != -2147024809) + { + throw; + } + else + { + scope.LogWarning("File is not necessarily located in the current web. Not retrieving {0}", serverRelativeUrl); + } + } + } + else + { + objectHandlerBase.WriteMessage("No connector present to persist footer logo.", ProvisioningMessageType.Error); + scope.LogError("No connector present to persist footer logo."); + } + } + else + { + success = true; + } + return success; + } + + private static void CopyStream(Stream source, Stream destination) + { + byte[] buffer = new byte[32768]; + int bytesRead; + + do + { + bytesRead = source.Read(buffer, 0, buffer.Length); + destination.Write(buffer, 0, bytesRead); + } while (bytesRead != 0); + } } } diff --git a/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FontOption.cs b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FontOption.cs new file mode 100644 index 000000000..3468acbfa --- /dev/null +++ b/src/lib/PnP.Framework/Provisioning/ObjectHandlers/Utilities/FontOption.cs @@ -0,0 +1,24 @@ +using PnP.Core.Model.SharePoint; +using System.Text.Json.Serialization; + +namespace PnP.Framework.Provisioning.ObjectHandlers.Utilities +{ + internal class FontOption : IFontOption + { + /// + /// fontFamilyKey + /// + [JsonPropertyName("fontFamilyKey")] + public string FamilyKey { get; set; } + /// + /// fontFace + /// + [JsonPropertyName("fontFace")] + public string Face { get; set; } + /// + /// fontVariantWeight + /// + [JsonPropertyName("fontVariantWeight")] + public string VariantWeight { get; set; } + } +}