Skip to content
This repository has been archived by the owner on Jan 19, 2021. It is now read-only.

Add parameter ExtractWebParts to command Add-PnPFileToProvisioningTemplate #2288

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 119 additions & 28 deletions Commands/Provisioning/Site/AddFileToProvisioningTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
using OfficeDevPnP.Core.Framework.Provisioning.Providers;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;
using SharePointPnP.PowerShell.CmdletHelpAttributes;
using SharePointPnP.PowerShell.Commands.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Net;
using PnPFileLevel = OfficeDevPnP.Core.Framework.Provisioning.Model.FileLevel;
using SPFile = Microsoft.SharePoint.Client.File;

namespace SharePointPnP.PowerShell.Commands.Provisioning.Site
{
Expand All @@ -37,10 +40,14 @@ namespace SharePointPnP.PowerShell.Commands.Provisioning.Site
Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl ""Shared%20Documents/ProjectStatus.docs""",
Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected site. The url can be server relative or web relative. If specifying a server relative url has to start with the current site url.",
SortOrder = 5)]
[CmdletExample(
Code = @"PS:> Add-PnPFileToProvisioningTemplate -Path template.pnp -SourceUrl ""SitePages/Home.aspx"" -ExtractWebParts",
Remarks = "Adds a file to a PnP Provisioning Template retrieved from the currently connected site. If the file is a classic page, also extract its webparts. The url can be server relative or web relative. If specifying a server relative url has to start with the current site url.",
SortOrder = 6)]
public class AddFileToProvisioningTemplate : PnPWebCmdlet
{
const string parameterSet_LOCALFILE = "Local File";
const string parameterSet_REMOTEFILE = "Remove File";
const string parameterSet_REMOTEFILE = "Remote File";

[Parameter(Mandatory = true, Position = 0, HelpMessage = "Filename of the .PNP Open XML site template to read from, optionally including full path.")]
public string Path;
Expand All @@ -63,6 +70,9 @@ public class AddFileToProvisioningTemplate : PnPWebCmdlet
[Parameter(Mandatory = false, Position = 5, HelpMessage = "Set to overwrite in site, Defaults to true")]
public SwitchParameter FileOverwrite = true;

[Parameter(Mandatory = false, Position = 6, ParameterSetName = parameterSet_REMOTEFILE, HelpMessage = "Include webparts if the file is a page")]
public SwitchParameter ExtractWebParts = true;

[Parameter(Mandatory = false, Position = 4, HelpMessage = "Allows you to specify ITemplateProviderExtension to execute while loading the template.")]
public ITemplateProviderExtension[] TemplateProviderExtensions;

Expand All @@ -84,40 +94,28 @@ protected override void ProcessRecord()
{
throw new ApplicationException("Invalid template file!");
}
// Add a file from the connected Web
if (this.ParameterSetName == parameterSet_REMOTEFILE)
{
SelectedWeb.EnsureProperty(w => w.ServerRelativeUrl);
if (ExtractWebParts)
{
ClientContext.Load(SelectedWeb, web => web.Url, web => web.Id, web => web.ServerRelativeUrl);
ClientContext.Load(((ClientContext)SelectedWeb.Context).Site, site => site.Id, site => site.ServerRelativeUrl, site => site.Url);
ClientContext.Load(SelectedWeb.Lists, lists => lists.Include(l => l.Title, l => l.RootFolder.ServerRelativeUrl, l => l.Id));
}

ClientContext.ExecuteQuery();

var sourceUri = new Uri(SourceUrl, UriKind.RelativeOrAbsolute);
var serverRelativeUrl =
sourceUri.IsAbsoluteUri ? sourceUri.AbsolutePath :
SourceUrl.StartsWith("/", StringComparison.Ordinal) ? SourceUrl :
SelectedWeb.ServerRelativeUrl.TrimEnd('/') + "/" + SourceUrl;

var file = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl);

var fileName = file.EnsureProperty(f => f.Name);
var folderRelativeUrl = serverRelativeUrl.Substring(0, serverRelativeUrl.Length - fileName.Length - 1);
var folderWebRelativeUrl = HttpUtility.UrlKeyValueDecode(folderRelativeUrl.Substring(SelectedWeb.ServerRelativeUrl.TrimEnd('/').Length + 1));
if (ClientContext.HasPendingRequest) ClientContext.ExecuteQuery();
try
{
#if SP2013 || SP2016
var fi = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl);
#else
var fi = SelectedWeb.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(serverRelativeUrl));
#endif
var fileStream = fi.OpenBinaryStream();
ClientContext.ExecuteQueryRetry();
using (var ms = fileStream.Value)
{
AddFileToTemplate(template, ms, folderWebRelativeUrl, fileName, folderWebRelativeUrl);
}
}
catch (WebException exc)
{
WriteWarning($"Can't add file from url {serverRelativeUrl} : {exc}");
}
AddSPFileToTemplate(template, file);
}
// Add a file from the file system
else
{
if (!System.IO.Path.IsPathRooted(Source))
Expand All @@ -128,17 +126,108 @@ protected override void ProcessRecord()
// Load the file and add it to the .PNP file
using (var fs = System.IO.File.OpenRead(Source))
{
Folder = Folder.Replace("\\", "/");
Folder = Folder.Replace('\\', '/');

var fileName = Source.IndexOf("\\", StringComparison.Ordinal) > 0 ? Source.Substring(Source.LastIndexOf("\\") + 1) : Source;
var fileName = Source.IndexOf(System.IO.Path.DirectorySeparatorChar) > 0
? Source.Substring(Source.LastIndexOf(System.IO.Path.DirectorySeparatorChar) + 1)
: Source;
var container = !string.IsNullOrEmpty(Container) ? Container : string.Empty;
AddFileToTemplate(template, fs, Folder, fileName, container);
}
}
}

private void AddFileToTemplate(ProvisioningTemplate template, Stream fs, string folder, string fileName, string container)
private 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);
var serverRelativeUrl = file.ServerRelativeUrl;
var fileName = file.Name;
var folderRelativeUrl = serverRelativeUrl.Substring(0, serverRelativeUrl.Length - fileName.Length - 1);
var folderWebRelativeUrl = HttpUtility.UrlKeyValueDecode(folderRelativeUrl.Substring(SelectedWeb.ServerRelativeUrl.TrimEnd('/').Length + 1));

try
{
#if SP2013 || SP2016
var fi = SelectedWeb.GetFileByServerRelativeUrl(serverRelativeUrl);
#else
var fi = SelectedWeb.GetFileByServerRelativePath(ResourcePath.FromDecodedUrl(serverRelativeUrl));
#endif

IEnumerable<WebPart> webParts = null;
if (ExtractWebParts)
{
webParts = ExtractSPFileWebParts(file).ToArray();
}

var fileStream = fi.OpenBinaryStream();
ClientContext.ExecuteQueryRetry();
using (var ms = fileStream.Value)
{
AddFileToTemplate(template, ms, folderWebRelativeUrl, fileName, folderWebRelativeUrl, webParts);
}
}
catch (WebException exc)
{
WriteWarning($"Can't add file from url {serverRelativeUrl} : {exc}");
}
}

private IEnumerable<WebPart> 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, wp => wp.ZoneId);
yield return new WebPart
{
Contents = Tokenize(SelectedWeb.GetWebPartXml(spwp.Id, file.ServerRelativeUrl)),
Order = (uint)spwp.WebPart.ZoneIndex,
Title = spwp.WebPart.Title,
Zone = spwp.ZoneId
};
}
}
}
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}");
}

private void AddFileToTemplate(
ProvisioningTemplate template,
Stream fs,
string folder,
string fileName,
string container,
IEnumerable<WebPart> webParts = null
)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (fs == null) throw new ArgumentNullException(nameof(fs));

var source = !string.IsNullOrEmpty(container) ? (container + "/" + fileName) : fileName;

template.Connector.SaveFileStream(fileName, container, fs);
Expand All @@ -163,6 +252,8 @@ private void AddFileToTemplate(ProvisioningTemplate template, Stream fs, string
Overwrite = FileOverwrite,
};

if (webParts != null) newFile.WebParts.AddRange(webParts);

template.Files.Add(newFile);

// Determine the output file name and path
Expand Down
115 changes: 107 additions & 8 deletions Commands/Utilities/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,116 @@
using System.Text.RegularExpressions;
using System;
using System.Diagnostics;
using System.Text;

namespace SharePointPnP.PowerShell.Commands.Utilities
{
/// <summary>
/// StringExtensions provides useful methods regarding string manipulation
/// </summary>
public static class StringExtensions
{
public static string ReplaceCaseInsensitive(this string input, string search, string replacement)
[DebuggerStepThrough]
public static string ReplaceCaseInsensitive(this string str, string oldValue, string newValue)
{
return Regex.Replace(
input,
Regex.Escape(search),
replacement.Replace("$", "$$"),
RegexOptions.IgnoreCase
);
return Replace(str, oldValue, newValue, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another
/// specified string according the type of search to use for the specified string.
/// </summary>
/// <param name="str">The string performing the replace method.</param>
/// <param name="oldValue">The string to be replaced.</param>
/// <param name="newValue">The string replace all occurrences of <paramref name="oldValue"/>.
/// If value is equal to <c>null</c>, than all occurrences of <paramref name="oldValue"/> will be removed from the <paramref name="str"/>.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the search.</param>
/// <returns>A string that is equivalent to the current string except that all instances of <paramref name="oldValue"/> are replaced with <paramref name="newValue"/>.
/// If <paramref name="oldValue"/> is not found in the current instance, the method returns the current instance unchanged.</returns>
// Credits to https://stackoverflow.com/a/45756981/588868
[DebuggerStepThrough]
public static string Replace(
this string str,
string oldValue,
string @newValue,
StringComparison comparisonType
)
{
// Check inputs.
if (str == null)
{
// Same as original .NET C# string.Replace behavior.
throw new ArgumentNullException(nameof(str));
}
if (str.Length == 0)
{
// Same as original .NET C# string.Replace behavior.
return str;
}
if (oldValue == null)
{
// Same as original .NET C# string.Replace behavior.
throw new ArgumentNullException(nameof(oldValue));
}
if (oldValue.Length == 0)
{
// Same as original .NET C# string.Replace behavior.
throw new ArgumentException("String cannot be of zero length.");
}

//if (oldValue.Equals(newValue, comparisonType))
//{
//This condition has no sense
//It will prevent method from replacesing: "Example", "ExAmPlE", "EXAMPLE" to "example"
//return str;
//}

// Prepare string builder for storing the processed string.
// Note: StringBuilder has a better performance than String by 30-40%.
var resultStringBuilder = new StringBuilder(str.Length);

// Analyze the replacement: replace or remove.
var isReplacementNullOrEmpty = string.IsNullOrEmpty(@newValue);

// Replace all values.
const int valueNotFound = -1;
int foundAt;
var startSearchFromIndex = 0;
while ((foundAt = str.IndexOf(oldValue, startSearchFromIndex, comparisonType)) != valueNotFound)
{
// Append all characters until the found replacement.
var @charsUntilReplacment = foundAt - startSearchFromIndex;
var isNothingToAppend = @charsUntilReplacment == 0;
if (!isNothingToAppend)
{
resultStringBuilder.Append(str, startSearchFromIndex, @charsUntilReplacment);
}

// Process the replacement.
if (!isReplacementNullOrEmpty)
{
resultStringBuilder.Append(@newValue);
}

// Prepare start index for the next search.
// This needed to prevent infinite loop, otherwise method always start search
// from the start of the string. For example: if an oldValue == "EXAMPLE", newValue == "example"
// and comparisonType == "any ignore case" will conquer to replacing:
// "EXAMPLE" to "example" to "example" to "example" … infinite loop.
startSearchFromIndex = foundAt + oldValue.Length;
if (startSearchFromIndex == str.Length)
{
// It is end of the input string: no more space for the next search.
// The input string ends with a value that has already been replaced.
// Therefore, the string builder with the result is complete and no further action is required.
return resultStringBuilder.ToString();
}
}

// Append the last part to the result.
var @charsUntilStringEnd = str.Length - startSearchFromIndex;
resultStringBuilder.Append(str, startSearchFromIndex, @charsUntilStringEnd);

return resultStringBuilder.ToString();
}
}
}