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:

   1:  
   2: function Fix-String($val)
   3: {
   4:     return $val.Replace($oldUrl, $newUrl)
   5: }
   6:  
   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: }
  15:  
  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:     }
  25:  
  26:     $src = ""
  27:     if ($val.GetType().Name -eq "ImageFieldValue")
  28:     {
  29:         $src = $val.ImageUrl
  30:     }
  31:     else
  32:     {
  33:         $src = $val
  34:     }
  35:  
  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:     }
  42:  
  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: }
  55:  
  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:     }
  72:  
  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:     }
  79:  
  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: }
  95:  
  96: function Fix-Page($item)
  97: {
  98:     $file = $item.File
  99:     Write-Host "Check links on page" $file.Url -foregroundcolor green
 100:  
 101:     $shouldBePublished = $false
 102:     if ($file.Level -eq [Microsoft.SharePoint.SPFileLevel]::Published)
 103:     {
 104:         $shouldBePublished = $true
 105:     }
 106:  
 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()
 113:  
 114:     # check field value first
 115:     Fix-Page-Field $item "PublishingPageContent"
 116:  
 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:     }
 130:  
 131:     if ($webParts.Count -eq 0)
 132:     {
 133:         return
 134:     }
 135:  
 136:     $webParts | ForEach-Object { Fix-Web-Part $_ $webPartManager }
 137:  
 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: }
 152:  
 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: }
 160:  
 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: }
 166:  
 167: Start-Transcript -Path "output.log" -Append -Force -Confirm:$false
 168: $oldUrl = "/example1.com/"
 169: $newUrl = "/example2.com/"
 170: $webApp = Get-SPWebApplication "http://example.com"
 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:  }
  19:   
  20:  $wa = Get-SPWebApplication "http://example.com"
  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 http://example1.com -Path C:\Backup\example1.bak

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

Restore-SPSite http://example2.com -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:

image

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

image

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.

Thursday, November 14, 2013

Problem with SPContextPageInfo.IsWebWelcomePage on imported Sharepoint sites

I wrote several articles already about problems with SPExport/SPImport API of Sharepoint (see Using SPExport/SPImport for copying content with managed metadata and Retain object identity during export and import of subsites in Sharepoint into different site collection hierarchy location). In this post I will describe one more interesting problem which you may face with when work with mentioned API. Starting from Sharepoint 2010 there is SPContextPageInfo.IsWebWelcomePage global property, which returns true when current page is site’s welcome page. It is still not so widely used: more often developers continue to use PublishingWeb.DefaultPage and compare it with current page’s url or id, which was available from Sharepoint 2007. However the last method requires opening of PublishingWeb first, which may impact performance depending on the context where you use it, so in general I would prefer to use SPContextPageInfo.IsWebWelcomePage now.

It is not so well-known fact that OTB Sharepoint AspMenu also uses this property for determining selected menu item (it adds “selected” css class to the menu item which corresponds to the current page as you probably know). So if this property won’t work as expected, your navigation may be broken. The problem is that if site was imported from backup of other site using SPExport/SPImport API, SPContextPageInfo.IsWebWelcomePage may always return false.

This problem comes from Sharepoint internals. In order to avoid it you may use the following workaround: create custom http module which will check whether current request goes to the site’s welcome page and if yes, set it to true. Before you will use it, pay attention to the several important things:

  1. http modules are executed for all requests, including images, css and js. Keep it in mind, because it may significantly affect performance;
  2. it is not possible to change SPContextPageInfo.IsWebWelcomePage using standard object model, because it has internal setter, so reflection will be needed;
  3. the following code will work for English publishing sites only and only when they have welcome page always set to default.aspx. In your case neither of these can be true: in this case you should modify the code for your needs. The more proper way to define welcome page would be to use method with PublishingWeb.DefaultPage, but remember that it should have very good performance. So at least add caching if you will do it this way.

Here is the code of http module:

   1:  public class FixNavigationModule : IHttpModule
   2:  {
   3:      private HttpApplication app;
   4:   
   5:      public void Dispose()
   6:      {
   7:      }
   8:   
   9:      public void Init(HttpApplication application)
  10:      {
  11:              this.app = application;
  12:              this.app.PreRequestHandlerExecute += preRequestHandlerExecute;
  13:      }
  14:   
  15:      private void preRequestHandlerExecute(object sender, EventArgs eventArgs)
  16:      {
  17:          try
  18:          {
  19:              string url = this.app.Request.Url.AbsoluteUri.ToLower();
  20:              // module is executed for all file types including css and js. In order to keep good performance
  21:              // we should check only those requests which are coming to aspx pages
  22:              if (!url.Contains(".aspx"))
  23:              {
  24:                  return;
  25:              }
  26:   
  27:              if (url.Contains("/pages/default.aspx") && !SPContext.Current.ContextPageInfo.IsWebWelcomePage)
  28:              {
  29:                  // setter for SPContextPageInfo.IsWebWelcomePage is internal, so we have to use reflection here
  30:                  this.setIsWebWelcomePage(true);
  31:              }
  32:          }
  33:          catch (Exception x)
  34:          {
  35:                          // log
  36:          }
  37:      }
  38:   
  39:      private void setIsWebWelcomePage(bool isWebWelcomePage)
  40:      {
  41:          var obj = SPContext.Current.ContextPageInfo;
  42:   
  43:          BindingFlags bf = 0 | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
  44:          MethodInfo mi = obj.GetType().FindMembers(MemberTypes.Method, bf, Type.FilterName, "set_IsWebWelcomePage")[0] as MethodInfo;
  45:          mi.Invoke(obj, new object[]{ isWebWelcomePage });
  46:      }
  47:  }

