HiddenNetwork.com Banner
Showing posts with label Event Handlers. Show all posts
Showing posts with label Event Handlers. Show all posts

Tuesday, April 22, 2008

Event handlers configuration settings best practices

Where to place configuration settings for event handlers?
I have thought about it and came up with a few options:

Place config information in the event handler registration info
Every time you register an event handler with a list you can specify data specifically for that registration. This can be done with a feature, or using code (see my utility pack for an easy way to do that)
PRO:

  1. Can be deployed using a feature. We can have different config per list.
CON:
  1. We only have a seperate config per list (no one place to manage all lists).
  2. Limited to 256 characters.
  3. You will need to run code to change the configuration.

Place config information in the web.config <appSettings> section
I was surprised to find that the event handlers get access to the web.config as if they were web applications. Cool!
PRO:

  1. One place to manage per web application. can be deployed using a web application feature and some custom code (http://blog.tedpattison.net/Lists/Posts/Post.aspx?ID=4)
CON:
  1. Hard to deploy every time you create a new web application in the farm. This can be resolved by creating a feature and stapling it at the farm level (scope is webapplication) that modifies the web.config to include the section you want, and setting some defaults - so that the event handler finds what it is looking for when a new web application is created.

Place config information in a settings file on the file system
PRO:

  1. One place for the enitre farm, can be deployed using a solution package
CON:
  1. Location is hard coded in the event handler (unless we use this approach mixed with option one!).
  2. Folder must allow users to read, or impersonation will be required.

Place config information in a settings file in the GAC, next to the assembly
I used this a few times. You can put a file right next to the dll, and use a helper class in the dll to deserialize it and read the values.
PRO:

  1. one place for the enitre farm, no hard coding of location.
  2. Use Mide Woodring's AssemblySettings class to read from it. can probably be deployed as a farm feature
CON:
  1. Every time a new dll version is entered, the config will be deleted and will have to be added again.

Place config information in a document library\list in the root sharepoint site of a site collection or in the central administration site or the shared services site for that web application
PRO:

  1. Deployable as a feature at the site collection level
CON:
  1. still requires custom code to get the config.
  2. performance hit on reading the configuration (can be minimized with caching).
  3. (if root site collection option) Only supports site collection level configuration.

Special thanks to Reza Alirezaei who pointed out some spelling mistakes and commented on the web.config section.

Wednesday, February 06, 2008

Sample event handler to set a field as a pr imary key (enforce no duplicates)

Got this as a request from a reader- how to prevent users from adding items with same titles as ones that already exist in the list.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace SPSTIPS.SharePoint.EventHandlers
{
    public class TitlePrimaryEventHandler:SPItemEventReceiver
    {
        const string TITLE_QUERY = @"<Query><Where><Eq><FieldRef Name=""Title"" /><Value Type=""Text"">{0}</Value></Eq></Where></Query>";
        public override void ItemAdding(SPItemEventProperties properties)
        {
            if (properties.AfterProperties["Title"] != null)
            {
                //get the title of the new item
                string currentTitle = properties.AfterProperties["Title"].ToString();
                //get the web site object
                using (SPWeb web = properties.OpenWeb())
                {
                    //get the current list
                    SPList list = web.Lists[properties.ListId];
                    //query the list to check if there are items with the same title
                    SPQuery q = new SPQuery();
                    q.Query = string.Format(TITLE_QUERY, currentTitle);
                    SPListItemCollection itemsWithSameTitle = list.GetItems(q);
                    //if there are items, cancel the add, and show an error to the user.
                    if (itemsWithSameTitle.Count > 0)
                    {
                        properties.Cancel = true;
                        properties.ErrorMessage = "There is already an item with the title \"" + currentTitle + "\ in this list".";
                    }
                }
            }
        }
    }
}

Thursday, November 29, 2007

Why event handlers are triggered twice?

A lot of people call me Mr Event Handlers (ok, that is not 100% correct. Currently I am the only one calling me that, but never mind), but today's article comes from a Friend who we will refer to as Antin.
Antin did some research on why event handlers behaved strangely in publishing sites and emailed me the results. He logged every event that happens when you check-in a file in a publishing site, and logged the thread of each call, along with the time, so he figured out the sequence.

I think his results are important for all of us writing event handlers - we need to know what is happening when. If you do find this helpful, post a "thank you antin" comment on this post, and I will make sure he know about it (enough encouragment and he may start his own blog, the lazy bugger).

The test results:
(In brackets will be thread number, then list item is old/new, then before properties is old/new, then after values is old/new) Change AND Check-in:
1. Updating (10; old; old; old)
2. Updating (10; new; new; new)
3. CheckingIn (10; new; new; new)
4. CheckedIn (15; new; new; new)
5. Updated (11; new; new; new)
6. Updated (1; new; old; old;)

Change AND Save:
1. Updating (10; old; old; old)
2. Updated (12;new; old; old)

Check-in ONLY
1. CheckingIn (9; new; new; new)
2. CheckedIn (11; new; new; new)
3. Updated (13; new; new; new)
4. Updated (10; new; new; new)

Publish ONLY
1. Updating (1; new; new; new)
2. Updated (13; new; new; new)

If you look at what happens for the save only and the check in only and the combination of new/old values it looks like it working like this:
1. ItemUpdating - from the Save event
2. ItemUpdating - from the CheckIn event (which should fire after the Updated from the Save)
3. ItemCheckingIn
4. ItemCheckedIn
5. ItemUpdated - from the CheckIn event
6. ItemUpdated - from the Save event

Tuesday, June 26, 2007

Update to ItemAdding fiasco

This is to let you know that I updated my posts about the event handlers and ItemAdding, after I did some more research and found the correct way to get and set item's properties during ItemAdding. I swear this method did not work during Beta2 when I posted my old posts, and I swear that contacts in Microsoft did tell me it was by design and not possible (I have the emails to show that).
However, it turned out that it is possible, and easy, and I posted an update and a code sample in the old posts:
Bad news - synchronous list events bug (or missing feature)
Synchronous Add List event (ItemAdding) will not give access to item properties

Thursday, March 22, 2007

Sample Event Handler to set Permissions

This is something I have been seeing the need for in the forums for quite some time, and today I foudn an hour to whip this together. A request is often made to have an event handler that automagically sets the permissions on a list item so that only the author of the item can edit it, while other people with contributor permissions on the library can only read it.
Sometimes the logic is more complex, but I think this sample code should be enough to get anyone started!

Please note that it is a sample code and should not be used in production environment without some major changes. For example - getting the user name and password of a user with elevated permissions on the list should be from an encrypted config file. Another example for a change is changing the logging to use a trace log.
All that aside, this is still a good working example that shows some interesting things:

  • How to get a list item in the event handler
  • How to impersonate another user totaly (not using RunWithElevatedPrivilages)
  • How to change an item's permissions
  • How to create a new permission role in a web site
  • How to check if a role exists or not (nice trick there - can you spot it?)

Before the code, let me just thank Ivan Wilson who suggested that I tackle this problem, and co-workers Douglas Leung and Elizabeth Dionisio who sent me the code we wrote together a month back when I tought them to write event handlers. This code is much different than that one, but the base is the same.
I hope this helps someone!

using System;
using System.Globalization;
using System.ComponentModel;
using System.IO;
using System.Data;
using System.Text;
using System.Xml;
using System.Collections;
using System.Configuration;
using System.Diagnostics;
using System.Web;
using System.Security;
using System.Security.Policy;
using System.Security.Principal;
using System.Security.Permissions;
using System.Runtime.InteropServices;
using Microsoft.SharePoint;
namespace SharePointTips.SharePoint.Samples.EventHandlers
{
    
/// <summary>
    /// This is the event receiver that traps the item added event of the sharepoint list it is attached to.
    /// </summary>
    class ListItemSecuritySetter:SPItemEventReceiver
    {
        #region constants
        
/// <summary>
        /// defines the permission set for editors. 
        /// </summary>
        const SPBasePermissions c_EditorPermissions = SPBasePermissions.EditListItems | SPBasePermissions.ViewListItems;
        
/// <summary>
        /// defines the permission set for readers
        /// </summary>
        const SPBasePermissions c_ReaderPermissions = SPBasePermissions.ViewListItems;
        
/// <summary>
        /// The name of the role that will be created for the author
        /// </summary>
        const string c_AuthorRoleName = "Item Author and Editor";
        
/// <summary>
        /// The name of the role that will be created for the reader
        /// </summary>
        const string c_ReaderRoleName = "Item Reader";
        
/// <summary>
        /// Debug mode writes almost every action to the file log. In production switch this to false. Recommend changing that to read from a configuration file
        /// </summary>
        const bool DEBUG_MODE = true;
        
/// <summary>
        /// The page where the log file will be created. Recommend changing that to read from a configuration file
        /// </summary>
        const string c_logFileFolder = @"c:\temp";        
        #endregion

        #region
 class properties
        
/// <summary>
        /// Returns the name of the log file to be used (file name only - not path)
        /// The string returned will contain {0} and {1} that should be replaced with list name and site name.
        /// </summary>
        private string LogFileName
        {
            
get
            {
                
return "ListItemSecuritySetter-{0}-{1}-" + DateTime.Now.Year.ToString() + DateTime.Now.Month.ToString() + DateTime.Now.Day.ToString() + ".htm";
            }
        }

        #endregion

        #region
 local variables

        
HTMLFileLogging log;

        #endregion

        #region
 event handler event trapping
        
/// <summary>
        /// The sharepoint event for ItemAdded
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAdded(SPItemEventProperties properties)
        {
            
//run the default event handlers on the item
            base.ItemAdded(properties);


            
//create the log handler object
            log = new HTMLFileLogging(c_logFileFolder, string.Format(LogFileName,properties.ListTitle,properties.OpenWeb().Title) , truefalse);
            
this.WriteToLog("ItemAdded was triggered",true);
            
//if in debug mode, write all the properties in the item to the log
            if(DEBUG_MODE)
                WriteItemPropertiesToLog(properties.ListItem);
            
            
this.WriteToLog("Setting permissions"true);
            
try
            {
                
//impersonate an administrator who can change permissions on list items in the list
                ImpersonationUtility imp = ImpersonationUtility.ImpersonateAdmin();
                
//open the spweb object to get the token for the user
                using (SPWeb webOrigUser = properties.OpenWeb())
                {
                    
//get the token for the impersonation user (this will get the user that the ImpersonationUtility is using)
                    SPUserToken token = webOrigUser.AllUsers[WindowsIdentity.GetCurrent().Name].UserToken;
                    
//reopen the spweb object with the new token - full impersonation!
                    using (SPSite site = new SPSite(properties.SiteId, token))
                    {
                        
using (SPWeb web = site.OpenWeb(properties.RelativeWebUrl))
                        {

                            
//call the function that changes the permissions on the list item. 
                            //Do not use properties.ListItem since that will break impersonation!
                            SetAuthorAsOnlyEditor(web.Lists[properties.ListId].GetItemById(properties.ListItemId));
                        }
                    }
                }

                
            }
            
catch (Exception ex)
            {
                
this.WriteToLog("Setting permissions encountered an error: " + ex.Message + Environment.NewLine + ex.ToString(), true);
            }
        }
        
        #endregion

        #region
 custom functions
        
/// <summary>
        /// Loops over the item properties and prints them to the log file. should only be called in debug mode!
        /// </summary>
        /// <param name="item">the list item that is currently handled</param>
        private void WriteItemPropertiesToLog(SPListItem item)
        {
            
this.WriteToLog("Item Properties:",false);
            
foreach(SPField field in item.Fields)
            {
                
try
                {
                    
this.WriteToLog(field.Title + " : " + item[field.InternalName].ToString(), false);
                }
                
catch { }
            }            
        }
        
/// <summary>
        /// Function sets the permission on the list item so that the author has edit permission on the item, 
        /// and all other people with access to the document library can only read.
        /// Relies on the values in the constants c_EditorPermissions and c_ReaderPermissions.
        /// </summary>
        /// <param name="item">the list item that is currently handled</param>
        private void SetAuthorAsOnlyEditor(SPListItem item)
        {
            
            
using (SPWeb currentWeb = item.Web)
            {
                
this.WriteToLog("Getting author from item"true);
                
//get the author from the item. 'Author' is a built-in property, so it should be in all lists.
                string authorValue = item["Author"].ToString();                
                
SPFieldUserValue authorUserValue = new SPFieldUserValue(currentWeb, authorValue);
                
SPUser authorUser = authorUserValue.User;
                
this.WriteToLog("Got author name:'" + authorUser.Name + "', email: '" + authorUser.Email + "'"true);
                
                
this.WriteToLog("Breaking role inheritance for the item"true);                
                
//break the security of the item from the list, but keep the permissions
                item.BreakRoleInheritance(true);
                
//change the permissions of everyone with access to this item so they are readers only (c_ReaderPermissions)
                ChangeItemExistingRoles(item);

                
this.WriteToLog("Creating role '" + c_AuthorRoleName + "' in the site if needed"true);
                
//create a security definition in the web for an author-editor
                SPRoleDefinition def = CreateRoleInSite(currentWeb,c_AuthorRoleName,c_EditorPermissions);
                
this.WriteToLog("Assigning role to the user"true);
                
//Set the author user with the permissions defined
                SPRoleAssignment authorRole = new SPRoleAssignment(authorUser.LoginName, authorUser.Email, authorUser.Name, authorUser.Notes);
                
this.WriteToLog("Binding the role assignment of the user to the definition"true);
                authorRole.RoleDefinitionBindings.Add(def);
                
this.WriteToLog("Adding the role to the item"true);
                item.RoleAssignments.Add(authorRole);
                
this.WriteToLog("Updating the item"true);
                item.Update();
                
this.WriteToLog("Success!"true);
            }
        }
        
/// <summary>
        /// This function will make everyone with access to the item a reader.
        /// Loops over all the roles that exist in the item, removes the bindings and adds the reader permission definition to them
        /// </summary>
        /// <param name="item">the list item currently handled</param>
        private void ChangeItemExistingRoles(SPListItem item)
        {
            
//get, and if necessary create, the reader role
            SPRoleDefinition readerDef = CreateRoleInSite(item.Web, c_ReaderRoleName, c_ReaderPermissions);

            
            
foreach (SPRoleAssignment roleAssignment in item.RoleAssignments)
            {
                
//delete the existing permissions
                roleAssignment.RoleDefinitionBindings.RemoveAll();
                
//add the reader permission
                roleAssignment.RoleDefinitionBindings.Add(readerDef);
                roleAssignment.Update();
                item.Update();

            }
        }
        
/// <summary>
        /// Gets and if necessary creates the role in the site.
        /// </summary>
        /// <param name="web">The site where the role should be created</param>
        /// <param name="roleName">The name of the role to create</param>
        /// <param name="permissions">The permission set to give the role. Example: SPBasePermissions.EditListItems | SPBasePermissions.ViewListItems</param>
        /// <returns></returns>
        private SPRoleDefinition CreateRoleInSite(SPWeb web,string roleName,SPBasePermissions permissions)
        {
            
this.WriteToLog("Checking if role '"+roleName+"' exists in the web"true);
            
//check that the role exists
            if (RoleExists(web, roleName))
            {
                
this.WriteToLog("Role exists in the web"true);
                
//role exists - return it
                return web.RoleDefinitions[roleName];
            }
            
else
            {
                
//role does not exist in the site-  create it and return.
                this.WriteToLog("Role does not exist in the web. creating a new role"true);
                
//Create the role definition in the web by the name specified in c_AuthorRoleName
                SPRoleDefinition def = new SPRoleDefinition();
                def.BasePermissions = permissions;
                def.Name = roleName;
                
this.WriteToLog("Adding the role to the FirstUniqueRoleDefinitionWeb"true);
                web.FirstUniqueRoleDefinitionWeb.RoleDefinitions.Add(def);
                
this.WriteToLog("Updating the web"true);
                web.FirstUniqueRoleDefinitionWeb.Update();
                web.FirstUniqueRoleDefinitionWeb.Dispose();
                
this.WriteToLog("Reopening the current web object"true);
                web = web.Site.OpenWeb();
                
this.WriteToLog("Verifying role is in current web"true);
                
if (RoleExists(web, roleName))
                    
return web.RoleDefinitions[roleName];
                
else
                {
                    
throw new Exception("Role does not exist?");
                }

            }
        }
        
/// <summary>
        /// This function checks the spweb objec to see if a specific role exists (by name)
        /// </summary>
        /// <param name="web">the spweb object for the site to contain the role.</param>
        /// <param name="roleName">the name of the role searched for</param>
        /// <returns></returns>
        private bool RoleExists(SPWeb web, string roleName)
        {
            
this.WriteToLog("Loading the RoleDefinitions xml string:"true);
            
this.WriteToLog(web.RoleDefinitions.Xml, true);
            
//read the xml of the roledefinitions
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(web.RoleDefinitions.Xml);
            
this.WriteToLog("Searching for the role in the xml"true);
            
//search for the role with the name in the xml
            XmlNode node = doc.SelectSingleNode("//Role[@Name='"+roleName+"']");
            
//if the search returned null, the role does not exist
            if (node == null)
                
return false;
            
else
                return true;
        }
        
/// <summary>
        /// writes a message to the log file
        /// </summary>
        /// <param name="message">The message to write</param>
        /// <param name="debugOnly">If true, the message will only get written when the code runs in debug mode.</param>
        private void WriteToLog(string message,bool debugOnly)
        {
            
if (DEBUG_MODE || !debugOnly)
                log.WriteToLogFile(message);            
        }
        #endregion
    }
    
/// <summary>
    /// Handles simple file logging. Recommend switching to trace log.
    /// </summary>
    class HTMLFileLogging
    {
        #region class properties
        
private string logFolderPath = @"c:\logs";
        
public string LogFolderPath
        {
            
get
            {
                
return logFolderPath;
            }
            
set
            {
                
if (Directory.Exists(value))
                {
                    logFolderPath = 
value;
                    
if (logFolderPath.EndsWith("\\"))
                    {
                        logFolderPath = logFolderPath.Remove(logFolderPath.Length);
                    }
                }
                
else
                {
                    
throw new DirectoryNotFoundException();
                }
            }
        }
        
private string logFileName = "";
        
public string LogFileName
        {
            
get
            {
                
return logFileName;
            }
            
set
            {
                logFileName = 
value;
            }
        }
        
public string LogFilePath
        {
            
get
            {
                
return this.LogFolderPath + "\\" + this.LogFileName;
            }
        }
        
        #endregion
        
        #region CTOR
        
/// <summary>
        /// Create a HTMLFileLogging object
        /// </summary>
        /// <param name="folderPath">The path of the folder that will hold the log file (no file name)</param>
        /// <param name="fileName">The name of the file to create</param>
        /// <param name="createPath">When this is set to true and the folder does not exist, the code will create the folder.</param>
        /// <param name="deleteFile">When this is set to true and the file exists, the code will delete the file and create a new one</param>
        public HTMLFileLogging(string folderPath, string fileName, bool createPath, bool deleteFile)
        {
         
            
this.LogFileName = fileName;
            
            
if (createPath && !Directory.Exists(folderPath))
            {
                
Directory.CreateDirectory(folderPath);
            }
            
this.LogFolderPath = folderPath;
            
if (File.Exists(this.LogFilePath) && deleteFile)
            {
                
File.Delete(this.LogFilePath);
            }

        }

        #endregion

        #region
 custom code
        
/// <summary>
        /// Writes a string to the log file. 
        /// </summary>
        /// <param name="message">a string to write. supports html tags.</param>
        public void WriteToLogFile(string message)
        {
            
try
            {
                
StreamWriter sw = new StreamWriter(this.LogFilePath,true);
                sw.WriteLine(
"<p>");
                sw.WriteLine(
"<date>" + DateTime.Now.ToShortDateString()+ "</date> <time>" + DateTime.Now.ToLongTimeString() + "</time> <br /> <message>" + message + "</message>");
                sw.WriteLine(
"</p>");
                sw.Flush();
                sw.Close();
            }
            
catch (Exception ex)
            {
            }
        }

        #endregion

    }
    
/// <summary>
    /// Thanks to impersonation example of Victor Vogelpoel [Macaw] 
    /// http://dotnetjunkies.com/WebLog/victorv/archive/category/2032.aspx
    /// </summary>
    public sealed class ImpersonationUtility
    {        
        
private static string ADMINDOMAINACCOUNT = @"domain\user";//CHANGE THIS!
        private static string ADMINDOMAIN = "domain";//CHANGE THIS!
        private static string ADMINACCOUNT = "user";//CHANGE THIS!
        private static string ADMINPASSWORD = "password";//CHANGE THIS!
        private WindowsImpersonationContext _wiContext;
        
public IntPtr token;
        
/// <summary>
        /// Private ctor.
        /// </summary>
        private ImpersonationUtility()
        { }
        
/// <summary>
        /// Start impersonating the administrator.
        /// </summary>
        /// <returns>an ImpersonationUtility instance.</returns>
        public static ImpersonationUtility ImpersonateAdmin()
        {
            
ImpersonationUtility imp = new ImpersonationUtility();
            imp._ImpersonateAdmin();
            
return imp;
        }
        
/// <summary>
        /// Undo the impersonation.
        /// </summary>
        public void Undo()
        {
            
if (this._wiContext != null)
            {
                
this._wiContext.Undo();
                
this._wiContext = null;
            }
        }
        
private void _ImpersonateAdmin()
        {
            token = 
IntPtr.Zero;
            
IntPtr tokenDuplicate = IntPtr.Zero;
            
try
            {
                
// Only start admin impersonation if we're not the admin...
                if (String.Compare(ADMINDOMAINACCOUNT, WindowsIdentity.GetCurrent().Name, trueCultureInfo.InvariantCulture) != 0)
                {
                    
// Temporarily stop the impersonation started by Web.Config.
                    // WindowsIdentity.Impersonate() will store the current identity (the
                    // account of the requestor) and return to the account of the ApplicationPool
                    // which is "DOMAIN\SPAdmin".
                    this._wiContext = WindowsIdentity.Impersonate(IntPtr.Zero);
                    
// But somehow the reverted account "DOMAIN\SPAdmin" still does
                    // not have enough privileges to access SharePoint objects, so
                    // we're logging in DOMAIN\SPAdmin again...
                    if (NativeMethods.LogonUserA(ADMINACCOUNT, ADMINDOMAIN, ADMINPASSWORD, NativeMethods.LOGON32_LOGON_INTERACTIVE,
                    
NativeMethods.LOGON32_PROVIDER_DEFAULT, ref token) != 0)
                    {
                        
if (NativeMethods.DuplicateToken(token, 2, ref tokenDuplicate) != 0)
                        {
                            
WindowsIdentity wi = new WindowsIdentity(tokenDuplicate);
                            
// NOTE: Impersonate may fail if account that tries to impersonate does
                            // not hold the "Impersonate after Authentication" privilege
                            // See local security policy - user rights assignment.
                            // Note that the ImpersonationContext from the Impersonate() call
                            // is ignored. Upon the Undo() call, the original account
                            // will be reinstated.
                            wi.Impersonate();
                        }
                        
else
                        {
                            
throw new Win32Exception(Marshal.GetLastWin32Error(),
                            
"Impersonation: Error duplicating token after logon for user \"DOMAIN\\SPAdmin\"");
                        }
                    }
                    
else
     &