diff --git a/Commands/Base/BaseFileProvisioningCmdlet.cs b/Commands/Base/BaseFileProvisioningCmdlet.cs new file mode 100644 index 000000000..b2b01daa4 --- /dev/null +++ b/Commands/Base/BaseFileProvisioningCmdlet.cs @@ -0,0 +1,304 @@ +using Microsoft.SharePoint.Client; +using Microsoft.SharePoint.Client.Utilities; +using OfficeDevPnP.Core.Framework.Provisioning.Connectors; +using OfficeDevPnP.Core.Framework.Provisioning.Model; +using OfficeDevPnP.Core.Framework.Provisioning.Providers; +using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml; +using SharePointPnP.PowerShell.Commands.Provisioning; +using SharePointPnP.PowerShell.Commands.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text; +using PnPFileLevel = OfficeDevPnP.Core.Framework.Provisioning.Model.FileLevel; +using SPFile = Microsoft.SharePoint.Client.File; + +namespace SharePointPnP.PowerShell.Commands +{ + /// + /// Base class for commands related to adding file to template + /// + public class BaseFileProvisioningCmdlet : PnPWebCmdlet + { + protected const string PSNAME_LOCAL_SOURCE = "LocalSourceFile"; + protected const string PSNAME_REMOTE_SOURCE = "RemoteSourceFile"; + + [Parameter(Mandatory = true, Position = 0, HelpMessage = "Filename of the .PNP Open XML provisioning template to read from, optionally including full path.")] + public string Path; + + [Parameter(Mandatory = false, Position = 3, HelpMessage = "The target Container for the file to add to the in-memory template, optional argument.")] + public string Container; + + [Parameter(Mandatory = false, Position = 4, HelpMessage = "The level of the files to add. Defaults to Published")] + public PnPFileLevel FileLevel = PnPFileLevel.Published; + + [Parameter(Mandatory = false, Position = 5, HelpMessage = "Set to overwrite in site, Defaults to true")] + public SwitchParameter FileOverwrite = true; + + [Parameter(Mandatory = false, Position = 6, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "Include webparts when the file is a page")] + public SwitchParameter ExtractWebParts = true; + + [Parameter(Mandatory = false, Position = 7, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "Include webparts when the file is a page")] + public SwitchParameter ExtractFileProperties = true; + + [Parameter(Mandatory = false, Position = 8, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")] + public ITemplateProviderExtension[] TemplateProviderExtensions; + + protected readonly ProgressRecord _progressEnumeration = new ProgressRecord(0, "Activity", "Status") { Activity = "Enumerating folder" }; + protected readonly ProgressRecord _progressFilesEnumeration = new ProgressRecord(1, "Activity", "Status") { Activity = "Extracting files" }; + protected readonly ProgressRecord _progressFileProcessing = new ProgressRecord(2, "Activity", "Status") { Activity = "Extracting file" }; + + protected override void ProcessRecord() + { + base.ProcessRecord(); + var ctx = (ClientContext)SelectedWeb.Context; + ctx.Load(SelectedWeb, web => web.Id, web => web.ServerRelativeUrl, web => web.Url); + if (ExtractWebParts) + { + ctx.Load(ctx.Site, site => site.Id, site => site.ServerRelativeUrl, site => site.Url); + ctx.Load(SelectedWeb.Lists, lists => lists.Include(l => l.Title, l => l.RootFolder.ServerRelativeUrl, l => l.Id)); + } + ctx.ExecuteQueryRetry(); + } + + protected ProvisioningTemplate LoadTemplate() + { + if (!System.IO.Path.IsPathRooted(Path)) + { + Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path); + } + // Load the template + var template = ReadProvisioningTemplate + .LoadProvisioningTemplateFromFile(Path, + TemplateProviderExtensions); + + if (template == null) + { + throw new ApplicationException("Invalid template file!"); + } + + return template; + } + + /// + /// Add a file to the template + /// + /// The provisioning template to add the file to + /// Stream to read the file content + /// target folder in the provisioning template + /// Name of the file + /// Container path within the template (pnp file) or related to the xml templage + /// WebParts to include + /// Properties of the file + protected void AddFileToTemplate( + ProvisioningTemplate template, + Stream fs, + string folder, + string fileName, + string container, + IEnumerable webParts = null, + IDictionary properties = null + ) + { + if (template == null) throw new ArgumentNullException(nameof(template)); + if (fs == null) throw new ArgumentNullException(nameof(fs)); + if (fileName == null) throw new ArgumentNullException(nameof(fileName)); + + var source = !string.IsNullOrEmpty(container) ? (container + "/" + fileName) : fileName; + + template.Connector.SaveFileStream(fileName, container, fs); + + if (template.Connector is ICommitableFileConnector) + { + ((ICommitableFileConnector)template.Connector).Commit(); + } + + var existing = template.Files.FirstOrDefault(f => f.Src == $"{container}/{fileName}" && f.Folder == folder); + + if (existing != null) + template.Files.Remove(existing); + + var newFile = new OfficeDevPnP.Core.Framework.Provisioning.Model.File + { + Src = source, + Folder = folder, + Level = FileLevel, + Overwrite = FileOverwrite + }; + + if (webParts != null) newFile.WebParts.AddRange(webParts); + if (properties != null) + { + foreach (var property in properties) + { + newFile.Properties.Add(property.Key, property.Value); + } + } + template.Files.Add(newFile); + + // Determine the output file name and path + var outFileName = System.IO.Path.GetFileName(Path); + var outPath = new FileInfo(Path).DirectoryName; + + var fileSystemConnector = new FileSystemConnector(outPath, ""); + var formatter = XMLPnPSchemaFormatter.LatestFormatter; + var extension = new FileInfo(Path).Extension.ToLowerInvariant(); + if (extension == ".pnp") + { + var provider = new XMLOpenXMLTemplateProvider(template.Connector as OpenXMLConnector); + var templateFileName = outFileName.Substring(0, outFileName.LastIndexOf(".", StringComparison.Ordinal)) + ".xml"; + + provider.SaveAs(template, templateFileName, formatter, TemplateProviderExtensions); + } + else + { + XMLTemplateProvider provider = new XMLFileSystemTemplateProvider(Path, ""); + provider.SaveAs(template, Path, formatter, TemplateProviderExtensions); + } + } + + /// + /// Adds a remote file to a template + /// + /// Template to add the file to + /// The SharePoint file to retrieve and add + protected void AddSPFileToTemplate(ProvisioningTemplate template, SPFile file) + { + if (template == null) throw new ArgumentNullException(nameof(template)); + if (file == null) throw new ArgumentNullException(nameof(file)); + + file.EnsureProperties(f => f.Name, f => f.ServerRelativeUrl); + + _progressFileProcessing.StatusDescription = $"Extracting file {file.ServerRelativeUrl}"; + var folderRelativeUrl = file.ServerRelativeUrl.Substring(0, file.ServerRelativeUrl.Length - file.Name.Length - 1); + var folderWebRelativeUrl = HttpUtility.UrlKeyValueDecode(folderRelativeUrl.Substring(SelectedWeb.ServerRelativeUrl.TrimEnd('/').Length + 1)); + if (ClientContext.HasPendingRequest) ClientContext.ExecuteQuery(); + try + { + IEnumerable webParts = null; + if (ExtractWebParts) + { + webParts = ExtractSPFileWebParts(file).ToArray(); + _progressFileProcessing.PercentComplete = 25; + _progressFileProcessing.StatusDescription = $"Extracting webpart from {file.ServerRelativeUrl} "; + WriteProgress(_progressFileProcessing); + } + + using (var fi = SPFile.OpenBinaryDirect(ClientContext, file.ServerRelativeUrl)) + using (var ms = new MemoryStream()) + { + _progressFileProcessing.PercentComplete = 50; + _progressFileProcessing.StatusDescription = $"Reading file {file.ServerRelativeUrl}"; + WriteProgress(_progressFileProcessing); + // We are using a temporary memory stream because the file connector is seeking in the stream + // and the stream provided by OpenBinaryDirect does not allow it + fi.Stream.CopyTo(ms); + ms.Position = 0; + IDictionary properties = null; + if (ExtractFileProperties && string.Compare(System.IO.Path.GetExtension(file.Name), ".aspx", true) == 0) + { + _progressFileProcessing.PercentComplete = 35; + _progressFileProcessing.StatusDescription = $"Extracting properties from {file.ServerRelativeUrl}"; + properties = XmlPageDataHelper.ExtractProperties( + Encoding.UTF8.GetString(ms.ToArray()) + ).ToDictionary(p => p.Key, p => Tokenize(p.Value)); + } + AddFileToTemplate(template, ms, folderWebRelativeUrl, file.Name, folderWebRelativeUrl, webParts, properties); + _progressFileProcessing.PercentComplete = 100; + _progressFileProcessing.StatusDescription = $"Adding file {file.ServerRelativeUrl} to template"; + _progressFileProcessing.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFileProcessing); + } + } + catch (Exception exc) + { + WriteWarning($"Error trying to add file {file.ServerRelativeUrl} : {exc.Message}"); + } + } + + private IEnumerable ExtractSPFileWebParts(SPFile file) + { + if (file == null) throw new ArgumentNullException(nameof(file)); + + if (string.Compare(System.IO.Path.GetExtension(file.Name), ".aspx", true) == 0) + { + foreach (var spwp in SelectedWeb.GetWebParts(file.ServerRelativeUrl)) + { + spwp.EnsureProperties(wp => wp.WebPart +#if !SP2016 // Missing ZoneId property in SP2016 version of the CSOM Library + , wp => wp.ZoneId +#endif + ); + yield return new WebPart + { + Contents = Tokenize(SelectedWeb.GetWebPartXml(spwp.Id, file.ServerRelativeUrl)), + Order = (uint)spwp.WebPart.ZoneIndex, + Title = spwp.WebPart.Title, +#if !SP2016 // Missing ZoneId property in SP2016 version of the CSOM Library + Zone = spwp.ZoneId +#endif + }; + } + } + } + + /// + /// Adds a local file to a template + /// + /// Template to add the file to + /// Full path to a local file + /// Destination folder of the added file + protected void AddLocalFileToTemplate(ProvisioningTemplate template, string file, string folder) + { + if (template == null) throw new ArgumentNullException(nameof(template)); + if (file == null) throw new ArgumentNullException(nameof(file)); + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + _progressFileProcessing.Activity = $"Extracting file {file}"; + _progressFileProcessing.StatusDescription = "Adding file {file}"; + _progressFileProcessing.PercentComplete = 0; + WriteProgress(_progressFileProcessing); + + try + { + var fileName = System.IO.Path.GetFileName(file); + var container = !string.IsNullOrEmpty(Container) ? Container : folder.Replace("\\", "/"); + + using (var fs = System.IO.File.OpenRead(file)) + { + AddFileToTemplate(template, fs, folder.Replace("\\", "/"), fileName, container); + } + } + catch (Exception exc) + { + WriteWarning($"Error trying to add file {file} : {exc.Message}"); + } + _progressFileProcessing.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFileProcessing); + } + + private string Tokenize(string input) + { + if (string.IsNullOrEmpty(input)) return input; + + foreach (var list in SelectedWeb.Lists) + { + var webRelativeUrl = list.GetWebRelativeUrl(); + if (!webRelativeUrl.StartsWith("_catalogs", StringComparison.Ordinal)) + { + input = input + .ReplaceCaseInsensitive(list.Id.ToString("D"), "{listid:" + list.Title + "}") + .ReplaceCaseInsensitive(webRelativeUrl, "{listurl:" + list.Title + "}"); + } + } + return input.ReplaceCaseInsensitive(SelectedWeb.Url, "{site}") + .ReplaceCaseInsensitive(SelectedWeb.ServerRelativeUrl, "{site}") + .ReplaceCaseInsensitive(SelectedWeb.Id.ToString(), "{siteid}") + .ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.ServerRelativeUrl, "{sitecollection}") + .ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.Id.ToString(), "{sitecollectionid}") + .ReplaceCaseInsensitive(((ClientContext)SelectedWeb.Context).Site.Url, "{sitecollection}"); + } + } +} \ No newline at end of file diff --git a/Commands/Extensions/SPFileExtensions.cs b/Commands/Extensions/SPFileExtensions.cs new file mode 100644 index 000000000..54b379ba9 --- /dev/null +++ b/Commands/Extensions/SPFileExtensions.cs @@ -0,0 +1,151 @@ +using Microsoft.SharePoint.Client; +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using System.Web; + +namespace SharePointPnP.PowerShell.Commands.Extensions +{ + public static class SPFileExtensions + { + public static IList GetPropertyBag(this File file) + { + var ctx = (ClientContext)file.Context; + var web = ctx.Web; + var webRelativeFileUrl = file.ServerRelativeUrl.Replace(web.ServerRelativeUrl.TrimEnd('/') + '/', ""); + + using (var wc = new WebClientEx()) + { + if (file.Context.Credentials != null) + { + wc.Credentials = file.Context.Credentials; + } + else + { + wc.UseDefaultCredentials = true; + } + + var requestUrl = web.Url.TrimEnd('/') + "/_vti_bin/_vti_aut/author.dll"; + + wc.Headers.Add(HttpRequestHeader.ContentType, "application/x-www-form-urlencoded"); + wc.Headers.Add("X-Vermeer-Content-Type", "application/x-www-form-urlencoded"); + + var query = HttpUtility.ParseQueryString(string.Empty); + query.Add("method", "getDocsMetaInfo"); + query.Add("url_list", $"[{webRelativeFileUrl}]"); + + var rpcResult = Encoding.UTF8.GetString( + wc.UploadData(requestUrl, "POST", Encoding.UTF8.GetBytes(query.ToString())) + ); + + return ParseRpcResult(rpcResult); + } + } + + public struct RpcProperty + { + private readonly string _key; + private readonly object _value; + private readonly bool _writable; + + public RpcProperty(string key, object value, bool writable) + { + this._key = key; + this._value = value; + this._writable = writable; + } + + public string Key => _key; + + public object Value => _value; + + public bool Writable => _writable; + + public override string ToString() + { + return new { Key, Value, Writable }.ToString(); + } + } + + private static IList ParseRpcResult(string rpcResult) + { + var result = new List(); + + using (var sr = new System.IO.StringReader(rpcResult)) + { + string currentLine; + var hasReachedMetadata = false; + while ((currentLine = sr.ReadLine()) != null) + { + if (!hasReachedMetadata) + { + if (currentLine == "
  • meta_info=") + { + // SKip the next ul line + sr.ReadLine(); + hasReachedMetadata = true; + } + // else nothing to do + } + else + { + if (currentLine == "") + { + break; // end of data has been reached + } + else + { + var key = currentLine.Substring(4); + var rawValue = sr.ReadLine().Substring(4); + var typeInfo = rawValue.Substring(0, 1); + var writable = rawValue.Substring(1, 1) == "W"; + var strValue = HttpUtility.HtmlDecode(rawValue.Substring(3)); + object value = null; + switch (typeInfo) + { + case "B": + value = Convert.ToBoolean(strValue); + break; + + case "I": + value = Convert.ToInt32(strValue); + break; + + case "F": + case "T": + value = DateTime.Parse(strValue); + break; + + case "S": + case "V": + value = strValue; + break; + + default: + throw new InvalidOperationException("Unknown RPC type"); + } + var prop = new RpcProperty(key, value, writable); + result.Add(prop); + } + } + } + return result; + } + } + + // See https://stackoverflow.com/a/43172235/588868 + protected class WebClientEx : WebClient + { + protected override System.Net.WebRequest GetWebRequest(Uri address) + { + var request = base.GetWebRequest(address) as HttpWebRequest; + if (request != null) + { + request.ServicePoint.Expect100Continue = false; + } + return request; + } + } + } +} \ No newline at end of file diff --git a/Commands/Provisioning/AddFileToProvisioningTemplate.cs b/Commands/Provisioning/AddFileToProvisioningTemplate.cs new file mode 100644 index 000000000..3438a0aef --- /dev/null +++ b/Commands/Provisioning/AddFileToProvisioningTemplate.cs @@ -0,0 +1,91 @@ +using Microsoft.SharePoint.Client; +using SharePointPnP.PowerShell.CmdletHelpAttributes; +using System; +using System.Management.Automation; + +namespace SharePointPnP.PowerShell.Commands.Provisioning +{ + [Cmdlet(VerbsCommon.Add, "PnPFileToProvisioningTemplate")] + [CmdletHelp("Adds a file to a PnP Provisioning Template", + Category = CmdletHelpCategory.Provisioning)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder", + Remarks = "Adds a file to a PnP Provisioning Template", + SortOrder = 1)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder", + Remarks = "Adds a file reference to a PnP Provisioning XML Template", + SortOrder = 2)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false", + Remarks = "Adds a file to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.", + SortOrder = 3)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container", + Remarks = "Adds a file to a PnP Provisioning Template with a custom container for the file", + SortOrder = 4)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl $urlOfFile", + Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.", + SortOrder = 5)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl $urlOfFile -ExtractWebParts:$false", + Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.", + SortOrder = 6)] + public class AddFileToProvisioningTemplate : BaseFileProvisioningCmdlet + { + /* +* Path, FileLevel, FileOverwrite and TemplateProviderExtensions fields are in the base class +* */ + + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The file to add to the in-memory template, optionally including full path.")] + public string Source; + + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The file to add to the in-memory template, specifying its url in the current connected Web.")] + public string SourceUrl; + + [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the file to add to the in-memory template.")] + public string Folder; + + protected override void ProcessRecord() + { + base.ProcessRecord(); + var template = LoadTemplate(); + if (this.ParameterSetName == PSNAME_REMOTE_SOURCE) + { + var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute); + + // Get the server relative url of the file, whatever the input url is (absolute, server relative or web relative form) + var serverRelativeUrl = + sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file) + SourceUrl.StartsWith("/", StringComparison.Ordinal) ? SourceUrl : // The url is server relative. Take it as is (/sites/web/folder/file) + SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceUrl; // The url is web relative, prepend by the web url (folder/file) + + _progressFileProcessing.PercentComplete = 0; + _progressFileProcessing.RecordType = ProgressRecordType.Processing; + _progressFileProcessing.StatusDescription = $"Getting file info {serverRelativeUrl}"; + + var file = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl); + + AddSPFileToTemplate(template, file); + } + else + { + if (!System.IO.Path.IsPathRooted(Source)) + { + Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source); + } + + // Load the file and add it to the .PNP file + Folder = Folder.Replace("\\", "/"); + + _progressFileProcessing.PercentComplete = 0; + _progressFileProcessing.RecordType = ProgressRecordType.Processing; + _progressFileProcessing.StatusDescription = $"Getting file info {Source}"; + WriteProgress(_progressFileProcessing); + + AddLocalFileToTemplate(template, Source, Folder); + } + } + } +} \ No newline at end of file diff --git a/Commands/Provisioning/AddFilesToProvisioningTemplate.cs b/Commands/Provisioning/AddFilesToProvisioningTemplate.cs new file mode 100644 index 000000000..533a6dd1e --- /dev/null +++ b/Commands/Provisioning/AddFilesToProvisioningTemplate.cs @@ -0,0 +1,148 @@ +using Microsoft.SharePoint.Client; +using SharePointPnP.PowerShell.CmdletHelpAttributes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using SPFile = Microsoft.SharePoint.Client.File; + +namespace SharePointPnP.PowerShell.Commands.Provisioning +{ + [Cmdlet(VerbsCommon.Add, "PnPFilesToProvisioningTemplate")] + [CmdletHelp("Adds files to a PnP Provisioning Template", + Category = CmdletHelpCategory.Provisioning)] + [CmdletExample( + Code = @"PS:> Add-PnPFilesToProvisioningTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder", + Remarks = "Adds files to a PnP Provisioning Template from a local folder", + SortOrder = 1)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -SourceFolder $sourceFolder -Folder $targetFolder", + Remarks = "Adds files reference to a PnP Provisioning XML Template", + SortOrder = 2)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false", + Remarks = "Adds files to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the files if it exists in the site.", + SortOrder = 3)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Recurse", + Remarks = "Adds files to a PnP Provisioning Template from a local folder recursively.", + SortOrder = 4)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder -Container $container", + Remarks = "Adds files to a PnP Provisioning Template with a custom container for the files", + SortOrder = 5)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder", + Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.", + SortOrder = 6)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder -ExtractWebParts:$false", + Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.", + SortOrder = 7)] + public class AddFilesToProvisioningTemplate : BaseFileProvisioningCmdlet + { + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The source folder to add to the in-memory template, optionally including full path.")] + public string SourceFolder; + + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The source folder to add to the in-memory template, specifying its url in the current connected Web.")] + public string SourceFolderUrl; + + [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")] + public string Folder; + + [Parameter(Mandatory = false, Position = 8, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")] + public SwitchParameter Recurse = false; + + protected override void ProcessRecord() + { + base.ProcessRecord(); + var template = LoadTemplate(); + if (this.ParameterSetName == PSNAME_REMOTE_SOURCE) + { + var sourceUri = new Uri(SourceFolderUrl, UriKind.RelativeOrAbsolute); + // Get the server relative url of the folder, whatever the input url is (absolute, server relative or web relative form) + var serverRelativeUrl = + sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file) + SourceFolderUrl.StartsWith("/", StringComparison.Ordinal) ? SourceFolderUrl : // The url is server relative. Take it as is (/sites/web/folder/file) + SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceFolderUrl; // The url is web relative, prepend by the web url (folder/file) + + var folder = SelectedWeb.GetFolderByServerRelativeUrl(serverRelativeUrl); + + var files = EnumRemoteFiles(folder, Recurse).OrderBy(f => f.ServerRelativeUrl).ToArray(); + _progressEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressEnumeration); + + for (int i = 0; i < files.Length; i++) + { + var file = files[0]; + + _progressFilesEnumeration.PercentComplete = 100 * i / files.Length; + _progressFilesEnumeration.StatusDescription = $"Processing file {file.ServerRelativeUrl}"; + WriteProgress(_progressFilesEnumeration); + AddSPFileToTemplate(template, file); + } + _progressFilesEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFilesEnumeration); + } + else + { + if (!System.IO.Path.IsPathRooted(SourceFolder)) + { + SourceFolder = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, SourceFolder); + } + + var files = Directory.GetFiles(SourceFolder, "*", Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderBy(f => f).ToArray(); + + for (int i = 0; i < files.Length; i++) + { + var file = files[i]; + + _progressFilesEnumeration.PercentComplete = 100 * i / files.Length; + + WriteProgress(_progressFilesEnumeration); + + var localFileFolder = System.IO.Path.GetDirectoryName(file); + // relative folder of the leaf file within the directory structure, under the source folder + var relativeFolder = Folder + localFileFolder.Substring(SourceFolder.Length); + // Load the file and add it to the .PNP file + AddLocalFileToTemplate(template, file, relativeFolder); + } + _progressFilesEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFilesEnumeration); + } + } + + private IEnumerable EnumRemoteFiles(Folder folder, bool recurse) + { + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + var ctx = folder.Context; + ctx.Load(folder, fld => fld.ServerRelativeUrl); + ctx.Load(folder.Files, files => files.Include(f => f.ServerRelativeUrl, f => f.Name)); + ctx.ExecuteQueryRetry(); + + _progressEnumeration.RecordType = ProgressRecordType.Processing; + _progressEnumeration.StatusDescription = $"Enumerating files in folder {folder.ServerRelativeUrl}"; + WriteProgress(_progressEnumeration); + foreach (var file in folder.Files) + { + yield return file; + } + + if (recurse) + { + ctx.Load(folder.Folders); + ctx.ExecuteQueryRetry(); + + foreach (var subFolder in folder.Folders) + { + foreach (var file in EnumRemoteFiles(subFolder, recurse)) + { + yield return file; + } + } + } + } + } +} \ No newline at end of file diff --git a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs index ad972c19c..ccddf765e 100644 --- a/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs +++ b/Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs @@ -1,125 +1,91 @@ -using OfficeDevPnP.Core.Framework.Provisioning.Connectors; -using OfficeDevPnP.Core.Framework.Provisioning.Model; -using OfficeDevPnP.Core.Framework.Provisioning.Providers; -using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml; +using Microsoft.SharePoint.Client; using SharePointPnP.PowerShell.CmdletHelpAttributes; using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Management.Automation; -using System.Text; -using System.Threading.Tasks; -namespace SharePointPnP.PowerShell.Commands.Provisioning.Site +namespace SharePointPnP.PowerShell.Commands.Provisioning { - [Cmdlet(VerbsCommon.Add, "PnPFileToProvisioningTemplate")] + [Cmdlet(VerbsCommon.Add, "PnPFileToSiteTemplate")] [CmdletHelp("Adds a file to a PnP Provisioning Template", Category = CmdletHelpCategory.Provisioning)] [CmdletExample( - Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder", - Remarks = "Adds a file to a PnP Site Template", + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder", + Remarks = "Adds a file to a PnP Provisioning Template", SortOrder = 1)] [CmdletExample( - Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder", - Remarks = "Adds a file reference to a PnP Site XML Template", + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.xml -Source $sourceFilePath -Folder $targetFolder", + Remarks = "Adds a file reference to a PnP Provisioning XML Template", SortOrder = 2)] [CmdletExample( - Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false", - Remarks = "Adds a file to a PnP Site Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.", + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source ""./myfile.png"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false", + Remarks = "Adds a file to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the file if it exists in the site.", SortOrder = 3)] [CmdletExample( - Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container", - Remarks = "Adds a file to a PnP Site Template with a custom container for the file", + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -Source $sourceFilePath -Folder $targetFolder -Container $container", + Remarks = "Adds a file to a PnP Provisioning Template with a custom container for the file", SortOrder = 4)] - - public class AddFileToProvisioningTemplate : PSCmdlet + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceUrl $urlOfFile", + Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.", + SortOrder = 5)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceUrl $urlOfFile -ExtractWebParts:$false", + Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.", + SortOrder = 6)] + public class AddFileToSiteTemplate : BaseFileProvisioningCmdlet { - [Parameter(Mandatory = true, Position = 0, HelpMessage = "Filename of the .PNP Open XML site template to read from, optionally including full path.")] - public string Path; + /* +* Path, FileLevel, FileOverwrite and TemplateProviderExtensions fields are in the base class +* */ - [Parameter(Mandatory = true, Position = 1, HelpMessage = "The file to add to the in-memory template, optionally including full path.")] + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The file to add to the in-memory template, optionally including full path.")] public string Source; - [Parameter(Mandatory = true, Position = 2, HelpMessage = "The target Folder for the file to add to the in-memory template.")] - public string Folder; - - [Parameter(Mandatory = false, Position = 3, HelpMessage = "The target Container for the file to add to the in-memory template, optional argument.")] - public string Container; - - [Parameter(Mandatory = false, Position = 4, HelpMessage = "The level of the files to add. Defaults to Published")] - public FileLevel FileLevel = FileLevel.Published; - - [Parameter(Mandatory = false, Position = 5, HelpMessage = "Set to overwrite in site, Defaults to true")] - public SwitchParameter FileOverwrite = true; + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The file to add to the in-memory template, specifying its url in the current connected Web.")] + public string SourceUrl; - [Parameter(Mandatory = false, Position = 4, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")] - public ITemplateProviderExtension[] TemplateProviderExtensions; + [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the file to add to the in-memory template.")] + public string Folder; protected override void ProcessRecord() { - if (!System.IO.Path.IsPathRooted(Path)) - { - Path = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Path); - } - if(!System.IO.Path.IsPathRooted(Source)) + base.ProcessRecord(); + var template = LoadTemplate(); + if (this.ParameterSetName == PSNAME_REMOTE_SOURCE) { - Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source); - } - // Load the template - var template = ReadProvisioningTemplate - .LoadProvisioningTemplateFromFile(Path, - TemplateProviderExtensions); + var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute); - if (template == null) - { - throw new ApplicationException("Invalid template file!"); - } + // Get the server relative url of the file, whatever the input url is (absolute, server relative or web relative form) + var serverRelativeUrl = + sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file) + SourceUrl.StartsWith("/", StringComparison.Ordinal) ? SourceUrl : // The url is server relative. Take it as is (/sites/web/folder/file) + SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceUrl; // The url is web relative, prepend by the web url (folder/file) - // Load the file and add it to the .PNP file - using (var fs = new FileStream(Source, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - Folder = Folder.Replace("\\", "/"); - - var fileName = Source.IndexOf("\\") > 0 ? Source.Substring(Source.LastIndexOf("\\") + 1) : Source; - var container = !string.IsNullOrEmpty(Container) ? Container : string.Empty; - var source = !string.IsNullOrEmpty(container) ? (container + "/" + fileName) : fileName; + _progressFileProcessing.PercentComplete = 0; + _progressFileProcessing.RecordType = ProgressRecordType.Processing; + _progressFileProcessing.StatusDescription = $"Getting file info {serverRelativeUrl}"; - template.Connector.SaveFileStream(fileName, container, fs); + var file = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl); - if (template.Connector is ICommitableFileConnector) + AddSPFileToTemplate(template, file); + } + else + { + if (!System.IO.Path.IsPathRooted(Source)) { - ((ICommitableFileConnector)template.Connector).Commit(); + Source = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, Source); } - template.Files.Add(new OfficeDevPnP.Core.Framework.Provisioning.Model.File - { - Src = source, - Folder = Folder, - Level = FileLevel, - Overwrite = FileOverwrite, - }); - - // Determine the output file name and path - var outFileName = System.IO.Path.GetFileName(Path); - var outPath = new FileInfo(Path).DirectoryName; + // Load the file and add it to the .PNP file + Folder = Folder.Replace("\\", "/"); - var fileSystemConnector = new FileSystemConnector(outPath, ""); - var formatter = XMLPnPSchemaFormatter.LatestFormatter; - var extension = new FileInfo(Path).Extension.ToLowerInvariant(); - if (extension == ".pnp") - { - var provider = new XMLOpenXMLTemplateProvider(template.Connector as OpenXMLConnector); - var templateFileName = outFileName.Substring(0, outFileName.LastIndexOf(".", StringComparison.Ordinal)) + ".xml"; + _progressFileProcessing.PercentComplete = 0; + _progressFileProcessing.RecordType = ProgressRecordType.Processing; + _progressFileProcessing.StatusDescription = $"Getting file info {Source}"; + WriteProgress(_progressFileProcessing); - provider.SaveAs(template, templateFileName, formatter, TemplateProviderExtensions); - } - else - { - XMLTemplateProvider provider = new XMLFileSystemTemplateProvider(Path, ""); - provider.SaveAs(template, Path, formatter, TemplateProviderExtensions); - } + AddLocalFileToTemplate(template, Source, Folder); } } } -} +} \ No newline at end of file diff --git a/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs b/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs new file mode 100644 index 000000000..700194ab3 --- /dev/null +++ b/Commands/Provisioning/Site/AddFilesToSiteTemplate.cs @@ -0,0 +1,148 @@ +using Microsoft.SharePoint.Client; +using SharePointPnP.PowerShell.CmdletHelpAttributes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using SPFile = Microsoft.SharePoint.Client.File; + +namespace SharePointPnP.PowerShell.Commands.Provisioning.Site +{ + [Cmdlet(VerbsCommon.Add, "PnPFilesToSiteTemplate")] + [CmdletHelp("Adds files to a PnP Provisioning Template", + Category = CmdletHelpCategory.Provisioning)] + [CmdletExample( + Code = @"PS:> Add-PnPFilesToSiteTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder", + Remarks = "Adds files to a PnP Provisioning Template from a local folder", + SortOrder = 1)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.xml -SourceFolder $sourceFolder -Folder $targetFolder", + Remarks = "Adds files reference to a PnP Provisioning XML Template", + SortOrder = 2)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Folder ""folderinsite"" -FileLevel Published -FileOverwrite:$false", + Remarks = "Adds files to a PnP Provisioning Template, specifies the level as Published and defines to not overwrite the files if it exists in the site.", + SortOrder = 3)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder ""./myfolder"" -Recurse", + Remarks = "Adds files to a PnP Provisioning Template from a local folder recursively.", + SortOrder = 4)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolder $sourceFolder -Folder $targetFolder -Container $container", + Remarks = "Adds files to a PnP Provisioning Template with a custom container for the files", + SortOrder = 5)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder", + Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web. The url can be either full, server relative or Web relative url.", + SortOrder = 6)] + [CmdletExample( + Code = @"PS:> Add-PnPFileToSiteTemplate -Path template.pnp -SourceFolderUrl $urlOfFolder -ExtractWebParts:$false", + Remarks = "Adds files to a PnP Provisioning Template retrieved from the currently connected web, disabling WebPart extraction.", + SortOrder = 7)] + public class AddFilesToSiteTemplate : BaseFileProvisioningCmdlet + { + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The source folder to add to the in-memory template, optionally including full path.")] + public string SourceFolder; + + [Parameter(Mandatory = true, Position = 1, ParameterSetName = PSNAME_REMOTE_SOURCE, HelpMessage = "The source folder to add to the in-memory template, specifying its url in the current connected Web.")] + public string SourceFolderUrl; + + [Parameter(Mandatory = true, Position = 2, ParameterSetName = PSNAME_LOCAL_SOURCE, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")] + public string Folder; + + [Parameter(Mandatory = false, Position = 8, HelpMessage = "The target Folder for the source folder to add to the in-memory template.")] + public SwitchParameter Recurse = false; + + protected override void ProcessRecord() + { + base.ProcessRecord(); + var template = LoadTemplate(); + if (this.ParameterSetName == PSNAME_REMOTE_SOURCE) + { + var sourceUri = new Uri(SourceFolderUrl, UriKind.RelativeOrAbsolute); + // Get the server relative url of the folder, whatever the input url is (absolute, server relative or web relative form) + var serverRelativeUrl = + sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath : // The url is absolute, extract the absolute path (http://server/sites/web/folder/file) + SourceFolderUrl.StartsWith("/", StringComparison.Ordinal) ? SourceFolderUrl : // The url is server relative. Take it as is (/sites/web/folder/file) + SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceFolderUrl; // The url is web relative, prepend by the web url (folder/file) + + var folder = SelectedWeb.GetFolderByServerRelativeUrl(serverRelativeUrl); + + var files = EnumRemoteFiles(folder, Recurse).OrderBy(f => f.ServerRelativeUrl).ToArray(); + _progressEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressEnumeration); + + for (int i = 0; i < files.Length; i++) + { + var file = files[i]; + + _progressFilesEnumeration.PercentComplete = 100 * i / files.Length; + _progressFilesEnumeration.StatusDescription = $"Processing file {file.ServerRelativeUrl}"; + WriteProgress(_progressFilesEnumeration); + AddSPFileToTemplate(template, file); + } + _progressFilesEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFilesEnumeration); + } + else + { + if (!System.IO.Path.IsPathRooted(SourceFolder)) + { + SourceFolder = System.IO.Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, SourceFolder); + } + + var files = Directory.GetFiles(SourceFolder, "*", Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderBy(f => f).ToArray(); + + for (int i = 0; i < files.Length; i++) + { + var file = files[i]; + + _progressFilesEnumeration.PercentComplete = 100 * i / files.Length; + + WriteProgress(_progressFilesEnumeration); + + var localFileFolder = System.IO.Path.GetDirectoryName(file); + // relative folder of the leaf file within the directory structure, under the source folder + var relativeFolder = Folder + localFileFolder.Substring(SourceFolder.Length); + // Load the file and add it to the .PNP file + AddLocalFileToTemplate(template, file, relativeFolder); + } + _progressFilesEnumeration.RecordType = ProgressRecordType.Completed; + WriteProgress(_progressFilesEnumeration); + } + } + + private IEnumerable EnumRemoteFiles(Folder folder, bool recurse) + { + if (folder == null) throw new ArgumentNullException(nameof(folder)); + + var ctx = folder.Context; + ctx.Load(folder, fld => fld.ServerRelativeUrl); + ctx.Load(folder.Files, files => files.Include(f => f.ServerRelativeUrl, f => f.Name)); + ctx.ExecuteQueryRetry(); + + _progressEnumeration.RecordType = ProgressRecordType.Processing; + _progressEnumeration.StatusDescription = $"Enumerating files in folder {folder.ServerRelativeUrl}"; + WriteProgress(_progressEnumeration); + foreach (var file in folder.Files) + { + yield return file; + } + + if (recurse) + { + ctx.Load(folder.Folders); + ctx.ExecuteQueryRetry(); + + foreach (var subFolder in folder.Folders) + { + foreach (var file in EnumRemoteFiles(subFolder, recurse)) + { + yield return file; + } + } + } + } + } +} \ No newline at end of file diff --git a/Commands/SharePointPnP.PowerShell.Commands.csproj b/Commands/SharePointPnP.PowerShell.Commands.csproj index e953d2835..4b2a10a41 100644 --- a/Commands/SharePointPnP.PowerShell.Commands.csproj +++ b/Commands/SharePointPnP.PowerShell.Commands.csproj @@ -570,6 +570,7 @@ + @@ -635,6 +636,7 @@ + @@ -796,6 +798,7 @@ + @@ -1084,7 +1087,9 @@ - + + + diff --git a/Commands/Utilities/XmlPageDataHelper.cs b/Commands/Utilities/XmlPageDataHelper.cs new file mode 100644 index 000000000..521a3b9bd --- /dev/null +++ b/Commands/Utilities/XmlPageDataHelper.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace SharePointPnP.PowerShell.Commands.Utilities +{ + public static class XmlPageDataHelper + { + /// + /// Extract properties from Xml data hidden in aspx file + /// + /// Full page content including xml data + /// Properties extracted from the page. + public static IDictionary ExtractProperties(string documentContent) + { + // Seek for the Xml data within the page + var match = Regex.Match(documentContent, "(.*)<\\/xml>.*<\\/SharePoint:CTFieldRefs>", RegexOptions.Singleline); + if (match.Success) + { + // Wrap the actual data to be xml compliant + var xmlDataStr = $"{match.Groups[1].Value}"; + + var xmlData = XElement.Parse(xmlDataStr); + + return xmlData + .Elements("{mso}CustomDocumentProperties").Elements() + .Where(el => !string.IsNullOrEmpty(el.Value)) + .ToDictionary(el => el.Name.LocalName, el => el.Value); + } + else + { + throw new ApplicationException("Invalid documentContent. Is is an .aspx file?"); + } + } + } +} \ No newline at end of file diff --git a/Tests/SharePointPnP.PowerShell.Tests.csproj b/Tests/SharePointPnP.PowerShell.Tests.csproj index 72cda4e62..9baa7658d 100644 --- a/Tests/SharePointPnP.PowerShell.Tests.csproj +++ b/Tests/SharePointPnP.PowerShell.Tests.csproj @@ -354,6 +354,7 @@ + diff --git a/Tests/Utilities/XmlPageDataHelperTests.cs b/Tests/Utilities/XmlPageDataHelperTests.cs new file mode 100644 index 000000000..83cc7963d --- /dev/null +++ b/Tests/Utilities/XmlPageDataHelperTests.cs @@ -0,0 +1,60 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SharePointPnP.PowerShell.Commands.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharePointPnP.PowerShell.Commands.Utilities.Tests +{ + [TestClass()] + public class XmlPageDataHelperTests + { + [TestMethod()] + public void ExtractPropertiesTest() + { + var sampleContent = @"<%@ Page Inherits=""Microsoft.SharePoint.Publishing.TemplateRedirectionPage,Microsoft.SharePoint.Publishing,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c"" %> <%@ Reference VirtualPath=""~TemplatePageUrl"" %> <%@ Reference VirtualPath=""~masterurl/custom.master"" %> +<%@ Register Tagprefix=""SharePoint"" Namespace=""Microsoft.SharePoint.WebControls"" Assembly=""Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"" %> + + +Recherche globale"; + + var result = XmlPageDataHelper.ExtractProperties(sampleContent); + + Assert.IsNotNull(result); + Assert.IsTrue(result.ContainsKey("PublishingPageLayout")); + Assert.AreEqual(result["PublishingPageLayout"], "/sites/somesite/_catalogs/masterpage/BlankWebPartPage.aspx, Page de composant WebPart vierge"); + + } + } +} \ No newline at end of file