Update thanks to Japser Beerens:
if ($page.File.Level -eq [Microsoft.SharePoint.SPFileLevel]::Checkout)
changed to
if ($page.File.CheckOutType.value__ -ne 2) #2 = SPCheckOutType.None
because first statement would fail if checked-out by someone else.
This post will descibe what you need, and what to watch out for when you are planning to add webparts to a publishing page using powershell. Scroll down and skip the plumbing part for the good bits.
First of all: get PowerGUI (or PowerGui for visual studio using the extension gallery) this will make your life a whole lot easier.
Lets start with the information we have and create a batchfile. This is not required, but makes life a lot easier when you want to execute your powershell script multiple times, or have to deal with a Sys-Op that does not know any powershell.
The case: we would like to add webparts to multiple zones on publishing pages of a portal.
In the webpart gallery we have a custom webpart (CQWP) with the title: "Related Documents"
This webpart needs to be added to the webpartzone with zoneid called "zone1" in all pages that have a certain pagelayout.
We create a batchfile and set all the parameters we need.
PS: The script is capable of putting multiple webparts into a single zone.
Batchfile
echo off
SET URL="http://contoso.com"
SET WEBPARTTOADD="Related Documents"
SET ZONEID="Zone1"
powershell.exe -command .\AddWebParts.ps1 '%URL%' '%WEBPARTTOADD%' '%ZONEID%'
pause
Now we can start on the AddWebParts.ps1 file.
Create the input params
Param($siteUrl,$WebPartsToAdd,$ZoneId);
Set the pagelayoutfilenames we want to add the webparts to, and we know the zoneid exists on.
This is required because the (very)LimitedWebpartManager does not allow us to check if a webpartzone exists in a page!
If the zone does not exist, SharePoint will just put the webpart in the first zone it can find.
$AllowedPageLayouts = "ArticleLeft.aspx","ArticleLeft.aspx";
Add the sharepoint powershell snapin
Add-PSSnapin Microsoft.SharePoint.PowerShell
With all the plumbing done we can start on actually doing something.
function InsertWebPartInWebPartZone($siteUrl, $WebPartsToAdd, $webPartZone)
{
# Create a new instance of the site.
$site = Get-SPSite (New-Object System.Uri($siteUrl));
# Iterate trough all the pages
foreach($web in $site.AllWebs)
{
Write-Host "==============================="
Write-Host "Currently parsing" $web.Title;
Write-Host "==============================="
#check if we are in a publising web
if( ! [Microsoft.SharePoint.Publishing.PublishingWeb]::IsPublishingWeb($web) )
{
Write-Host "Web is not a publishing web, skipping"
continue;
}
# Retrieve the pages library
$pagesLibrary = $web.Lists["Pages"];
$allowunsafeupdates = $web.AllowUnsafeUpdates
$web.AllowUnsafeUpdates = $true
# Iterate through all pages
foreach($page in $pagesLibrary.Items)
{
#check for null
if($page -eq $null)
{
continue;
}
#check if page is checked out already
#if ($page.File.Level -eq [Microsoft.SharePoint.SPFileLevel]::Checkout)
if ($page.File.CheckOutType.value__ -ne 2) #2 = SPCheckOutType.None
{
Write-Host $page.Url "is Checked-Out already. Skipping
}
else
{
#check if page is a publising page and if it is of allowed pagelayouts to change
$pubPage = [Microsoft.SharePoint.Publishing.PublishingPage]::GetPublishingPage($page)
if( ($pubpage -ne $null) -and ($AllowedPageLayouts -contains $pubPage.Layout.Name))
{
Write-Host "Page is of valid layout, processing page (" $page.Name ")"
}
else
{
Write-Host "Page is of invalid layout (" $pubPage.Layout.Name "). Skipping."
continue;
}
Write-Host "starting page processing -----"
# Checking out page
$page.File.CheckOut()
$index = 0;
foreach($webPartToAdd in $WebPartsToAdd)
{
# Retrieve the webpart from the webpartgallery
$webPart = GetWebPartFromGallery $site $webPartToAdd;
if($webpart -eq $null)
{
Write-Host "Unable to retrieve $webPartToAdd to insert. Skipping.";
continue;
}
# Check if the page already contains this webpart, if so, remove it
RemoveWebPartFromPage $page $webPart.Title;
# Add or re-add the webpart to the page in the specified zone
AddWebPartToPage $page $webPart $webPartZone $index;
# Update the index
$index++;
}
# Build up the message
$checkInMessage = "This page is checked in by the Add WebPart Script";
# Check the page in and Publish if required
$page.File.CheckIn($checkInMessage);
if($page.ParentList.EnableMinorVersions -eq $true)
{
$Page.File.Publish("Published");
}
Write-Host "page done"
}
}
$web.AllowUnsafeUpdates = $allowunsafeupdates;
# Dispose of the web
$web.Dispose();
}
}
In the previous function we called several functions, in the next section we will outline these functions
- GetWebPartFromGallery
- RemoveWebPartFromPage
- AddWebPartToPage
Get WebPartFromGallery reads a webparts XML definition. It reads the definition from the webpartgallery and finds the webpart based on the webpart name.
function GetWebPartFromGallery($site, $webPartName)
{
Write-Host "Getting " $webPartName " from gallery."
# Get the rootweb (and get the webpart gallery)
$web = $site.OpenWeb();
$webPartGallery = $web.Lists["Web Part Gallery"]
if($webPartGallery -eq $null)
{
Write-Host("Unable to retrieve Webpartgallery");
}
$webpart = $null;
foreach($wp in $webPartGallery.Items)
{
#find the webpart we are looking for
if($wp.Title -eq $webPartName)
{
$webpart = $wp;
Write-Host "Webpart found in gallery"
break;
}
}
if($webpart -eq $null)
{
Write-Host("Unable to retrieve webpart: $webPartName");
}
return $webpart;
}
The next two functions do all the heavy lifting in this script (the good stuff).
The first function allows us to rerun the script multiple times. This is allows us to make changed to a webpart, upload it to the gallery and replace the existing webparts with new ones on all pages.
It searched the page for a webpart with a certain title. if the webpart is found on the page, the webpart will be removed. After that the second (add) function will be called.
The second function adds the webpart that was read from the gallery onto the page into the specific zone.
Both functions contain a section (needs refactoring :) ) thats sets the HTTPContext. This is the part of the functions that is very interesting, and basically the reason for writing this blogpost.
This code was added, because when we are instantiating webparts that contain URL's (like the CQWP contains an XslItemLink, XslMainLink or XslHeaderLink ) The properties of the webpart class are used to set the values in the webpart object.
These URL properties internally call makeserverrelative and that uses the HTTP context. However, because we are running inside of powershell, these is no HttpContext. This problem does not occur with the standard SharePoint webparts I tested, because the XML definition of these webparts does not contain any URL properties. But when trying to put the customised CQWP (related documents) on the page, I got an error. After a lot of debugging manually creating a HTTPContext was the solution because of the reason stated above.
I hope this information will save someone a lot of time.
#Remove webpart based on Webpart title
function RemoveWebPartFromPage($page, $webPartTitle)
{
#make sure the page is checked-out
if ($page.File.Level -ne [Microsoft.SharePoint.SPFileLevel]::Checkout)
{
Write-Host $page.Url " is not Checked-Out. Cannot remove webpart from the page.";
return $false;
}
# set the current httpcontext, needed for contentquerywebparts webparts that use the
# xslitemlink, xslmainlink or xslheaderlink property
# else makeserverrelative url will fail, because there no context
if ($null -eq [System.Web.HttpContext]::Current)
{
$sw = New-Object System.IO.StringWriter
$resp = New-Object System.Web.HttpResponse $sw
$req = New-Object System.Web.HttpRequest "", $web.Url, ""
$htc = New-Object System.Web.HttpContext $req, $resp
#explicitly cast $web to spweb object else sharepoint will
#see it as a PSObject, and AddWebpart wil fail
$htc.Items["HttpHandlerSPWeb"] = $web -as [Microsoft.SharePoint.SPweb]
[System.Web.HttpContext]::Current = $htc
}
$webPartCollection = $page.Web.GetWebPartCollection($page.Url,[Microsoft.SharePoint.WebPartPages.Storage]::Shared);
$webPartStorageKey = $null;
foreach($wp in $webPartCollection)
{
$webpart = $wp -as [Microsoft.SharePoint.WebPartPages.WebPart];
if($webpart.Title -eq $webPartTitle)
{
$webPartStorageKey = $webpart.StorageKey;
break;
}
}
if($webPartStorageKey -eq $null)
{
return $false;
}
$webPartCollection.Delete($webPartStorageKey);
return $true;
}
# Adds the desired webpart to the page
function AddWebPartToPage($page, $webPartListItem, $webpartZoneId, $index)
{
Write-Host "Adding" $webPartListItem.Name " to " $page.Name;
if ($page.File.Level -ne [Microsoft.SharePoint.SPFileLevel]::Checkout)
{
Write-Host $page.Url "is not Checked-Out. Cannot add webpart to the page.";
return $false;
}
# Get the webpartmanager
$wpManager = $page.Web.GetLimitedWebPartManager($page.Url,[System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared);
# Get the webpart's xml
$errorMsg = "";
$xmlReader = New-Object System.Xml.XmlTextReader($webPartListItem.File.OpenBinaryStream());
$webPart = $wpManager.ImportWebPart($xmlReader, [ref]$errorMsg) -as [Microsoft.SharePoint.WebPartPages.WebPart];
try
{
# set the current httpcontext, needed for contentquerywebparts webparts that use the
# xslitemlink, xslmainlink or xslheaderlink property
# else makeserverrelative url will fail, because there no context
if ($null -eq [System.Web.HttpContext]::Current)
{
$sw = New-Object System.IO.StringWriter
$resp = New-Object System.Web.HttpResponse $sw
$req = New-Object System.Web.HttpRequest "", $web.Url, ""
$htc = New-Object System.Web.HttpContext $req, $resp
#explicitly cast $web to spweb object else sharepoint will
#see it as a PSObject, and AddWebpart wil fail
$htc.Items["HttpHandlerSPWeb"] = $web -as [Microsoft.SharePoint.SPweb]
[System.Web.HttpContext]::Current = $htc
}
# Add webpart to the page
$wpManager.AddWebPart($webPart, $webpartZoneId, $index);
}
catch
{
Write-Host "An error occurred. Unable to add webpart " $webPartListItem.Title;
Write-Host $_.Exception.ToString();
}
[System.Web.HttpContext]::Current = $null
$wpManager.Dispose();
return $true;
}
I always start a Transcript before executing the script.
Start-Transcript
InsertWebPartInWebPartZone $siteUrl $WebPartsToAdd $ZoneId
Stop-Transcript