diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4451251..89a8529e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ### Added +- Added `Export-PnPEnterpriseWikiPackage`, `New-PnPEnterpriseWikiMigrationPlan`, and `Import-PnPEnterpriseWikiMigrationPackage` to preserve a complete source field snapshot, create a digest-approved target plan and field-by-field report, and import only recognized fields with fresh target and lifecycle verification. [#5457](https://github.com/pnp/powershell/pull/5457) + ### Changed ### Fixed diff --git a/documentation/Export-PnPEnterpriseWikiPackage.md b/documentation/Export-PnPEnterpriseWikiPackage.md new file mode 100644 index 000000000..114fe9d99 --- /dev/null +++ b/documentation/Export-PnPEnterpriseWikiPackage.md @@ -0,0 +1,204 @@ +--- +Module Name: PnP.PowerShell +title: Export-PnPEnterpriseWikiPackage +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/Export-PnPEnterpriseWikiPackage.html +--- + +# Export-PnPEnterpriseWikiPackage + +## SYNOPSIS + +**Required Permissions** + +* SharePoint: Sites.Read.All (application) or AllSites.Read (delegated) + +Exports a source-only Enterprise Wiki snapshot that can be planned later without reconnecting to the source. + +## SYNTAX + +### Identity + +```powershell +Export-PnPEnterpriseWikiPackage [-Identity] -OutputPath [-NoWebParts] + [-MaximumDependencyBytes ] [-Force] [-Connection ] +``` + +### All + +```powershell +Export-PnPEnterpriseWikiPackage -All -OutputPath [-NoWebParts] + [-MaximumDependencyBytes ] [-Force] [-Connection ] +``` + +## DESCRIPTION + +Captures an Enterprise Wiki page without requiring or inspecting a target connection. The resulting `enterprise-wiki-export.json` contains a digest-sealed source snapshot. + +The snapshot enumerates every field definition in the source Pages library. Each field receives an entry even when SharePoint did not return a value or the current importer does not understand its runtime type. Entries retain identity, title, type, full schema XML, flags, capture status, structured known values, and best-effort raw type/text/JSON evidence. A later planner can therefore recover newly supported fields without reading the source again. + +Publishing HTML, shared Web Parts, dependencies, security, lifecycle evidence, and a before/after source stability fence are also captured. The returned `EnterpriseWikiExportResult.ExportPath` binds directly to `New-PnPEnterpriseWikiMigrationPlan`. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$source = Connect-PnPOnline https://contoso.sharepoint.com/sites/legacy -Interactive -ReturnConnection +Export-PnPEnterpriseWikiPackage -Identity Pages/R11.aspx -OutputPath .\R11 -Connection $source +``` + +Writes `.\R11\enterprise-wiki-export.json`. No target connection is needed. + +### EXAMPLE 2 + +```powershell +Export-PnPEnterpriseWikiPackage -All -OutputPath .\wiki-export -Connection $source +``` + +Exports every Enterprise Wiki Page into a separate numbered directory. + +### EXAMPLE 3 + +```powershell +$export = Export-PnPEnterpriseWikiPackage Pages/R11.aspx -OutputPath .\R11 -Connection $source +$export.Snapshot.Fields | Select-Object InternalName, TypeAsString, CaptureStatus, HasValue, Kind, RawType +``` + +Reviews the complete source field inventory. + +## PARAMETERS + +### -All + +Exports all pages whose content type derives from Enterprise Wiki Page. Project Page is excluded. + +```yaml +Type: SwitchParameter +Parameter Sets: All +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Connection + +Optional connection to the source web. Retrieve it with `Connect-PnPOnline -ReturnConnection` or `Get-PnPConnection`. + +```yaml +Type: PnPConnection +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrites an existing local export file. It does not change migration eligibility or overwrite SharePoint content. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Identity + +Source page name, relative path, server-relative path, or absolute URL. + +```yaml +Type: String +Parameter Sets: Identity +Aliases: ServerRelativeUrl + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -MaximumDependencyBytes + +Maximum size of each referenced SharePoint resource embedded in the snapshot. The default is 10 MiB. + +```yaml +Type: Int64 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: 10485760 +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -NoWebParts + +Skips shared Web Part export. Publishing HTML and all list-item fields are still captured. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OutputPath + +Local file or directory for the source export. A directory produces `enterprise-wiki-export.json`. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Verbose + +Shows detailed information about the operation. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: vb + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[New-PnPEnterpriseWikiMigrationPlan](New-PnPEnterpriseWikiMigrationPlan.md) diff --git a/documentation/Import-PnPEnterpriseWikiMigrationPackage.md b/documentation/Import-PnPEnterpriseWikiMigrationPackage.md new file mode 100644 index 000000000..0ce764030 --- /dev/null +++ b/documentation/Import-PnPEnterpriseWikiMigrationPackage.md @@ -0,0 +1,221 @@ +--- +Module Name: PnP.PowerShell +title: Import-PnPEnterpriseWikiMigrationPackage +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/Import-PnPEnterpriseWikiMigrationPackage.html +--- + +# Import-PnPEnterpriseWikiMigrationPackage + +## SYNOPSIS + +**Required Permissions** + +* SharePoint: Sites.FullControl.All (application) or AllSites.FullControl (delegated) + +Imports an Enterprise Wiki page from an explicitly approved migration package. + +## SYNTAX + +### Approved + +```powershell +Import-PnPEnterpriseWikiMigrationPackage [-PackagePath] -ApprovedPlanDigest + [-ReceiptPath ] [-Force] [-Connection ] [-WhatIf] [-Confirm] +``` + +### AutoApprove + +```powershell +Import-PnPEnterpriseWikiMigrationPackage [-PackagePath] -AutoApprove + [-ReceiptPath ] [-Force] [-Connection ] [-WhatIf] [-Confirm] +``` + +## DESCRIPTION + +Validates both digests, verifies the target connection, repeats target preflight, and executes only the sealed actions. It does not reconnect to the source or silently reinterpret fields. + +Only field actions marked `Apply` are written. The receipt lists every field action, whether it was attempted, whether it succeeded, and its message. A page planned as `Published` is published only when planned fields succeed. All other source lifecycle states are checked in as `Draft`. Import is create-only; `-Force` only controls local receipt overwrite. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$plan = New-PnPEnterpriseWikiMigrationPlan .\R11\enterprise-wiki-export.json -Connection $target +Import-PnPEnterpriseWikiMigrationPackage $plan.PackagePath -ApprovedPlanDigest $plan.PlanDigest -Connection $target +``` + +Imports exactly the plan whose digest was reviewed. + +### EXAMPLE 2 + +```powershell +Export-PnPEnterpriseWikiPackage Pages/R11.aspx -OutputPath .\R11 -Connection $source | + New-PnPEnterpriseWikiMigrationPlan -Connection $target | + Import-PnPEnterpriseWikiMigrationPackage -AutoApprove -Connection $target +``` + +Runs the three-stage pipeline. `-AutoApprove` skips separate human digest entry, but not validation. + +### EXAMPLE 3 + +```powershell +$receipt = Import-PnPEnterpriseWikiMigrationPackage .\R11\enterprise-wiki-package.json -ApprovedPlanDigest $digest -Connection $target +$receipt.FieldResults | Format-Table InternalName, PlannedDisposition, Attempted, Succeeded, Message +$receipt | Select-Object ExpectedLifecycle, ActualFileLevel, LifecycleMatched +``` + +Reviews field execution and fresh lifecycle readback. + +## PARAMETERS + +### -ApprovedPlanDigest + +Exact SHA-256 plan digest that was reviewed. + +```yaml +Type: String +Parameter Sets: Approved +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AutoApprove + +Uses the digest contained in the package. It does not bypass digest validation or blockers. + +```yaml +Type: SwitchParameter +Parameter Sets: AutoApprove +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm + +Prompts before importing. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Connection + +Optional connection to the exact target web in the plan. + +```yaml +Type: PnPConnection +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrites an existing local receipt. It does not overwrite target content or bypass blockers. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PackagePath + +Path to `enterprise-wiki-package.json` or its directory. Accepts a plan result through `PackagePath`. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: Path + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue, ByPropertyName) +Accept wildcard characters: False +``` + +### -ReceiptPath + +Local file or directory for `enterprise-wiki-import-receipt.json`. The default is next to the package. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Verbose + +Shows detailed information about the operation. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: vb + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf + +Shows what would be imported without writing to SharePoint. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[New-PnPEnterpriseWikiMigrationPlan](New-PnPEnterpriseWikiMigrationPlan.md) diff --git a/documentation/New-PnPEnterpriseWikiMigrationPlan.md b/documentation/New-PnPEnterpriseWikiMigrationPlan.md new file mode 100644 index 000000000..7348abad4 --- /dev/null +++ b/documentation/New-PnPEnterpriseWikiMigrationPlan.md @@ -0,0 +1,221 @@ +--- +Module Name: PnP.PowerShell +title: New-PnPEnterpriseWikiMigrationPlan +schema: 2.0.0 +applicable: SharePoint Online +external help file: PnP.PowerShell.dll-Help.xml +online version: https://pnp.github.io/powershell/cmdlets/New-PnPEnterpriseWikiMigrationPlan.html +--- + +# New-PnPEnterpriseWikiMigrationPlan + +## SYNOPSIS + +**Required Permissions** + +* SharePoint: Sites.Read.All (application) or AllSites.Read (delegated) + +Creates a target-specific, digest-sealed Enterprise Wiki migration plan and complete review report. + +## SYNTAX + +```powershell +New-PnPEnterpriseWikiMigrationPlan [-ExportPath] [-OutputPath ] + [-TargetPageName ] [-AllowUniquePermissions] [-AllowManagedMetadataSubstitution] + [-BlockExternalResources] [-Force] [-Connection ] +``` + +## DESCRIPTION + +Reads a source-only export, probes the target web represented by `-Connection`, and writes `enterprise-wiki-package.json` plus `enterprise-wiki-report.md`. It does not modify SharePoint. + +Every source field remains in the snapshot. Only recognized, writable, type-compatible fields with supported values receive `Apply`. Unrecognized fields receive `EvidenceOnly`; user, lookup, and taxonomy values receive `RequiresMapping`; every other skip has an explicit disposition and reason. + +Lifecycle has no publish Boolean. Source `Level = Published` maps to `Published` when checkout and moderation evidence do not conflict. Every other or contradictory state maps conservatively to `Draft`. + +The report covers all envelope, source, policy, fence, lifecycle, content, field, Web Part, dependency, security, target-probe, replacement, assertion, blocker, and warning data. Large payloads are shown by length, SHA-256, and preview while the full value remains in JSON. + +Real captured examples: R11 had version `1.1`, checkout `Online`, level `Draft`, and moderation status `3`, so its new target lifecycle is `Draft`. E05 had checkout `None`, level `Published`, and moderation status `0`, so its target lifecycle is `Published`. An unknown `OOCLReference` value remains in the snapshot with `EvidenceOnly` and can be recovered by a future mapper. + +## EXAMPLES + +### EXAMPLE 1 + +```powershell +$target = Connect-PnPOnline https://contoso.sharepoint.com/sites/new -Interactive -ReturnConnection +New-PnPEnterpriseWikiMigrationPlan .\R11\enterprise-wiki-export.json -TargetPageName R11.aspx -Connection $target +``` + +Creates the package and report next to the export. + +### EXAMPLE 2 + +```powershell +Export-PnPEnterpriseWikiPackage Pages/R11.aspx -OutputPath .\R11 -Connection $source | + New-PnPEnterpriseWikiMigrationPlan -TargetPageName R11.aspx -Connection $target +``` + +Uses pipeline binding through `EnterpriseWikiExportResult.ExportPath`. + +### EXAMPLE 3 + +```powershell +$plan = New-PnPEnterpriseWikiMigrationPlan .\R11\enterprise-wiki-export.json -Connection $target +$plan.Plan.FieldActions | Group-Object Disposition | Select-Object Name, Count +$plan.ReportPath +$plan.PlanDigest +``` + +Reviews field decisions, the report path, and the approval digest. + +## PARAMETERS + +### -AllowManagedMetadataSubstitution + +Allows planning to continue without a reviewed taxonomy mapping. Those values remain `RequiresMapping` and are not written. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AllowUniquePermissions + +Allows planning to continue for a source page with unique permissions. Permissions remain evidence-only. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -BlockExternalResources + +Makes external renderable resources blockers instead of preserving their original external URLs. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Connection + +Optional connection to the target web. Retrieve it with `Connect-PnPOnline -ReturnConnection` or `Get-PnPConnection`. + +```yaml +Type: PnPConnection +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExportPath + +Path to `enterprise-wiki-export.json` or its directory. Accepts an export result through `ExportPath`. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: Path + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue, ByPropertyName) +Accept wildcard characters: False +``` + +### -Force + +Overwrites existing local package and report files. It does not bypass blockers or overwrite target content. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OutputPath + +Local file or directory for the migration package. By default, files are written next to the export. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -TargetPageName + +Target page filename, server-relative path, or absolute URL. The source filename is used by default. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Verbose + +Shows detailed information about the operation. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: vb + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Export-PnPEnterpriseWikiPackage](Export-PnPEnterpriseWikiPackage.md) + +[Import-PnPEnterpriseWikiMigrationPackage](Import-PnPEnterpriseWikiMigrationPackage.md) diff --git a/src/Commands/Model/EnterpriseWikiExportResult.cs b/src/Commands/Model/EnterpriseWikiExportResult.cs new file mode 100644 index 000000000..f695c5cad --- /dev/null +++ b/src/Commands/Model/EnterpriseWikiExportResult.cs @@ -0,0 +1,36 @@ +using PnP.Framework.EnterpriseWiki; +using System; + +namespace PnP.PowerShell.Commands.Model +{ + /// + /// Describes a sealed source-only Enterprise Wiki export and its local file. + /// + public sealed class EnterpriseWikiExportResult + { + public EnterpriseWikiExportResult(EnterpriseWikiExportPackage export, string exportPath) + { + Export = export ?? throw new ArgumentNullException(nameof(export)); + ExportPath = string.IsNullOrWhiteSpace(exportPath) + ? throw new ArgumentException("An export path is required.", nameof(exportPath)) + : exportPath; + } + + public EnterpriseWikiExportPackage Export { get; } + + public string ExportPath { get; } + + public string SchemaVersion => Export.SchemaVersion; + + public DateTimeOffset ExportedAtUtc => Export.ExportedAtUtc; + + public string SnapshotDigest => Export.SnapshotDigest; + + public EnterpriseWikiSnapshot Snapshot => Export.Snapshot; + + public override string ToString() + { + return ExportPath; + } + } +} diff --git a/src/Commands/Model/EnterpriseWikiMigrationPlanResult.cs b/src/Commands/Model/EnterpriseWikiMigrationPlanResult.cs new file mode 100644 index 000000000..4d7230bc8 --- /dev/null +++ b/src/Commands/Model/EnterpriseWikiMigrationPlanResult.cs @@ -0,0 +1,54 @@ +using PnP.Framework.EnterpriseWiki; +using System; + +namespace PnP.PowerShell.Commands.Model +{ + /// + /// Describes a sealed Enterprise Wiki migration plan and its review artifacts. + /// + public sealed class EnterpriseWikiMigrationPlanResult + { + public EnterpriseWikiMigrationPlanResult( + EnterpriseWikiMigrationPackage package, + string packagePath, + string reportPath) + { + Package = package ?? throw new ArgumentNullException(nameof(package)); + PackagePath = string.IsNullOrWhiteSpace(packagePath) + ? throw new ArgumentException("A package path is required.", nameof(packagePath)) + : packagePath; + ReportPath = string.IsNullOrWhiteSpace(reportPath) + ? throw new ArgumentException("A report path is required.", nameof(reportPath)) + : reportPath; + } + + public EnterpriseWikiMigrationPackage Package { get; } + + public string PackagePath { get; } + + public string ReportPath { get; } + + public string SchemaVersion => Package.SchemaVersion; + + public DateTimeOffset PlannedAtUtc => Package.PlannedAtUtc; + + public EnterpriseWikiPackageState State => Package.State; + + public bool IsExecutable => Package.Plan?.IsExecutable == true; + + public EnterpriseWikiSnapshot Snapshot => Package.Snapshot; + + public EnterpriseWikiMigrationPlan Plan => Package.Plan; + + public string SnapshotDigest => Package.SnapshotDigest; + + public string PlanDigest => Package.PlanDigest; + + public EnterpriseWikiCustomerReport Report => Package.Report; + + public override string ToString() + { + return PackagePath; + } + } +} diff --git a/src/Commands/Pages/ExportEnterpriseWikiPackage.cs b/src/Commands/Pages/ExportEnterpriseWikiPackage.cs new file mode 100644 index 000000000..0cbcd2407 --- /dev/null +++ b/src/Commands/Pages/ExportEnterpriseWikiPackage.cs @@ -0,0 +1,119 @@ +using PnP.Framework.EnterpriseWiki; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Pages +{ + [Cmdlet(VerbsData.Export, "PnPEnterpriseWikiPackage", DefaultParameterSetName = ParameterSetIdentity)] + [OutputType(typeof(EnterpriseWikiExportResult))] + [RequiredApiApplicationPermissions("sharepoint/Sites.Read.All")] + [RequiredApiDelegatedPermissions("sharepoint/AllSites.Read")] + public class ExportEnterpriseWikiPackage : PnPWebCmdlet + { + private const string ParameterSetIdentity = "Identity"; + private const string ParameterSetAll = "All"; + + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ParameterSetName = ParameterSetIdentity)] + [Alias("ServerRelativeUrl")] + [ValidateNotNullOrEmpty] + public string Identity { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = ParameterSetAll)] + public SwitchParameter All { get; set; } + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string OutputPath { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter NoWebParts { get; set; } + + [Parameter(Mandatory = false)] + [ValidateRange(1, long.MaxValue)] + public long MaximumDependencyBytes { get; set; } = 10 * 1024 * 1024; + + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + protected override void ExecuteCmdlet() + { + var service = new EnterpriseWikiMigrationService(); + IReadOnlyList sourcePages = ParameterSetName == ParameterSetAll + ? service.Discover(Connection.Context) + : new[] { Identity }; + if (sourcePages.Count == 0) + { + WriteVerbose("No Enterprise Wiki pages were found in the current web."); + return; + } + + var outputRoot = ResolveLocalPath(OutputPath); + for (var index = 0; index < sourcePages.Count; index++) + { + var sourcePage = sourcePages[index]; + var itemOutputPath = ParameterSetName == ParameterSetAll + ? Path.Combine(outputRoot, $"{index + 1:D3}-{MakeSafeDirectoryName(Path.GetFileNameWithoutExtension(GetLeafName(sourcePage)))}") + : outputRoot; + + WriteProgress(new ProgressRecord( + 181, + "Export Enterprise Wiki source snapshot", + $"{index + 1}/{sourcePages.Count}: {sourcePage}") + { + PercentComplete = (index * 100) / sourcePages.Count + }); + + var export = service.Export(Connection.Context, new EnterpriseWikiExportOptions + { + SourcePageServerRelativeUrl = sourcePage, + IncludeWebParts = !NoWebParts, + MaximumDependencyBytes = MaximumDependencyBytes + }); + var exportPath = EnterpriseWikiPackageSerializer.SaveExport(itemOutputPath, export, Force); + WriteVerbose($"Enterprise Wiki export written to '{exportPath}'. Snapshot digest: {export.SnapshotDigest}"); + foreach (var warning in export.Snapshot.Warnings) + { + WriteWarning(warning); + } + foreach (var blocker in export.Snapshot.Blockers) + { + WriteWarning($"BLOCKER: {blocker}"); + } + WriteObject(new EnterpriseWikiExportResult(export, exportPath)); + } + + WriteProgress(new ProgressRecord(181, "Export Enterprise Wiki source snapshot", "Completed") + { + RecordType = ProgressRecordType.Completed + }); + } + + private string ResolveLocalPath(string value) + { + return Path.IsPathRooted(value) + ? Path.GetFullPath(value) + : Path.GetFullPath(Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, value)); + } + + private static string GetLeafName(string value) + { + var path = Uri.TryCreate(value, UriKind.Absolute, out var absolute) ? absolute.AbsolutePath : value; + path = Uri.UnescapeDataString(path ?? string.Empty).Replace('\\', '/').TrimEnd('/'); + var separator = path.LastIndexOf('/'); + return separator < 0 ? path : path.Substring(separator + 1); + } + + private static string MakeSafeDirectoryName(string value) + { + var invalid = Path.GetInvalidFileNameChars(); + return new string((value ?? "enterprise-wiki") + .Select(character => invalid.Contains(character) ? '-' : character) + .ToArray()); + } + } +} diff --git a/src/Commands/Pages/ImportEnterpriseWikiMigrationPackage.cs b/src/Commands/Pages/ImportEnterpriseWikiMigrationPackage.cs new file mode 100644 index 000000000..7c3b2bf5b --- /dev/null +++ b/src/Commands/Pages/ImportEnterpriseWikiMigrationPackage.cs @@ -0,0 +1,78 @@ +using PnP.Framework.EnterpriseWiki; +using PnP.PowerShell.Commands.Attributes; +using System; +using System.IO; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Pages +{ + [Cmdlet(VerbsData.Import, "PnPEnterpriseWikiMigrationPackage", DefaultParameterSetName = ParameterSetApproved, SupportsShouldProcess = true)] + [OutputType(typeof(EnterpriseWikiImportReceipt))] + [RequiredApiApplicationPermissions("sharepoint/Sites.FullControl.All")] + [RequiredApiDelegatedPermissions("sharepoint/AllSites.FullControl")] + public class ImportEnterpriseWikiMigrationPackage : PnPWebCmdlet + { + private const string ParameterSetApproved = "Approved"; + private const string ParameterSetAutoApprove = "AutoApprove"; + + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Alias("Path")] + [ValidateNotNullOrEmpty] + public string PackagePath { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = ParameterSetApproved)] + [ValidateNotNullOrEmpty] + public string ApprovedPlanDigest { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = ParameterSetAutoApprove)] + public SwitchParameter AutoApprove { get; set; } + + [Parameter(Mandatory = false)] + public string ReceiptPath { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + protected override void ExecuteCmdlet() + { + var resolvedPackagePath = ResolveLocalPath(PackagePath); + var package = EnterpriseWikiPackageSerializer.LoadMigration(resolvedPackagePath); + var approvedDigest = ParameterSetName == ParameterSetAutoApprove + ? package.PlanDigest + : ApprovedPlanDigest; + if (!ShouldProcess( + package.Plan.TargetPageServerRelativeUrl, + $"Import Enterprise Wiki page from approved plan {approvedDigest}")) + { + return; + } + + var service = new EnterpriseWikiMigrationService(); + var receipt = service.Import(Connection.Context, package, approvedDigest); + var receiptPath = string.IsNullOrWhiteSpace(ReceiptPath) + ? Path.GetDirectoryName(ResolvePackageFile(resolvedPackagePath)) + : ResolveLocalPath(ReceiptPath); + var savedReceiptPath = EnterpriseWikiPackageSerializer.SaveReceipt(receiptPath, receipt, Force); + WriteVerbose($"Enterprise Wiki import receipt written to '{savedReceiptPath}'."); + foreach (var warning in receipt.Warnings) + { + WriteWarning(warning); + } + WriteObject(receipt); + } + + private string ResolveLocalPath(string value) + { + return Path.IsPathRooted(value) + ? Path.GetFullPath(value) + : Path.GetFullPath(Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, value)); + } + + private static string ResolvePackageFile(string value) + { + return Directory.Exists(value) || string.IsNullOrEmpty(Path.GetExtension(value)) + ? Path.Combine(value, EnterpriseWikiPackageSerializer.DefaultPackageFileName) + : value; + } + } +} diff --git a/src/Commands/Pages/NewEnterpriseWikiMigrationPlan.cs b/src/Commands/Pages/NewEnterpriseWikiMigrationPlan.cs new file mode 100644 index 000000000..3f3d05b93 --- /dev/null +++ b/src/Commands/Pages/NewEnterpriseWikiMigrationPlan.cs @@ -0,0 +1,128 @@ +using Microsoft.SharePoint.Client; +using PnP.Framework.EnterpriseWiki; +using PnP.Framework.Utilities; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Model; +using System; +using System.IO; +using System.Management.Automation; + +namespace PnP.PowerShell.Commands.Pages +{ + [Cmdlet(VerbsCommon.New, "PnPEnterpriseWikiMigrationPlan")] + [OutputType(typeof(EnterpriseWikiMigrationPlanResult))] + [RequiredApiApplicationPermissions("sharepoint/Sites.Read.All")] + [RequiredApiDelegatedPermissions("sharepoint/AllSites.Read")] + public class NewEnterpriseWikiMigrationPlan : PnPWebCmdlet + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [Alias("Path")] + [ValidateNotNullOrEmpty] + public string ExportPath { get; set; } + + [Parameter(Mandatory = false)] + public string OutputPath { get; set; } + + [Parameter(Mandatory = false)] + public string TargetPageName { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter AllowUniquePermissions { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter AllowManagedMetadataSubstitution { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter BlockExternalResources { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + protected override void ExecuteCmdlet() + { + var resolvedExportPath = ResolveLocalPath(ExportPath); + var export = EnterpriseWikiPackageSerializer.LoadExport(resolvedExportPath); + Connection.Context.Load(Connection.Context.Web, web => web.ServerRelativeUrl); + Connection.Context.ExecuteQueryRetry(); + var targetPages = Connection.Context.Web.GetPagesLibrary(); + var targetPagesRoot = $"{Connection.Context.Web.ServerRelativeUrl.TrimEnd('/')}/Pages"; + if (targetPages != null) + { + Connection.Context.Load(targetPages.RootFolder, folder => folder.ServerRelativeUrl); + Connection.Context.ExecuteQueryRetry(); + targetPagesRoot = targetPages.RootFolder.ServerRelativeUrl; + } + + var targetPagePath = ResolveTargetPagePath( + targetPagesRoot, + string.IsNullOrWhiteSpace(TargetPageName) + ? GetLeafName(export.Snapshot.Source.PageServerRelativeUrl) + : TargetPageName); + + var service = new EnterpriseWikiMigrationService(); + var package = service.Plan(Connection.Context, export, new EnterpriseWikiPlanningOptions + { + TargetPageServerRelativeUrl = targetPagePath, + RequireInheritedPermissions = !AllowUniquePermissions, + BlockOnManagedMetadata = !AllowManagedMetadataSubstitution, + AllowExternalResourceReferences = !BlockExternalResources, + CreateOnly = true + }); + var packageOutput = string.IsNullOrWhiteSpace(OutputPath) + ? Path.GetDirectoryName(ResolveExportFile(resolvedExportPath)) + : ResolveLocalPath(OutputPath); + var packagePath = EnterpriseWikiPackageSerializer.SaveMigration(packageOutput, package, Force); + var reportPath = Path.Combine( + Path.GetDirectoryName(packagePath) ?? string.Empty, + EnterpriseWikiPackageSerializer.DefaultReportFileName); + WriteVerbose($"Enterprise Wiki migration plan written to '{packagePath}'. Plan digest: {package.PlanDigest}"); + foreach (var warning in package.Plan.Warnings) + { + WriteWarning(warning); + } + foreach (var blocker in package.Plan.Blockers) + { + WriteWarning($"BLOCKER: {blocker}"); + } + WriteObject(new EnterpriseWikiMigrationPlanResult(package, packagePath, reportPath)); + } + + private string ResolveLocalPath(string value) + { + return Path.IsPathRooted(value) + ? Path.GetFullPath(value) + : Path.GetFullPath(Path.Combine(SessionState.Path.CurrentFileSystemLocation.Path, value)); + } + + private static string ResolveTargetPagePath(string pagesRoot, string value) + { + var candidate = value.Trim(); + if (Uri.TryCreate(candidate, UriKind.Absolute, out _) || candidate.StartsWith("/", StringComparison.Ordinal)) + { + return candidate; + } + + var fileName = candidate.ReplaceInvalidUrlChars("-"); + if (!fileName.EndsWith(".aspx", StringComparison.OrdinalIgnoreCase)) + { + fileName += ".aspx"; + } + return $"{pagesRoot.TrimEnd('/')}/{fileName}"; + } + + private static string ResolveExportFile(string value) + { + return Directory.Exists(value) || string.IsNullOrEmpty(Path.GetExtension(value)) + ? Path.Combine(value, EnterpriseWikiPackageSerializer.DefaultExportFileName) + : value; + } + + private static string GetLeafName(string value) + { + var path = Uri.TryCreate(value, UriKind.Absolute, out var absolute) ? absolute.AbsolutePath : value; + path = Uri.UnescapeDataString(path ?? string.Empty).Replace('\\', '/').TrimEnd('/'); + var separator = path.LastIndexOf('/'); + return separator < 0 ? path : path.Substring(separator + 1); + } + } +}