Friday, November 29, 2013

PowerShell script for fixing urls inside Content editor and Image viewer web parts in Sharepoint

Sometimes it may be needed to fix urls added by content editors in web parts (in this article we will consider ContentEditorWebPart and ImageWebPart, but you can extend the script for your own needs as you will need), e.g. when content was created on the test environment and then migrated to production. The following PowerShell script will do it automatically:

   2: function Fix-String($val)
   3: {
   4:     return $val.Replace($oldUrl, $newUrl)
   5: }
   7: function Should-Fix-String($val)
   8: {
   9:     if (-not $val -or -not $val.Contains($oldUrl))
  10:     {
  11:         return $false
  12:     }
  13:     return $true
  14: }
  16: function Fix-Page-Field($item, $fieldName)
  17: {
  18:     Write-Host "Check field $fieldName" -foregroundcolor green
  19:     $val = $item[$fieldName]
  20:     if (-not $val)
  21:     {
  22:         Write-Host "Field value is empty" -foregroundcolor green
  23:         return
  24:     }
  26:     $src = ""
  27:     if ($val.GetType().Name -eq "ImageFieldValue")
  28:     {
  29:         $src = $val.ImageUrl
  30:     }
  31:     else
  32:     {
  33:         $src = $val
  34:     }
  36:     $shouldFix = Should-Fix-String $src
  37:     if (-not $shouldFix)
  38:     {
  39:         Write-Host "Field value doesn't have links which should be fixed" -foregroundcolor green
  40:         return
  41:     }
  43:     $src = Fix-String $src
  44:     if ($val.GetType().Name -eq "ImageFieldValue")
  45:     {
  46:         $val.ImageUrl = $src
  47:         $item[$fieldName] = $val
  48:     }
  49:     else
  50:     {
  51:         $item[$fieldName] = $src
  52:     }
  53:     Write-Host "Field value was sucessfully fixed" -foregroundcolor green
  54: }
  56: function Fix-Web-Part($wp, $webPartManager)
  57: {
  58:     Write-Host "Check links in web part" $wp.Title -foregroundcolor green
  59:     $src = ""
  60:     if ($wp.GetType().Name -eq "ContentEditorWebPart")
  61:     {
  62:         $src = $wp.Content.InnerText
  63:     }
  64:     elseif ($wp.GetType().Name -eq "ImageWebPart")
  65:     {
  66:         $src = $wp.ImageLink
  67:     }
  68:     else
  69:     {
  70:         Write-Host "Web part type" $wp.GetType().Name "is not supported" -foregroundcolor yellow
  71:     }
  73:     $shouldFix = Should-Fix-String $src
  74:     if (-not $shouldFix)
  75:     {
  76:         Write-Host "Web part content doesn't have links which should be fixed" -foregroundcolor green
  77:         return
  78:     }
  80:     $src = Fix-String $src
  81:     if ($wp.GetType().Name -eq "ContentEditorWebPart")
  82:     {
  83:         $content = $wp.Content
  84:         $content.InnerText = $src
  85:         $wp.Content = $content
  86:         $webPartManager.SaveChanges($wp)
  87:     }
  88:     elseif ($wp.GetType().Name -eq "ImageWebPart")
  89:     {
  90:         $wp.ImageLink = $src
  91:         $webPartManager.SaveChanges($wp)
  92:     }
  93:     Write-Host "Web part content was successfully fixed" -foregroundcolor green
  94: }
  96: function Fix-Page($item)
  97: {
  98:     $file = $item.File
  99:     Write-Host "Check links on page" $file.Url -foregroundcolor green
 101:     $shouldBePublished = $false
 102:     if ($file.Level -eq [Microsoft.SharePoint.SPFileLevel]::Published)
 103:     {
 104:         $shouldBePublished = $true
 105:     }
 107:     if ($file.CheckOutType -ne [Microsoft.SharePoint.SPFile+SPCheckOutType]::None)
 108:     {
 109:        Write-Host "Undo checkout for page" $file.Url -foregroundcolor yellow
 110:        $file.UndoCheckOut()
 111:     }
 112:     $file.CheckOut()
 114:     # check field value first
 115:     Fix-Page-Field $item "PublishingPageContent"
 117:     # then content inside web parts
 118:     $webPartManager = $file.Web.GetLimitedWebPartManager($file.Url,
 119: [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
 120:     $webParts = @()
 121:     foreach ($wp in $webPartManager.WebParts)
 122:     {
 123:         Write-Host "Found web part" $wp.GetType().Name
 124:         if ($wp.GetType().Name -eq "ContentEditorWebPart" -or
 125: $wp.GetType().Name -eq "ImageWebPart")
 126:         {
 127:             $webParts += $wp
 128:         }
 129:     }
 131:     if ($webParts.Count -eq 0)
 132:     {
 133:         return
 134:     }
 136:     $webParts | ForEach-Object { Fix-Web-Part $_ $webPartManager }
 138:     Write-Host "Update and checkin page" $file.Url -foregroundcolor green
 139:     $item.Update()
 140:     $file.Update()
 141:     $file.CheckIn("Change links.")
 142:     if ($shouldBePublished)
 143:     {
 144:         Write-Host "Publish page" $file.Url -foregroundcolor green
 145:         $file.Publish("Change links.")
 146:         if ($file.DocumentLibrary.EnableModeration)
 147:         {
 148:             $file.Approve("System Approve - change links.")
 149:         }
 150:     }
 151: }
 153: function Fix-Links-On-Web($w)
 154: {
 155:     Write-Host "Check links on web" $w.Url -foregroundcolor green
 156:     $pw = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($w)
 157:     $pagesList = $pw.PagesList
 158:     $pagesList.Items | ForEach-Object { Fix-Page $_ }
 159: }
 161: function Fix-Links-On-Site($s)
 162: {
 163:     Write-Host "Check links on site" $s.Url -foregroundcolor green
 164:     $s.AllWebs | ForEach-Object { Fix-Links-On-Web $_ }
 165: }
 167: Start-Transcript -Path "output.log" -Append -Force -Confirm:$false
 168: $oldUrl = "/"
 169: $newUrl = "/"
 170: $webApp = Get-SPWebApplication ""
 171: $webApp.Sites | ForEach-Object { Fix-Links-On-Site $_ }
 172: Stop-Transcript

Script enumerates through all site collections, sites, pages and web parts, finds all ContentEditorWebPart and ImageWebPart on the pages and then changes old url (stored in $oldUrl variable) on new url ($newUrl). Also it fixes links inside standard PublishingPageContent field of the publishing pages.

As I wrote you can easily extend it for changing web parts of other types. Hope that it will helpful.

Thursday, November 28, 2013

Fix TaxonomyHiddenList guid after export/import of site collections in Sharepoint

In this post I will describe one more problem with using SPExport/SPImport API (see previous similar posts here, here and here). As you probably know TaxonomyHiddenList is special hidden list used by Sharepoint for using managed metadata on your site. It is located in the root site of site collection. One thing which is not so well known is that id of this list is also stored in the property bag of the site collection’s root web (SPWeb.Properties) in “TaxonomyHiddenList” key. The problem is that if you export existing site collection with SPExport API and then import it with SPImport with SPImportSettings.RetainObjectIdentity = false (which means that all objects in imported site will have new guids), list id is changed while value in property bag remains the same from source site collection. It causes many problems with managed metadata on exported site (e.g. it is not possible to save values in managed metadata fields).

In order to fix TaxonomyHiddenList value in the property bag I created the following PowerShell script:

   1:  function Fix-Taxonomy-Hidden-List($w)
   2:  {
   3:      $pv = $w.Properties["TaxonomyHiddenList"]
   4:      $l = $w.Lists["TaxonomyHiddenList"]
   5:      $id = $l.ID
   6:      Write-Host "    Property bag value: '$pv', list id: '$id'"
   7:      if ($pv.ToString().ToLower() -ne $id.ToString().ToLower())
   8:      {
   9:          Write-Host "    Property bag value differs from list id. It will be fixed" -foregroundcolor yellow
  10:          $w.Properties["TaxonomyHiddenList"] = $id
  11:          $w.Properties.Update()
  12:          Write-Host "    Property bag value was sucessfully fixed" -foregroundcolor green
  13:      }
  14:      else
  15:      {
  16:          Write-Host "    Property bag value equals to list id. Nothing should be fixed" -foregroundcolor green
  17:      }
  18:  }
  20:  $wa = Get-SPWebApplication ""
  21:  foreach ($s in $wa.Sites)
  22:  {
  23:      Write-Host "Checking $s.Url..."
  24:      Fix-Taxonomy-Hidden-List $s.RootWeb
  25:  }

It enumerates all site collections in specified web application (lines 21-25), checks value for “TaxonomyHiddenList” key in web property bag, compares it with TaxonomyHiddenList list guid and if they are different, it stores new value into property bag. You may run this script several times: after first time it will see that value in property bag equals the list id and won’t do anything. In order to use it on your environment change web application url on line 20 on your own. Hope it will help someone.

Monday, November 18, 2013

How to restore site collection from higher Sharepoint version

Sometimes you may face with situation that bug is only reproducibly on production, but not e.g. on QA or on your local development environment. Such problems are much harder to troubleshoot. Often they are caused by the content which exist only on production. And if troubleshooting directly on production is problematic (e.g. if you don’t have remote desktop access to it), you should get backup of site collection or whole content database, restore it on local dev env and try to reproduce bug here. But what to do if you have lower Sharepoint version on you local environment, than on production? Of course it is better to have the same versions, but world is not ideal and sometimes we may face with such situation. In this post I will show the trick of how to restore site collection from the higher Sharepoint version. Before to start I need to warn that this is actually a hack and you should not rely on it. There is no guarantee that it will work in your particular case, because new Sharepoint version may have different schema, incompatible with previous one (that’s why standard way is not allowed).

Ok, suppose that we have site collection backup, which is created with Backup-SPSite cmdlet:

Backup-SPSite -Path C:\Backup\example1.bak

We copied it on local environment and want to restore it with Restore-SPSite:

Restore-SPSite -Path C:\Backup\example1.bak –Confirm:$false

(Here I intentionally used different urls for source and target sites in order to show that it is possible to restore site collection to the different url). If we have lower Sharepoint version on the local environment we will get unclear nativehr exception, which won’t say anything. But if we will make our logging verbose and check Sharepoint logs, we will find the following error:

Could not deserialize site from C:\Backup\example1.bak. Microsoft.SharePoint.SPException: Schema version of backup 15.0.4505.1005 does not match current schema version 15.0.4420.1017.

(Exact version number is not important. For this post it is only important that source version 15.0.4505.1005 is higher than target version 15.0.4420.1017).

What to do in this case? Mount-SPContentDatabase also won’t work because of the same reason, i.e. content database backup also won’t work. In this case we can either update our environment (and you should consider this option as basic) or go by non-standard way.

For non-standard way we will need hex editor. At first I thought that site collection backup is regular .cab file, so it will be possible to uncompress it, edit text files inside it and compress back (I described this trick in this post: Retain object identity during export and import of subsites in Sharepoint into different site collection hierarchy location), but this is not the case with site collection backups made with mentioned cmdlets. They look like regular binary files. So we will need some hex editor for modifying it. I used HxD hexeditor, but you can use any other as well.

If we will open backup file in it and will try to find the version, which we got from error message from the log, we will find that it is located in the beginning of the file:


The good thing is that version is stored only once. So we will change source version to the target in the hex editor now:


Now save it and run Restore-SPSite again. This time restore should work. Hope that this trick will help someone. But remember that it is hack and use it carefully.