Friday 16 May 2008

Extending the functionality of the EXPORT Operation of STSADM

With this task it was important to understand the functionality of the current export operation to see whether it was possible to extend it or create a new functionality altogether.
I assumed that the items to be exported were lists, document libraries, blogs, task lists and the like. I tried to imagine having to export my blog from one portal to another.
SharePoint Content Deployment and Migration API
WSS and MOSS use this API at various different places:
* Content Deployment
* STSADM -o export and import
* Copy/Move operations in Site Manager
* Variations
* MCMS 2002 database migration to MOSS 2007
yy

The Content Deployment and Migration API provides the following features:

  • export an entire site collection
  • export a specific site inside a site collection including or excluding content in subsites
  • export a list or document libraries or even of a folder inside a document library
  • export a single list items or documents from a document library
  • export dependent objects (like images referenced by a page) by following links
  • generate as a compressed export file or in uncompressed format
  • allow export with a define a maximum size for the generated compressed file (multiple export files will be created if required)
  • allow incremental export of items based on a given change token. This will export all items that have been created, changed or deleted after the timestamp in the change token.
  • import the exported content with or without identiy preservation (means items will keep their GUID or not)
  • import the exported content under the same or a differnt parent in the destination database
  • do link fixup during import.

In particular we are interested in the use of a change token to export changes only made since the last export.

Programming Reference

Assembly: Microsoft.SharePoint.dll
Namespace: Microsoft.SharePoint.Deployment

Important Objects in the API:

  • SPExport - controls the Export process
  • SPExportSettings - used to configure the export process
  • SPExportObject - defines which objects need to be exported
  • SPImport - controls the import process
  • SPImportSettings - used to configure the import process

The SPExport Class supports exporting specified content from a source Windows SharePoint Services site collection to a cabinet (.cab) file in XML format. This class participates with other classes in the Deployment namespace to support importing, exporting, publishing, and migrating Windows SharePoint content, as well as supporting backup and restore capabilities.

** You can initiate an export operation by first initializing an instance of the Microsoft.SharePoint.Deployment.SPExportSettings class with the required export settings, and then passing the SPExportSettings object to the constructor of SPExport class; you then call the SPExport.Run method.

The following code example demonstrates how to perform an incremental export. Notice that the code sets the ExportMethod property to ExportChanges and then provides a change token.

settings.ExportMethod = SPExportMethodType.ExportChanges;
settings.ExportChangeToken = "1;1;87a71761-2987-48eb-9d29-48428270e01;632937036861200000;5512";

Reference: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.deployment.aspx

To export an entire site collection:

SPExportSettings settings = new SPExportSettings();
settings.SiteUrl = "http://localhost:2000";
settings.ExportMethod = SPExportMethodType.ExportAll;
settings.FileLocation = @"c:\export";
settings.FileCompression = false;
settings.CommandLineVerbose = true;
SPExport export = new SPExport(settings);
export.Run();

where a SPExportSettings object is created to define the general configuration settings for the export to be performed. As we did not select a specific object to export the configured site collection will be selected for export. Then the SPExport object is created based on the configured settings and the export is started by calling the Run method.

The settings being used in the code above:

  • SiteUrl - this property defines which site collection the export should use. All objects being exported always have to be in the same site collection. The Content Deployment and Migration API cannot access items in different site collections in a single operation.
  • ExportMethod - this property allows to define whether to perform an incremental export (value = ExportChanges) or everything (value = ExportAll). Be aware that ExportChanges would require to provide an Export Change Token in a separate property.
  • FileLocation - this property defines where to store the exported content. The value should point to be an empty directory. If the directory does not exist it will be created during export. If file compression is being used, then only the compressed files will be stored on this location. The uncompressed files will be stored in the directory identified by the value of the system wide TMP environment variable. So you need to ensure that the directory the TMP environment variable points to also needs to have sufficient space available.
  • FileCompression - this property defines whether the content should be compressed into a CAB file. If you need to archive the exported content or need to transfer it to a different machine you should choose to compress. If you only export the content to import it afterwards using code on the same machine and don't need to archive (e.g. a copy or move operation) then you should decide to disable the compression as this is significantly quicker.
  • CommandVerbose - this parameter allows to control if the API should provide some verbose output. If you have ever seen the generated output when running STSADM -o export: this is exactly the flag the generates this output. If the value is false no output is generated.

