public void ExtractClientSidePage(Web web, ProvisioningTemplate template, ProvisioningTemplateCreationInformation creationInfo, PnPMonitoredScope scope, string pageUrl, string pageName, bool isHomePage, bool isTemplate = false) { PageToExport page = new PageToExport() { PageName = pageName, PageUrl = pageUrl, IsHomePage = isHomePage, IsTemplate = isTemplate, IsTranslation = false }; ExtractClientSidePage(web, template, creationInfo, scope, page); }
/// <summary> /// Extracts a client side page /// </summary> /// <param name="web">Web to extract the page from</param> /// <param name="template">Current provisioning template that will hold the extracted page</param> /// <param name="creationInfo">ProvisioningTemplateCreationInformation passed into the provisioning engine</param> /// <param name="scope">Scope used for logging</param> /// <param name="page">page to be exported</param> public void ExtractClientSidePage(Web web, ProvisioningTemplate template, ProvisioningTemplateCreationInformation creationInfo, PnPMonitoredScope scope, PageToExport page) { bool excludeAuthorInformation = false; if (creationInfo.ExtractConfiguration != null && creationInfo.ExtractConfiguration.Pages != null) { excludeAuthorInformation = creationInfo.ExtractConfiguration.Pages.ExcludeAuthorInformation; } try { List <string> errorneousOrNonImageFileGuids = new List <string>(); var pageToExtract = web.LoadClientSidePage(page.PageName); if (pageToExtract.Sections.Count == 0 && pageToExtract.Controls.Count == 0 && page.IsHomePage) { // This is default home page which was not customized...and as such there's no page definition stored in the list item. We don't need to extact this page. scope.LogInfo(CoreResources.Provisioning_ObjectHandlers_ClientSidePageContents_DefaultHomePage); } else { // Get the page content type string pageContentTypeId = pageToExtract.PageListItem[ContentTypeIdField].ToString(); if (!string.IsNullOrEmpty(pageContentTypeId)) { pageContentTypeId = GetParentIdValue(pageContentTypeId); } var isNews = pageToExtract.LayoutType != Pages.ClientSidePageLayoutType.Home && int.Parse(pageToExtract.PageListItem[OfficeDevPnP.Core.Pages.ClientSidePage.PromotedStateField].ToString()) == (int)Pages.PromotedState.Promoted; // Create the page; BaseClientSidePage extractedPageInstance; if (page.IsTranslation) { extractedPageInstance = new TranslatedClientSidePage(); (extractedPageInstance as TranslatedClientSidePage).PageName = page.PageName; } else { extractedPageInstance = new ClientSidePage(); (extractedPageInstance as ClientSidePage).PageName = page.PageName; } extractedPageInstance.PromoteAsNewsArticle = isNews; extractedPageInstance.PromoteAsTemplate = page.IsTemplate; extractedPageInstance.Overwrite = true; extractedPageInstance.Publish = true; extractedPageInstance.Layout = pageToExtract.LayoutType.ToString(); extractedPageInstance.EnableComments = !pageToExtract.CommentsDisabled; extractedPageInstance.Title = pageToExtract.PageTitle; extractedPageInstance.ContentTypeID = !pageContentTypeId.Equals(BuiltInContentTypeId.ModernArticlePage, StringComparison.InvariantCultureIgnoreCase) ? pageContentTypeId : null; extractedPageInstance.ThumbnailUrl = pageToExtract.ThumbnailUrl != null?TokenizeJsonControlData(web, pageToExtract.ThumbnailUrl) : ""; if (pageToExtract.PageHeader != null) { var extractedHeader = new ClientSidePageHeader() { Type = (ClientSidePageHeaderType)Enum.Parse(typeof(Pages.ClientSidePageHeaderType), pageToExtract.PageHeader.Type.ToString()), ServerRelativeImageUrl = TokenizeJsonControlData(web, pageToExtract.PageHeader.ImageServerRelativeUrl), TranslateX = pageToExtract.PageHeader.TranslateX, TranslateY = pageToExtract.PageHeader.TranslateY, LayoutType = (ClientSidePageHeaderLayoutType)Enum.Parse(typeof(Pages.ClientSidePageHeaderLayoutType), pageToExtract.PageHeader.LayoutType.ToString()), #if !SP2019 TextAlignment = (ClientSidePageHeaderTextAlignment)Enum.Parse(typeof(Pages.ClientSidePageHeaderTitleAlignment), pageToExtract.PageHeader.TextAlignment.ToString()), ShowTopicHeader = pageToExtract.PageHeader.ShowTopicHeader, ShowPublishDate = pageToExtract.PageHeader.ShowPublishDate, TopicHeader = pageToExtract.PageHeader.TopicHeader, AlternativeText = pageToExtract.PageHeader.AlternativeText, Authors = !excludeAuthorInformation ? pageToExtract.PageHeader.Authors : "", AuthorByLine = !excludeAuthorInformation ? pageToExtract.PageHeader.AuthorByLine : "", AuthorByLineId = !excludeAuthorInformation ? pageToExtract.PageHeader.AuthorByLineId : -1 #endif }; extractedPageInstance.Header = extractedHeader; // Add the page header image to template if that was requested if (creationInfo.PersistBrandingFiles && !string.IsNullOrEmpty(pageToExtract.PageHeader.ImageServerRelativeUrl)) { IncludePageHeaderImageInExport(web, pageToExtract.PageHeader.ImageServerRelativeUrl, template, creationInfo, scope); } } // define reusable RegEx pre-compiled objects string guidPattern = "\"{?[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}}?\""; Regex regexGuidPattern = new Regex(guidPattern, RegexOptions.Compiled); string guidPatternEncoded = "=[a-fA-F0-9]{8}(?:%2D|-)([a-fA-F0-9]{4}(?:%2D|-)){3}[a-fA-F0-9]{12}"; Regex regexGuidPatternEncoded = new Regex(guidPatternEncoded, RegexOptions.Compiled); string guidPatternNoDashes = "[a-fA-F0-9]{32}"; Regex regexGuidPatternNoDashes = new Regex(guidPatternNoDashes, RegexOptions.Compiled); string siteAssetUrlsPattern = "(?:\")(?<AssetUrl>[\\w|\\.|\\/|:|-]*\\/SiteAssets\\/SitePages\\/[\\w|\\.|\\/|:|-]*)(?:\")"; // OLD RegEx with Catastrophic Backtracking: @".*""(.*?/SiteAssets/SitePages/.+?)"".*"; Regex regexSiteAssetUrls = new Regex(siteAssetUrlsPattern, RegexOptions.Compiled); if (creationInfo.PersistBrandingFiles && !string.IsNullOrEmpty(extractedPageInstance.ThumbnailUrl)) { var thumbnailFileIds = new List <Guid>(); CollectImageFilesFromGenericGuids(regexGuidPatternNoDashes, null, extractedPageInstance.ThumbnailUrl, thumbnailFileIds); if (thumbnailFileIds.Count == 1) { var file = web.GetFileById(thumbnailFileIds[0]); web.Context.Load(file, f => f.Level, f => f.ServerRelativeUrl, f => f.UniqueId); web.Context.ExecuteQueryRetry(); // Item1 = was file added to the template // Item2 = file name (if file found) var imageAddedTuple = LoadAndAddPageImage(web, file, template, creationInfo, scope); if (imageAddedTuple.Item1) { extractedPageInstance.ThumbnailUrl = Regex.Replace(extractedPageInstance.ThumbnailUrl, file.UniqueId.ToString("N"), $"{{fileuniqueid:{file.ServerRelativeUrl.Substring(web.ServerRelativeUrl.Length).TrimStart("/".ToCharArray())}}}"); } } } // Add the sections foreach (var section in pageToExtract.Sections) { // Set order var sectionInstance = new CanvasSection() { Order = section.Order, BackgroundEmphasis = (Emphasis)section.ZoneEmphasis, }; if (section.VerticalSectionColumn != null) { sectionInstance.VerticalSectionEmphasis = (Emphasis)section.VerticalSectionColumn.VerticalSectionEmphasis; } // Set section type switch (section.Type) { case Pages.CanvasSectionTemplate.OneColumn: sectionInstance.Type = CanvasSectionType.OneColumn; break; case Pages.CanvasSectionTemplate.TwoColumn: sectionInstance.Type = CanvasSectionType.TwoColumn; break; case Pages.CanvasSectionTemplate.TwoColumnLeft: sectionInstance.Type = CanvasSectionType.TwoColumnLeft; break; case Pages.CanvasSectionTemplate.TwoColumnRight: sectionInstance.Type = CanvasSectionType.TwoColumnRight; break; case Pages.CanvasSectionTemplate.ThreeColumn: sectionInstance.Type = CanvasSectionType.ThreeColumn; break; case Pages.CanvasSectionTemplate.OneColumnFullWidth: sectionInstance.Type = CanvasSectionType.OneColumnFullWidth; break; #if !SP2019 case Pages.CanvasSectionTemplate.OneColumnVerticalSection: sectionInstance.Type = CanvasSectionType.OneColumnVerticalSection; break; case Pages.CanvasSectionTemplate.TwoColumnVerticalSection: sectionInstance.Type = CanvasSectionType.TwoColumnVerticalSection; break; case Pages.CanvasSectionTemplate.TwoColumnLeftVerticalSection: sectionInstance.Type = CanvasSectionType.TwoColumnLeftVerticalSection; break; case Pages.CanvasSectionTemplate.TwoColumnRightVerticalSection: sectionInstance.Type = CanvasSectionType.TwoColumnRightVerticalSection; break; case Pages.CanvasSectionTemplate.ThreeColumnVerticalSection: sectionInstance.Type = CanvasSectionType.ThreeColumnVerticalSection; break; #endif default: sectionInstance.Type = CanvasSectionType.OneColumn; break; } // Add controls to section foreach (var column in section.Columns) { foreach (var control in column.Controls) { // Create control CanvasControl controlInstance = new CanvasControl() { Column = column.IsVerticalSectionColumn ? section.Columns.IndexOf(column) + 1 : column.Order, ControlId = control.InstanceId, Order = control.Order, }; // Set control type if (control.Type == typeof(Pages.ClientSideText)) { controlInstance.Type = WebPartType.Text; // Set text content controlInstance.ControlProperties = new System.Collections.Generic.Dictionary <string, string>(1) { { "Text", TokenizeJsonTextData(web, (control as Pages.ClientSideText).Text) } }; } else { // set ControlId to webpart id controlInstance.ControlId = Guid.Parse((control as Pages.ClientSideWebPart).WebPartId); var webPartType = Pages.ClientSidePage.NameToClientSideWebPartEnum((control as Pages.ClientSideWebPart).WebPartId); switch (webPartType) { case Pages.DefaultClientSideWebParts.ContentRollup: controlInstance.Type = WebPartType.ContentRollup; break; #if !SP2019 case Pages.DefaultClientSideWebParts.BingMap: controlInstance.Type = WebPartType.BingMap; break; case Pages.DefaultClientSideWebParts.Button: controlInstance.Type = WebPartType.Button; break; case Pages.DefaultClientSideWebParts.CallToAction: controlInstance.Type = WebPartType.CallToAction; break; case Pages.DefaultClientSideWebParts.News: controlInstance.Type = WebPartType.News; break; case Pages.DefaultClientSideWebParts.PowerBIReportEmbed: controlInstance.Type = WebPartType.PowerBIReportEmbed; break; case Pages.DefaultClientSideWebParts.Sites: controlInstance.Type = WebPartType.Sites; break; case Pages.DefaultClientSideWebParts.GroupCalendar: controlInstance.Type = WebPartType.GroupCalendar; break; case Pages.DefaultClientSideWebParts.MicrosoftForms: controlInstance.Type = WebPartType.MicrosoftForms; break; case Pages.DefaultClientSideWebParts.ClientWebPart: controlInstance.Type = WebPartType.ClientWebPart; break; #endif case Pages.DefaultClientSideWebParts.ContentEmbed: controlInstance.Type = WebPartType.ContentEmbed; break; case Pages.DefaultClientSideWebParts.DocumentEmbed: controlInstance.Type = WebPartType.DocumentEmbed; break; case Pages.DefaultClientSideWebParts.Image: controlInstance.Type = WebPartType.Image; break; case Pages.DefaultClientSideWebParts.ImageGallery: controlInstance.Type = WebPartType.ImageGallery; break; case Pages.DefaultClientSideWebParts.LinkPreview: controlInstance.Type = WebPartType.LinkPreview; break; case Pages.DefaultClientSideWebParts.NewsFeed: controlInstance.Type = WebPartType.NewsFeed; break; case Pages.DefaultClientSideWebParts.NewsReel: controlInstance.Type = WebPartType.NewsReel; break; case Pages.DefaultClientSideWebParts.QuickChart: controlInstance.Type = WebPartType.QuickChart; break; case Pages.DefaultClientSideWebParts.SiteActivity: controlInstance.Type = WebPartType.SiteActivity; break; case Pages.DefaultClientSideWebParts.VideoEmbed: controlInstance.Type = WebPartType.VideoEmbed; break; case Pages.DefaultClientSideWebParts.YammerEmbed: controlInstance.Type = WebPartType.YammerEmbed; break; case Pages.DefaultClientSideWebParts.Events: controlInstance.Type = WebPartType.Events; break; case Pages.DefaultClientSideWebParts.Hero: controlInstance.Type = WebPartType.Hero; break; case Pages.DefaultClientSideWebParts.List: controlInstance.Type = WebPartType.List; break; case Pages.DefaultClientSideWebParts.PageTitle: controlInstance.Type = WebPartType.PageTitle; break; case Pages.DefaultClientSideWebParts.People: controlInstance.Type = WebPartType.People; break; case Pages.DefaultClientSideWebParts.QuickLinks: controlInstance.Type = WebPartType.QuickLinks; break; case Pages.DefaultClientSideWebParts.CustomMessageRegion: controlInstance.Type = WebPartType.CustomMessageRegion; break; case Pages.DefaultClientSideWebParts.Divider: controlInstance.Type = WebPartType.Divider; break; case Pages.DefaultClientSideWebParts.Spacer: controlInstance.Type = WebPartType.Spacer; break; case Pages.DefaultClientSideWebParts.ThirdParty: controlInstance.Type = WebPartType.Custom; break; default: controlInstance.Type = WebPartType.Custom; break; } if (excludeAuthorInformation) { #if !SP2019 if (webPartType == Pages.DefaultClientSideWebParts.News) { var properties = (control as Pages.ClientSideWebPart).Properties; var authorTokens = properties.SelectTokens("$..author").ToList(); foreach (var authorToken in authorTokens) { authorToken.Parent.Remove(); } var authorAccountNameTokens = properties.SelectTokens("$..authorAccountName").ToList(); foreach (var authorAccountNameToken in authorAccountNameTokens) { authorAccountNameToken.Parent.Remove(); } (control as Pages.ClientSideWebPart).PropertiesJson = properties.ToString(); } #endif } string jsonControlData = "\"id\": \"" + (control as Pages.ClientSideWebPart).WebPartId + "\", \"instanceId\": \"" + (control as Pages.ClientSideWebPart).InstanceId + "\", \"title\": " + JsonConvert.ToString((control as Pages.ClientSideWebPart).Title) + ", \"description\": " + JsonConvert.ToString((control as Pages.ClientSideWebPart).Description) + ", \"dataVersion\": \"" + (control as Pages.ClientSideWebPart).DataVersion + "\", \"properties\": " + (control as Pages.ClientSideWebPart).PropertiesJson + ""; // set the control properties if ((control as Pages.ClientSideWebPart).ServerProcessedContent != null) { // If we have serverProcessedContent then also export that one, it's important as some controls depend on this information to be present string serverProcessedContent = (control as Pages.ClientSideWebPart).ServerProcessedContent.ToString(Formatting.None); jsonControlData = jsonControlData + ", \"serverProcessedContent\": " + serverProcessedContent + ""; } if ((control as Pages.ClientSideWebPart).DynamicDataPaths != null) { // If we have serverProcessedContent then also export that one, it's important as some controls depend on this information to be present string dynamicDataPaths = (control as Pages.ClientSideWebPart).DynamicDataPaths.ToString(Formatting.None); jsonControlData = jsonControlData + ", \"dynamicDataPaths\": " + dynamicDataPaths + ""; } if ((control as Pages.ClientSideWebPart).DynamicDataValues != null) { // If we have serverProcessedContent then also export that one, it's important as some controls depend on this information to be present string dynamicDataValues = (control as Pages.ClientSideWebPart).DynamicDataValues.ToString(Formatting.None); jsonControlData = jsonControlData + ", \"dynamicDataValues\": " + dynamicDataValues + ""; } controlInstance.JsonControlData = "{" + jsonControlData + "}"; var untokenizedJsonControlData = controlInstance.JsonControlData; // Tokenize the JsonControlData controlInstance.JsonControlData = TokenizeJsonControlData(web, controlInstance.JsonControlData); // Export relevant files if this flag is set if (creationInfo.PersistBrandingFiles) { List <Guid> fileGuids = new List <Guid>(); Dictionary <string, string> exportedFiles = new Dictionary <string, string>(); Dictionary <string, string> exportedPages = new Dictionary <string, string>(); CollectSiteAssetImageFiles(regexSiteAssetUrls, web, untokenizedJsonControlData, fileGuids); CollectImageFilesFromGenericGuids(regexGuidPattern, regexGuidPatternEncoded, untokenizedJsonControlData, fileGuids); // Iterate over the found guids to see if they're exportable files foreach (var uniqueId in fileGuids) { try { if (!exportedFiles.ContainsKey(uniqueId.ToString()) && !errorneousOrNonImageFileGuids.Contains(uniqueId.ToString())) { // Try to see if this is a file var file = web.GetFileById(uniqueId); web.Context.Load(file, f => f.Level, f => f.ServerRelativeUrl); web.Context.ExecuteQueryRetry(); // Item1 = was file added to the template // Item2 = file name (if file found) var imageAddedTuple = LoadAndAddPageImage(web, file, template, creationInfo, scope); if (!string.IsNullOrEmpty(imageAddedTuple.Item2)) { if (!imageAddedTuple.Item2.EndsWith(".aspx", StringComparison.InvariantCultureIgnoreCase)) { if (imageAddedTuple.Item1) { // Keep track of the exported file path and it's UniqueId exportedFiles.Add(uniqueId.ToString(), file.ServerRelativeUrl.Substring(web.ServerRelativeUrl.Length).TrimStart("/".ToCharArray())); } } else { if (!exportedPages.ContainsKey(uniqueId.ToString())) { exportedPages.Add(uniqueId.ToString(), file.ServerRelativeUrl.Substring(web.ServerRelativeUrl.Length).TrimStart("/".ToCharArray())); } } } } } catch (Exception ex) { scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_ClientSidePageContents_ErrorDuringFileExport, ex.Message); errorneousOrNonImageFileGuids.Add(uniqueId.ToString()); } } // Tokenize based on the found files, use a different token for encoded guids do we can later on replace by a new encoded guid foreach (var exportedFile in exportedFiles) { controlInstance.JsonControlData = Regex.Replace(controlInstance.JsonControlData, exportedFile.Key.Replace("-", "%2D"), $"{{fileuniqueidencoded:{exportedFile.Value}}}", RegexOptions.IgnoreCase); controlInstance.JsonControlData = Regex.Replace(controlInstance.JsonControlData, exportedFile.Key, $"{{fileuniqueid:{exportedFile.Value}}}", RegexOptions.IgnoreCase); } foreach (var exportedPage in exportedPages) { controlInstance.JsonControlData = Regex.Replace(controlInstance.JsonControlData, exportedPage.Key.Replace("-", "%2D"), $"{{pageuniqueidencoded:{exportedPage.Value}}}", RegexOptions.IgnoreCase); controlInstance.JsonControlData = Regex.Replace(controlInstance.JsonControlData, exportedPage.Key, $"{{pageuniqueid:{exportedPage.Value}}}", RegexOptions.IgnoreCase); } } } // add control to section sectionInstance.Controls.Add(controlInstance); } } extractedPageInstance.Sections.Add(sectionInstance); } // Renumber the sections...when editing modern homepages you can end up with section with order 0.5 or 0.75...let's ensure we render section as of 1 int sectionOrder = 1; foreach (var sectionInstance in extractedPageInstance.Sections) { sectionInstance.Order = sectionOrder; sectionOrder++; } #if !SP2019 // Spaces support if (pageToExtract.LayoutType == Pages.ClientSidePageLayoutType.Spaces && !string.IsNullOrEmpty(pageToExtract.SpaceContent)) { extractedPageInstance.FieldValues.Add(Pages.ClientSidePage.SpaceContentField, pageToExtract.SpaceContent); } #endif // Add the page to the template if (page.IsTranslation) { var parentPage = template.ClientSidePages.Where(p => p.PageName == page.SourcePageName).FirstOrDefault(); if (parentPage != null) { var translatedPageInstance = (TranslatedClientSidePage)extractedPageInstance; translatedPageInstance.LCID = new CultureInfo(page.Language).LCID; parentPage.Translations.Add(translatedPageInstance); } } else { var clientSidePageInstance = (ClientSidePage)extractedPageInstance; if (page.TranslatedLanguages != null && page.TranslatedLanguages.Count > 0) { clientSidePageInstance.CreateTranslations = true; clientSidePageInstance.LCID = Convert.ToInt32(web.EnsureProperty(p => p.Language)); } template.ClientSidePages.Add(clientSidePageInstance); } // Set the homepage if (page.IsHomePage) { if (template.WebSettings == null) { template.WebSettings = new WebSettings(); } if (page.PageUrl.StartsWith(web.ServerRelativeUrl, StringComparison.InvariantCultureIgnoreCase)) { template.WebSettings.WelcomePage = page.PageUrl.Replace(web.ServerRelativeUrl + "/", ""); } else { template.WebSettings.WelcomePage = page.PageUrl; } } } } catch (ArgumentException ex) { scope.LogWarning(CoreResources.Provisioning_ObjectHandlers_ClientSidePageContents_NoValidPage, ex.Message); } }