After that add this http module to your web.config file on all WFE servers:

   1:  <add name="FixNavigationModule" type="MyNamespace.FixNavigationModule, MyAssembly" />

It will fix problem with SPContextPageInfo.IsWebWelcomePage property on imported site. Hope that it will help someone. If you know better solution please share it in comments.

Saturday, November 9, 2013

Problem with missing catalog connections when use customized search result source in Sharepoint 2013

Customized search result sources (Site settings > Search result sources) are one of the ways to customize search results shown in search web parts on your site. It allows to add additional search conditions which will be added to the search keywords entered by user in search textbox. Another way to do it is to change query in Content by search web parts’ properties one by one, but in this case changes will affect only single web part. While if you use customized search result source for site collection, all search web parts will use it by default (it will still possible to override search result source for particular web parts).

When create customized search result source, MS recommends to copy one of existing OTB search result sources and then change its properties:

image

Note that “Local SharePoint Results” source is set as default.

Here is the code which shows how we can copy “Local SharePoint Results” source programmatically to “Local SharePoint Results (customized)” and change its query text:

   1: var searchAppProxy = getSearchServiceApplicationProxy(site);
   2: var federationManager = new FederationManager(searchAppProxy);
   3: var searchOwner = new SearchObjectOwner(SearchObjectLevel.SPSite, site.RootWeb);
   4: var resultSource = federationManager.CreateSource(searchOwner);
   5: resultSource.Name = "Local SharePoint Results (customized)";
   6: resultSource.ProviderId = federationManager.ListProviders()["Local SharePoint Provider"].Id;
   7: resultSource.CreateQueryTransform(new QueryTransformProperties(), query);
   8: resultSource.Commit();
   9: federationManager.UpdateDefaultSource(resultSource.Id, searchOwner);

The method getSearchServiceApplicationProxy(), used in the example above is the following:

   1: private SearchServiceApplicationProxy getSearchServiceApplicationProxy(SPSite site)
   2: {
   3:     var serviceContext = SPServiceContext.GetContext(site);
   4:     if (serviceContext == null)
   5:     {
   6:         throw
   7: new Exception("Can't get search service application proxy: serviceContext is null");
   8:     }
   9:  
  10:     var searchAppProxy =
  11:         serviceContext.GetDefaultProxy(typeof(SearchServiceApplicationProxy))
  12:             as SearchServiceApplicationProxy;
  13:     if (searchAppProxy == null)
  14:     {
  15:         throw
  16: new Exception("Can't get search service application proxy: searchAppProxy is null");
  17:     }
  18:     return searchAppProxy;
  19: }

Note line 9 from the first example above where we call federationManager.UpdateDefaultSource() method. It sets customized search result source as default, which means all search web parts on the site will use it by default. After this search result sources will look like this in our site collection:

image

Everything is fine and our search web parts started to use new customized search result source. The problems begin when we will try to connect to catalog for using cross site publishing on our site collection.

Let’s assume that we have another site collection with News sub site and we want to publish Pages doclib on this sub site as catalog so content from this doclib will be available on other site collections and web applications. After we published it as catalog and made full crawl of the site which contains this doclib, appropriate catalog connection should appear in Site settings > Manage catalog connections on other site collections. However if we will go in this page on our site collection with customized default result source, there won’t be any connection available:

image

I.e. there are no catalog connections visible at all (if you have several sources published as catalog, no one will be shown). What is the problem?

First of all if you will create new OTB site collection and check catalog connections from there, you will find them:

image

So my hypothesis was that catalog connections disappear when we copy OTB result source to customized. But as it turned out the reason is not exactly in the copying. Catalog connections disappear when we set customized result source as default. They disappear not only from UI, it is also not possible to get them programmatically after that. The following code which gets catalog connection settings by name will return null:

   1: private CatalogConnectionSettings getCatalogByName(SPSite site, string catalogName)
   2: {
   3:     int num;
   4:     var catalogs = PublishingCatalogUtility.GetPublishingCatalogs(site, 0, 10,
   5:         string.Empty, out num);
   6:     if (catalogs == null)
   7:     {
   8:         return null;
   9:     }
  10:     foreach (var c in catalogs)
  11:     {
  12:         if (c.CatalogName == catalogName)
  13:         {
  14:             return c;
  15:         }
  16:     }
  17:     return null;
  18: }
 
How to avoid this problem? Well solution is quite simple: before to connect to catalog mark OTB “Local SharePoint Results” source as default:
image
After this catalog connections will appear in Site settings > Manage catalog connections and you will be able to connect to them (they may appear after couple of seconds, not immediately). Connect and then set customized “Local SharePoint Results (customized)” source back as default. As result you will have working catalog connection and customized search result source. Hope that this information will help you.

Wednesday, November 6, 2013

Unsecure content inside iframe page

Suppose that you have site A and site B. Site A is accessible through https, and you want to integrate site B to it via iframe. In order to avoid “Only secure content is displayed” message and blocking site B in the browser, site B should be accessible also via https:

   1: <iframe src="https://siteB.com" style="height: 200px; width: 100%"></iframe>

But it is not enough. If on site B page, which is shown in iframe, there will be any http links (e.g. js files included via http), you will again get “Only secure content is displayed” message, although this time site B won’t be blocked. Instead part of it will be shown in iframe immediately, but full page will be shown only when user will click Show all content button:

image

If you want to avoid this message completely, both site B should be access through https and content inside site B should have only https links.