To export specific items like lists, document libraries, list items or documents from a document library:

SPSite site = new SPSite("http://localhost:2000");
SPWeb web = site.OpenWeb("/SomeWeb");
SPList list = web.Lists["MyList"];
SPListItem listItem = list.Items[0]; // select the first list item

SPExportObject exportObject = new SPExportObject();
exportObject.Id = list.ID;
exportObject.Type = SPDeploymentObjectType.List;

SPExportObject exportObject = new SPExportObject();
exportObject.Id = listItem.UniqueId;
exportObject.Type = SPDeploymentObjectType.ListItem;

where the object type will change as per the type you are trying to export.

To export incremental items
If incremental export should be done it is required to save the Change Token of the last full or incremental export. This change token needs then be provided in the ExportSettings to allow the Content Deployment and Migration API to determine which items have changed as follows:
SPExportSettings settings = new SPExportSettings();
...
SPExport export = new SPExport(settings);
export.Run();
string ChangeToken = settings.CurrentChangeToken;
CurrentChangeToken is a read-only parameter populated during export. It contains the Change Token right after the export. So doing an incremental export providing this change token in the future will export all items that have been created, changed or deleted in the configured scope after the change token was generated.
The following code implements an export
SPExportSettings settings = new SPExportSettings();
settings.ExportMethod = SPExportMethodType.ExportChanges;
settings.ExportChangeToken = oldChangeToken;
...
  • ExportMethod - this property allows to define whether to perform an incremental export (value = ExportChanges) or everything (value = ExportAll).
  • ExportChangeToken - this property defines which items to export when using incremental deployment. the incremental export will only export items that have been created, changed or deleted in the configured scope after the change token was generated.
Anothwer way to extend the functionality of stsadm is to create a class that implements the ISPStsadCommand
although i had trouble finding a command to export only items changed form the last export!
References:

http://technet.microsoft.com/en-us/library/cc262759.aspx an explanation of the stsadm operation

http://technet.microsoft.com/en-us/library/cc263384.aspx stsadm operations out of the box

http://blogs.technet.com/stefan_gossner/archive/2007/08/29/deep-dive-into-the-sharepoint-content-deployment-and-migration-api-part-2.aspx

http://www.aisto.com/roeder/dotnet/ This is one magical tool I would recommend everyone use. Reflector is the class browser, explorer, analyzer and documentation viewer for .NET. Reflector allows to easily view, navigate, search, decompile and analyze .NET assemblies in C#, Visual Basic and IL. just install it and piiunt it to stsadm or any program and it will break down every single class, attribute, method and operations for the program!!!

http://www.andrewconnell.com/blog/articles/MossStsadmWcmCommands.aspx

http://msdn.microsoft.com/en-us/library/aa367988.aspx Command Line parameters

http://msdn.microsoft.com/en-us/library/aa979099.aspx The sharepoint Deployment object model

Tuesday 13 May 2008

Adding a custom disclaimer page to a Moss 2007 site

I had a task for a client of adding a compulsary terms and conditions page to a portal site. The requirement was for each user to be presented with this aspx page when they first log on to the portal, and then they have to either accept of reject the disclaimer. It they reject it the browser is closed. If they accept the value was to be saved somewhere so the data could be accessed in case of a query. This is especially needed when dealing with financial or legally regulated clients. There needed to be some way of catching every call to the portal and a check to see whether the user trying to log on has accepted the disclaimer or not then they would be redirected correctly.

My plan was first to use a list to store the accepted information but this would need to be more secure and uneditable so I opted for adding a custom attribute to the user profile for each user.
I would incorporate the disclaimer aspx page into the site by making it a site collection feature. I used visual studio to create my solution.

I used a codeplex template and modofied it to install the disclaimer page to my portal.

http://www.codeplex.com/stsdev

