Recycle Bin
With v2 of Sharepoint, restoring deleted documents was a much sought after elixir. And to that effect were made a number of attempts at coaxing the product to allow us to retain copies of the deleted items. Once such simplistic approach was documented and well presented by Maxin V Karpov and Eric Schoonover in the February 2005 issue of MSDN. The online version can be found here.
Recently me as well as a couple of my colleagues received calls from customers, seeking clarifications on how to implement the sample presented in the article. This called for a defininte reimplementation, and what an experience it was! Here are the code samples from that implementation.
I will start with the classes that will help implement the Upload and Delete Event Handlers. This is spread over two classes: BaseEventSink(which is an extended version of the file that gets downloaded from the event handler tool kit) and the EventSinkData class
/*=====================================================================
File: BaseEventSink.cs
Summary: Base class for cached event sinks.
---------------------------------------------------------------------
using System;
using System.Xml;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.IO;
using System.Diagnostics;
using System.Text;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.SharePoint;
using RecycleBin.SharePoint.Configuration;
using Microsoft.SharePoint.Administration;
namespace RecycleBin.SharePoint.EventSink
{
/// <summary>
/// Base class for cached event sinks.
/// Handles impersonation and caching event sink classes to handle multiple events on
/// the same list.
/// </summary>
public class BaseEventSink : Microsoft.SharePoint.IListEventSink
{
public BaseEventSink()
{
}
/// <summary>
/// Implementation of the OnEvent method. Windows SharePoint Services will
/// call this method when an event occurs.
/// </summary>
/// <param name="listEvent">The SPListEvent object that describes the event which occured.</param>
public virtual void OnEvent(Microsoft.SharePoint.SPListEvent listEvent)
{
WriteToFile("Event has been fired, and entered the custom event handler.");
WriteToFile("List Event dump:- " + listEvent.ToString());
if (listEvent == null)
{
throw new ArgumentNullException("listEvent", "list event object cannot be null");
}
WriteToFile("Getting the cachedEventSink");
BaseEventSink sink = GetCachedEventSink(listEvent);
WriteToFile("Got the Cached Event Sink");
WriteToFile("Starting the impersonation.");
WindowsImpersonationContext wip = null;
//Ensure each sink instance handles only one event at a time.
lock(sink)
{
try
{
//Make sure the sink class has the current SPListEvent object
sink.EventInfo = listEvent;
//Impersonate the appropriate user
WriteToFile("Impersonating the administrator");
WindowsIdentity id = sink.HandlerIdentity;
if (id != null)
wip = id.Impersonate();
WriteToFile("Impersonation successful");
WriteToFile("Transferring the control to the actual event handler.");
sink.HandleEvent();
}
finally
{
//Cleanup
if (wip != null)
wip.Undo();
if (m_web != null)
m_web.Close();
//Null out cached SPWeb and SPList objects so that
//fresh ones are obtained for the next event.
m_web = null;
m_list = null;
}
}
}
/// <summary>
/// HandleEvent is called when an event occurs. Child classes should perform the main
/// event handling actions here.
/// </summary>
protected virtual void HandleEvent()
{
// if (admin.IsCurrentUserGlobalAdmin())
{
WriteToFile("Entered the EventHandler");
WriteToFile("Event fired is of type:- " + EventInfo.Type.ToString());
switch(EventInfo.Type)
{
case SPListEventType.Delete:
WriteToFile("Delete event fired.");
WriteToFile("Transferring to the Delete handler.");
DeleteHandler();
break;
case SPListEventType.Move:
// if(!Include()){break;}
WriteToFile("Move Event fired.");
WriteToFile("Transferring to the Move handler.");
MoveHandler();
break;
case SPListEventType.Update:
case SPListEventType.Insert:
case SPListEventType.Copy:
// if(!Include()){break;}
WriteToFile("Update/Insert/Copy event fired.");
WriteToFile("Transferring to the Update/Insert/Copy handler.");
UpdateInsertCopyHandler();
break;
}
}
}
/// <summary>
/// The SPListEvent object that describes the current event.
/// </summary>
protected virtual SPListEvent EventInfo
{
get
{
return m_ListEvent;
}
set
{
m_ListEvent = value;
m_web = null;
m_list = null;
m_fileUrl = null;
m_sinkData = null;
}
}
/// <summary>
/// The SPWeb object for the web site that contains the list
/// that the event occured on.
/// </summary>
protected virtual SPWeb EventWeb
{
get
{
if (m_web == null)
{
WriteToFile("Getting the web context.");
m_web = m_ListEvent.Site.OpenWeb();
}
return m_web;
}
}
/// <summary>
/// The SPList object for the list the event occured on.
/// </summary>
protected virtual SPList EventList
{
get
{
if (m_list == null)
{
m_list = EventWeb.Lists[EventInfo.ListID];
}
return m_list;
}
}
/// <summary>
/// The url of the file the event occured on.
/// Equals UrlAfter if UrlAfter is not empty. Otherwise equals
/// UrlBefore.
/// </summary>
protected string EventFileUrl
{
get
{
if (m_fileUrl == null)
{
string webUrl = m_ListEvent.WebUrl == null ? "<NULL>" : m_ListEvent.WebUrl;
string webRelUrl = m_ListEvent.UrlAfter;
if (webRelUrl == null || webRelUrl.Length == 0)
{
webRelUrl = (m_ListEvent.UrlBefore == null || m_ListEvent.UrlBefore.Length == 0 ?
"<NULL>" : m_ListEvent.UrlBefore);
}
m_fileUrl = String.Format("{0}/{1}", webUrl, webRelUrl);
}
return m_fileUrl;
}
}
protected string Data
{
get
{
if (m_sinkData == null)
{
m_sinkData = m_ListEvent.SinkData;
}
return m_sinkData;
}
}
# region Custom Code
/// <summary>
/// Converts XML string retrieved from SinkData property into an object
/// </summary>
protected EventSinkData Configuration
{
get
{
try
{
m_ConfigData = EventSinkData.GetInstance(Data);
return m_ConfigData;
}
catch(Exception ex)
{
PublishException(ex.ToString());
}
return null;
}
}//Configuration
/// <summary>
/// Makes sure that the proper folder structure is in place for copying, moving or inserting a file in to the mirror
/// document library or the Recycle Bin document library.
/// </summary>
/// <param name="web">
/// Web where the folder structure needs to be built or ensured
/// </param>
/// <param name="finalUrl">
/// Url containing the structure that needs to be built, must contain the trailing "/" if it is not a link to a file.
/// If it is a link to a file the file name will be stripped and the folder structure ensured.
/// </param>
/// <returns>
/// File path that has been built
/// </returns>
protected static string EnsureParentFolder(SPWeb web, string finalUrl)
{
finalUrl = web.GetFile(finalUrl).Url;
int x = finalUrl.LastIndexOf("/");
string epf = String.Empty;
if(x <= -1)
return epf;
epf = finalUrl.Substring(0, x);
SPFolder folder = web.GetFolder(epf);
if(folder.Exists)
return epf;
SPFolder curFolder = web.RootFolder;
string [] folders = epf.Split('/');
for(int i = 0; i < folders.Length; i ++)
{
curFolder = curFolder.SubFolders.Add(folders[i]);
}
return epf;
}//EnsureParentFolder
protected static void PublishException(string errors)
{
if(errors.Length > 0)
{
if(!EventLog.SourceExists(SOURCENAME))
EventLog.CreateEventSource(SOURCENAME, LOGNAME);
EventLog el = new EventLog();
el.Source = LOGNAME;
el.WriteEntry(errors, EventLogEntryType.Error);
}
}
# endregion
# region Windows Impersonation Code
/// <summary>
/// The WindowsIdentity class of the identity the Event Sink should impersonate.
/// </summary>
protected virtual WindowsIdentity HandlerIdentity
{
get
{
m_Identity = CreateIdentity(Configuration.UserName,
Configuration.Domain,
Configuration.Password);
return m_Identity;
}
}
/// <summary>
/// Helper method to handle creating a WindowsIdentity object from a username / domain / password.
/// </summary>
/// <param name="User">The username of the account to impersonate.</param>
/// <param name="Domain">The domain of the account to impersonate.</param>
/// <param name="Password">The password of the account to impersonate.</param>
/// <returns></returns>
public static WindowsIdentity CreateIdentity(string User, string Domain, string Password)
{
// The Windows NT user token.
IntPtr tokenHandle = new IntPtr(0);
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_NETWORK_CLEARTEXT = 3;
tokenHandle = IntPtr.Zero;
// Call LogonUser to obtain a handle to an access token.
bool returnValue = LogonUser(User, Domain, Password,
LOGON32_LOGON_NETWORK_CLEARTEXT, LOGON32_PROVIDER_DEFAULT,
ref tokenHandle);
if (false == returnValue)
{
int ret = Marshal.GetLastWin32Error();
throw new Exception("LogonUser failed with error code: " + ret);
}
//The WindowsIdentity class makes a new copy of the token.
//It also handles calling CloseHandle for the copy.
WindowsIdentity id = new WindowsIdentity(tokenHandle);
CloseHandle(tokenHandle);
return id;
}
# endregion
/// <summary>
/// Get the cached event sink object to handle this event.
/// </summary>
/// <returns>The cached IListEventSink object that should handle the current event.</returns>
private BaseEventSink GetCachedEventSink(SPListEvent evt)
{
BaseEventSink sink = null;
//Syncrhonize both reads and writes to the cache.
//Even though the Hashtable class is threadsafe for reads, we don't want to have
//multiple sink instances per list. Otherwise, if many events occur on a list
//right off the bat, you could get many instances that all have to initialize
//themselves.
lock(SinkCache.SyncRoot)
{
sink = SinkCache[evt.ListID] as BaseEventSink;
//The cached sink is only fit if it is the same type and has the same data
//as the current sink.
if (sink == null ||
! sink.GetType().Equals(this.GetType()) ||
evt.SinkData != sink.Data)
{
//Update the cache, throw away the old sink if it exists.
SinkCache[evt.ListID] = this;
sink = this;
}
}
return sink;
}
# region Win32 API calls
[DllImport("advapi32.dll", SetLastError=true)]
private static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
[DllImport("kernel32.dll", CharSet=CharSet.Auto)]
private extern static bool CloseHandle(IntPtr handle);
# endregion
protected SPListEvent m_ListEvent;
private SPWeb m_web;
private SPList m_list;
private string m_fileUrl;
private string m_sinkData;
// Custom variables start
private EventSinkData m_ConfigData;
protected static Hashtable SinkCache = new Hashtable();
private const string SOURCENAME = "Recycle Bin";
private const string LOGNAME = "Recyclebin.EventSink";
private string strMirror = "New";
private string strRecycle = "Recycle Bin";
private WindowsIdentity m_Identity = null;
private SPGlobalAdmin admin = new SPGlobalAdmin();
private WindowsIdentity id;
// private EventSinkData e = new EventSinkData();
// Custom variables end
private void UpdateInsertCopyHandler()
{
WriteToFile("Entered the UpdateInsertCopy handler.");
string fileLocation = EventInfo.UrlAfter;
WriteToFile("Got the before location:- " + fileLocation);
// string newFileLoc = String.Concat(strMirror, "/", fileLocation.Remove(0,5));
string newFileLoc = String.Concat(Configuration.MirrorLib, "/", fileLocation.Remove(0,Configuration.MainLib.Length+1));
WriteToFile("Got the new location:- " + newFileLoc);
SPFile miscFile = EventWeb.GetFile(fileLocation);
try
{
//EnsureParentFolder(EventWeb, newFileLoc);
// miscFile.CopyTo(newFileLoc, true);
// SPFolder newFolder = EventWeb.GetFolder(strMirror);
WriteToFile("Getting the folder context.");
SPFolder newFolder = EventWeb.GetFolder(Configuration.MirrorLib);
WriteToFile("Preparing to copy the file.");
Byte[] byteArray = miscFile.OpenBinary();
WriteToFile("Copying the file.");
newFolder.Files.Add(miscFile.Name,byteArray);
WriteToFile("Copy done.");
}
catch(Exception ex)
{
WriteToFile("Exception occurred:- " + ex.ToString());
PublishException(ex.ToString());
}
}//UpdateInsertCopyHandler
private void DeleteHandler()
{
WriteToFile("Entered Delete handler.");
SPFile delFile = EventWeb.GetFile(String.Concat(Configuration.MirrorLib, "/", EventInfo.UrlBefore.Remove(0,Configuration.MainLib.Length+1)));
WriteToFile("Got the old address. " + String.Concat(Configuration.MirrorLib, "/", EventInfo.UrlBefore.Remove(0,Configuration.MainLib.Length+1)));
string newFileLoc = String.Concat(Configuration.RecycleBinLib, delFile.Url.Remove(0,Configuration.MirrorLib.Length));
WriteToFile("New address:- " + newFileLoc);
try
{
// EnsureParentFolder(EventWeb, newFileLoc);
// delFile.MoveTo(newFileLoc, true);
WriteToFile("Getting the folder context.");
SPFolder newFolder = EventWeb.GetFolder(Configuration.RecycleBinLib);
WriteToFile("Preparing to copy.");
Byte[] byteArray = delFile.OpenBinary();
WriteToFile("Copying the file.");
newFolder.Files.Add(delFile.Name,byteArray,true);
WriteToFile("Copying complete.");
}
catch(Exception ex)
{
WriteToFile("Exception occurred:- " + ex.ToString());
PublishException(ex.ToString());
}
}//DeleteHandler
private void MoveHandler()
{
string mirrorName = strMirror;
// SPFile moveFile = EventWeb.GetFile(String.Concat(mirrorName, "/", EventInfo.UrlBefore.Remove(0,5)));
SPFile moveFile = EventWeb.GetFile(String.Concat(Configuration.MirrorLib, "/", EventInfo.UrlBefore.Remove(0,Configuration.MirrorLib.Length-1)));
// string newFileLoc = String.Concat(mirrorName, "/", EventInfo.UrlAfter);
string newFileLoc = String.Concat(Configuration.MirrorLib, "/", EventInfo.UrlAfter);
try
{
EnsureParentFolder(EventWeb, newFileLoc);
moveFile.MoveTo(newFileLoc, true);
}
catch(Exception ex)
{
PublishException(ex.ToString());
}
}//MoveHandler
// Logging
private static void WriteToFile(String input)
{
try
{
string filePath=@"C:\sharepoint\Log.txt";
FileInfo logFile = new FileInfo(filePath);
if(logFile.Exists)
{
if (logFile.Length >= 100000)
File.Delete(filePath);
}
FileStream fs = new FileStream(filePath,FileMode.OpenOrCreate, FileAccess.ReadWrite);
StreamWriter w = new StreamWriter(fs);
w.BaseStream.Seek(0,SeekOrigin.End);
w.WriteLine(input);
w.WriteLine("--------------------------------------------------------------");
w.Flush();
w.Close();
}
catch(System.Exception ex)
{
PublishException(ex.ToString());
}
}
}
}
Continued in the next one due to the length of the post.........
Comments
- Anonymous
July 03, 2006
It is very useful to me pls send modifications to this. - Anonymous
February 05, 2007
I was ego surfing today and came upon the following links that related to the "Add A Recycle Bin To Windows SharePoint Services For Easy Document Recovery" artcile that I contributed to last year. http://blogs.msdn.com/harsh/archive/2005/10/17/481621.aspxGreat