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.........