STSDEV is a proof-of-concept utility application which demonstrates how to generate Visual Studio project files and solution files to facilitate the development and deployment of templates and components for the SharePoint 2007 platform including Windows SharePoint Services 3.0 (WSS) and Microsoft Office SharePoint Server 2007 (MOSS). Note that the current version of the stsdev utility only supports creating projects with the C# programming language.

I realised I needed an http handler to intercept every request for every url in the portal, this would perform the functionality of checking for the required value in the user profile. I wrote a class which intercepts the HTTP request pipeline before the default landing page loads,and this class performs a check on a custom profile attribute of our sharepoint user session. I used a Boolean flag to indicate if the user has accepted terms and conditions for site usage. If not, the user is redirected to a custom aspx page from th Http module code, which presents the disclaimer text and a couple of buttons to handle whether the user accepts it or not.

If the user clicks ok, I simply made the call to the user profile properties store and it updates the Boolean field accordingly, and then redirect the browser to the default landing page. The next time a user session is launched for accessing the intranet, the Http module will again intercept the request, the flag will be true, and we do nothing (the request continues on to the default landing page).

To accomplish this required some familiarity with Http modules in ASP.net in general, and also usage of the SharePoint object model to access the user properties. I would recommend starting out with a simple asp.net application with a couple of pages, and write a small class for the Http module which performs a redirect based on a Boolean flag held in a custom user profile attribute, created in the SharedServices admin console, and call the userprofile object to check for the property value in code. I used the UserProfileManager class, which references the Microsoft.Office.Server dll in the project, you can then get user profile info along the following lines...

using Microsoft.Office.Server.UserProfiles

//Get the current user
SPWeb litwareWeb = SPControl.GetContextWeb(Context);SPUser currentUser = litwareWeb.CurrentUser;//Create a new UserProfileManagerUserProfileManager pManager = new UserProfileManager();//Get the User Profile for the current userUserProfile uProfile = pManager.GetUserProfile(currentUser.LoginName);
And then access the properties within the UserProfile class using the collection reference (uProfile[‘CustomAttribute’].value or something like that...)

Using the template you will need to modify the ProjectFolder variable in the .targets file before you build the project –
E.G. "C:\Documents and Settings\tenille\My Documents\Visual Studio 2005\Projects\DisclaimerPage"

Code Examples:

The User Profile Check Class :

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.Office.Server.UserProfiles;
using System.Web.UI;
using System.Configuration;
namespace GLGCustomPages
{
public class UserProfileCheck : IHttpModule
{
public UserProfileCheck()
{ }
#region IHttpModule Members
public void Dispose()
{ }
public void Init(HttpApplication app)
{
// Hook into Release Request
app.ReleaseRequestState += new EventHandler(ReleaseRequest_Handler);
}
///
///

///
///
void ReleaseRequest_Handler(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
//HttpContext context = app.Context;
//context.User.Identity.Name;
if (!app.Request.RawUrl.Contains("Disclaimer.aspx"))
{
string originatingUrl = app.Request.Url.ToString();
using (SPSite site = new SPSite(SPContext.Current.Web.Url))
{
SPWeb currentWeb = site.RootWeb;
SPUser currentUser = currentWeb.CurrentUser;
//Create a new UserProfileManager
UserProfileManager pManager = new UserProfileManager();
//Get the User Profile for the current user
try
{
UserProfile uProfile = pManager.GetUserProfile(currentUser.LoginName);
if (uProfile["DisclaimerAcceptance"].Value.Equals(false))
{
app.Response.StatusCode = 301;
app.Response.AddHeader("Location", "http://" + app.Request.ServerVariables["HTTP_HOST"].ToString() + "/Pages/Disclaimer.aspx?Source="+ app.Server.UrlEncode(originatingUrl));
app.Response.End();
}
}
catch (Exception ex)
{
}
}
}
}
#endregion
}
}

The Disclaimer Page Class File:


using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.Administration;
namespace GLGCustomPages
{
public class DisclaimerPage : Page
{
//add in any page control references here
protected Button AcceptDisclaimer;
protected Button DeclineDisclaimer;
protected UserProfileManager pManager;
protected override void OnInit(EventArgs e)
{
AcceptDisclaimer.Click += new EventHandler(AcceptDisclaimer_Click);
DeclineDisclaimer.Click += new EventHandler(DeclineDisclaimer_Click);
base.OnInit(e);
}
protected void AcceptDisclaimer_Click(object sender, EventArgs e)
{
using (SPSite site = new SPSite(SPContext.Current.Web.Url))
{
SPWeb currentWeb = site.RootWeb;
SPUser currentUser = currentWeb.CurrentUser;
//Create a new UserProfileManager
UserProfileManager pManager = new UserProfileManager();
//Get the User Profile for the current user
UserProfile uProfile = pManager.GetUserProfile(currentUser.LoginName);
uProfile["DisclaimerAcceptance"].Value = true;
uProfile.Commit();
Response.Redirect(Request.QueryString["Source"].ToString());
}
//update the user profile attribute and then redirect to the default page
}
protected void DeclineDisclaimer_Click(object sender, EventArgs e)
{
//Response.Write("");
//close the browser event initiated from the client
}
}
}

Note: A snk file must be created in order for it to work.

The elements.xml and feature.xml files must be modofied correctly to build the project.

When deploying the feature, it must be activated in site collection settings and the user profile attribute must be made editable for users who are logging in for the first time. There was a lot of work involved and many errors later I managed to get it working. Any comments or modifications are most welcome.

Ghosting and Unghosting in SharePoint

Sharepoint 2007, built in ASP.NET 2.0, doesn't do that. That is a good thing, because all your customization doesn't get litterred all over the place as multiple files. Your customization sits in the database.

You see, ASP.NET 2.0 has a new concept called Virtual Page Parser. That allows the Sharepoint runtime to query for the aspx as a combination of what is on the file system, and what is in the database. That combination, then appears as a single aspx, and is then run through the pipe of ASP.NET - hence ending up as a class somewhere deep inside temporary asp.net files.
The huge advantage of this approach is of course the ability to revert back to what used to be your page before you completely mucked it up.

In SharePoint Portal Server 2003 (and WSS 2.0) it was a simple task to discover the ghosted status of pages in your SharePoint environment. Simply run "select * from docs where content is not null and leafname like '*.aspx' and listid is null". And to reghost them, all you have to do is set the content to null again.

In Microsoft Office SharePoint Server 2007 (and WSS 3.0), things have become substantially more complicated. In order to alleviate the performance impact of customizing (unghosting) pages, and to alleviate the design burden of modifying SharePoint's look and feel, Microsoft uses Master Pages and Page Layouts to handle the design of a single page in SharePoint. Unfortunately, this means that the content field is almost never null for a page in the database. So how do you figure out if a page is customized?

1. The SPFile object has a property called CustomizedPageStatus, which maps to an enumeration. The values of the enumeration are Customized, Uncustomized and None. (I really wonder what "None" means in this context. It seems to me customized and uncustomized are mutually exclusive, as well as comprehensive.) In the case of a Publishing Page object, i.e. an ASPX page in a "Pages" library with the publishing feature activated, this enumeration always == Customized. I check it like this:
foreach (SPListItem item in List.Items)
{
if (PublishingPage.IsPublishingPage(item))
{
NumberOfPages++;
PublishingPage pPage = PublishingPage.GetPublishingPage(item);
if (pPage.ListItem.File.CustomizedPageStatus == SPCustomizedPageStatus.Customized)
{
Console.WriteLine(pPage.Url + " is customized.");
}
}
}

2. The second way is to use PublishingPage.IsDisconnected. This method works much better than the first I mentioned, except in one (admittedly extreme) circumstance. This circumstance is when you have a publishing page object which does not have any assigned page layout. (Even customized (unghosted) pages have a page layout to fall back on.) The only time I have seen this circumstance is when migrating a SharePoint Portal Server 2003 portal, with a custom site definition for areas, and having a "Page Template Upgrade Definition" file fail. The upgrade will still work (no errors, no warnings), but the pages created have no page layout and are "broken".

3. Use SharePoint designer, the easiest way! SharePoint Designer seems to infallibly detect the customization status for any page. The trouble here of course, is that SharePoint Designer can not be put into a script like the first two methods, there fore limiting optimization.

Zootmastaflex

Zootmastaflex
The Queen of RockStars!