Monday, March 16, 2009

A Feature to Setup Site Auditing

Some of our SharePoint sites need auditing turned on to monitor changes to content. This is a fairly simple thing to do through the user interface (http://siteurl/_layouts/AuditSettings.aspx).

When using site definitions it is best to remove as many manual steps as you can. The following is a handy feature to automatically setup the Audit Settings.

The feature:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

 

using MaskedSharePointer.Logging;

 

namespace MaskedSharePointer.Features.Receivers

{

    public class SiteAuditingReceiver : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            if (!(properties.Feature.Parent is SPSite))

                throw new ApplicationException("Site Auditing Receiver is a site feature.");

 

            SPSite site = properties.Feature.Parent as SPSite;

 

            int auditFlags = 0;

 

            foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

            {

                try

                {

                    auditFlags += Convert.ToInt32(Enum.Parse(typeof(SPAuditMaskType), featureProperty.Value, true));

                }

                catch (Exception ex)

                {

                    Logger.LogException(ex);

                }

            }

 

            site.Audit.AuditFlags = (SPAuditMaskType)Enum.ToObject(typeof(SPAuditMaskType), auditFlags);

            site.Audit.Update();

        }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            if (!(properties.Feature.Parent is SPSite))

                throw new ApplicationException("Site Auditing Receiver is a site feature.");

 

            SPSite site = properties.Feature.Parent as SPSite;

            site.Audit.AuditFlags = SPAuditMaskType.None;

            site.Audit.Update();

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

    }

}



And the feature to activate the audit settings. Its customisable by changing the properties to the appropriate level of auditing:

<?xml version="1.0" encoding="utf-8" ?>

<Feature

    Title="Site Auditing"

    Description="Sets up site collection audit settings"

    Id="{GUID}"

    Scope="Site"

    Version="1.0.0.0"

    Hidden="FALSE"

    xmlns="http://schemas.microsoft.com/sharepoint/"

    ReceiverAssembly="MaskedSharePointer.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx"

    ReceiverClass="MaskedSharePointer.Features.Receivers.SiteAuditingReceiver">

    <Properties>

        <!-- One of these -->

        <Property Key="All" Value="All"/>

        <Property Key="None" Value="None"/>

 

        <!-- OR a combination of any of the following-->

        <Property Key="View" Value="View"/>

        <Property Key="CheckIn" Value="CheckIn"/>

        <Property Key="CheckOut" Value="CheckOut"/>

        <Property Key="ChildDelete" Value="ChildDelete"/>

        <Property Key="Copy" Value="Copy"/>

        <Property Key="Delete" Value="Delete"/>

        <Property Key="Move" Value="Move"/>

        <Property Key="ProfileChange" Value="ProfileChange"/>

        <Property Key="SchemaChange" Value="SchemaChange"/>

        <Property Key="SecurityChange" Value="SecurityChange"/>

        <Property Key="Undelete" Value="Undelete"/>

        <Property Key="Update" Value="Update"/>

        <Property Key="Workflow" Value="Workflow"/>

        <Property Key="Search" Value="Search"/>

    </Properties>

</Feature>

Setting up Timer Job to Cleanup Audit Log

I had a requirement to cleanup the SharePoint audit log after a certain amount of time (in our case a year). This is a simple operation using the SPAudit class and the DeleteEntries() method.

The timer job definition:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

using Microsoft.SharePoint.Administration;

using Microsoft.SharePoint.Utilities;

 

using MaskedSharePointer.Logging;

 

namespace MaskedSharePointer.TimerJobs

{

    public class AuditLogManagerJobDefinition : SPJobDefinition

    {

        private const string siteIdPropertyName = "SiteCollectionId";

        private const string daysToRetainPropertyName = "DaysToRetain";

 

        private const string dateFormat = "dd/MM/yyyy HH:mm:ss";

 

        public AuditLogManagerJobDefinition()

        {

        }

 

        public AuditLogManagerJobDefinition(string jobName, SPWebApplication webApp, Guid siteId, int daysToRetain): base(jobName, webApp, null, SPJobLockType.ContentDatabase)

        {

            this.Title = jobName;

            this.Properties.Add(siteIdPropertyName, siteId);

            this.Properties.Add(daysToRetainPropertyName, daysToRetain);

        }

 

        public override void Execute(Guid targetInstanceId)

        {

            try

            {

                Guid siteId = (Guid)this.Properties[siteIdPropertyName];

                int daysToRetain = (int)this.Properties[daysToRetainPropertyName];

 

                DateTime deleteEndDate = DateTime.Now.AddDays(-daysToRetain);

 

                using (SPSite site = new SPSite(siteId))

                {

                    SPAudit audit = site.Audit;                   

                    int recordsDeleted = audit.DeleteEntries(deleteEndDate);

 

                    Logger.LogInformation(string.Format("{3} Timer Job completed at {0}.  It removed all records created before {1} ({2} audit log records were deleted).", DateTime.Now.ToString(dateFormat), deleteEndDate.ToString(dateFormat), recordsDeleted, Title));

                }

            }

            catch (Exception ex)

            {

                Logger.LogException(ex);

            }

        }

    }

}



The feature to activate the timer job:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

using Microsoft.SharePoint.Administration;

 

using MaskedSharePointer.TimerJobs;

 

namespace MaskedSharePointer.Receivers

{

    public class AuditLogManager : SPFeatureReceiver

    {

        private readonly string _scheduleKey = "Schedule";

        private readonly string _daysToRetainKey = "DaysToRetain";

 

        private const int _defaultDaysToRetain = 365;

 

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            string jobName = properties.Definition.DisplayName;

 

            SPSite site = properties.Feature.Parent as SPSite;

 

            RemoveExistingTimerJob(site, jobName);

 

            int daysToRetain = GetDaysToRetain(properties);

 

            AuditLogManagerJobDefinition auditLogManagerJobDefinition = new AuditLogManagerJobDefinition(jobName, site.WebApplication, site.ID, daysToRetain);

            auditLogManagerJobDefinition.Schedule = GetSchedule(properties);

            auditLogManagerJobDefinition.Update();

        }

 

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            string jobName = properties.Definition.DisplayName;

 

            SPSite site = properties.Feature.Parent as SPSite;

            RemoveExistingTimerJob(site, jobName);

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

 

        private static void RemoveExistingTimerJob(SPSite site, string jobName)

        {

            foreach (SPJobDefinition job in site.WebApplication.JobDefinitions)

            {

                if (job.Name == jobName)

                {

                    job.Delete();

                    break;

                }

            }

        }

 

        private SPSchedule GetSchedule(SPFeatureReceiverProperties properties)

        {

            try

            {

                if (properties.Feature.Properties[_scheduleKey] != null)

                    return SPSchedule.FromString(properties.Feature.Properties[_scheduleKey].Value);

                else

                    return new SPWeeklySchedule();

            }

            catch

            {

                return new SPWeeklySchedule();

            }

        }

 

        private int GetDaysToRetain(SPFeatureReceiverProperties properties)

        {

            try

            {

                if (properties.Feature.Properties[_daysToRetainKey] != null)

                    return Convert.ToInt32(properties.Feature.Properties[_daysToRetainKey].Value);

                else

                    return _defaultDaysToRetain;

            }

            catch

            {

                return _defaultDaysToRetain;

            }

        }

    }

}



And the xml of the feature to setup how often to run the timer job. I run it daily as the call to DeleteEntries() does a simple SQL delete directly on the content database:

<?xml version="1.0" encoding="utf-8" ?>

<Feature

    Id="{GUID}"

    Title="Audit Log Manager Timer Job"

    Description="Timer job that clears the audit log after a certain amount of time."

    Scope="Site"

    Version="1.0.0.0"

    Hidden="FALSE"

    xmlns="http://schemas.microsoft.com/sharepoint/"

    ReceiverAssembly="MaskedSharePointer.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx"

    ReceiverClass="MaskedSharePointer.Receivers.AuditLogManager ">

    <Properties>

        <Property Key="DaysToRetain" Value="365" />

 

        <!-- Choose one of the following and modify as necassary -->

        <Property Key="Schedule" Value="every 20 minutes"/>

        <Property Key="Schedule" Value="hourly between 8 and 18"/>

        <Property Key="Schedule" Value="daily at 09:00:00"/>

        <Property Key="Schedule" Value="weekly at mon 09:00:00"/>

        <Property Key="Schedule" Value="monthly at 1 09:00:00"/>

        <Property Key="Schedule" Value="yearly at jan 1 09:00:00"/>

    </Properties>

</Feature>

Saving Sort Order to Column in Links List

The following code copies the sort order (using "Change Order") for a links list into a list column.

The sort order for a list item in a links list is stored in the Xml property, in the attribute ows_Order. This however is not migrated with content deployment, hence the need for this event handler.

This ItemEventReceiver is activated as a web scoped feature, on all links lists by using the following in the elements.xml file:

<Receivers ListTemplateId="103">



The event handler code is here:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Xml.Linq;

using System.Text;

 

using Microsoft.SharePoint;

 

namespace MaskedSharePointer.Features.Receivers

{

    public class LinksListReceiver : SPItemEventReceiver

    {

        private string _rankFieldName = "Rank";

        private string _xmlAttributeName = "ows_Order";

 

        public override void ItemAdded(SPItemEventProperties properties)

        {

            base.ItemAdded(properties);

            SaveSortOrder(properties);

        }

 

        public override void ItemUpdated(SPItemEventProperties properties)

        {

            base.ItemUpdated(properties);

            SaveSortOrder(properties);

        }

 

        private void SaveSortOrder(SPItemEventProperties properties)

        {

            this.DisableEventFiring();

            try

            {

                using (SPWeb web = properties.OpenWeb())

                {

                    SPList list = web.Lists[properties.ListTitle];

 

                    if (!list.Fields.ContainsField(_rankFieldName))

                    {

                        SPField newField = list.Fields.CreateNewField(SPFieldType.Number.ToString(), _rankFieldName);

                        newField.ReadOnlyField = true;

                        list.Fields.Add(newField);

                    }

 

                    foreach (SPListItem listItem in list.Items)

                    {

                        XElement element = XElement.Parse(listItem.Xml);

                        XAttribute attribute = element.Attribute(_xmlAttributeName);

 

                        decimal newRank = Convert.ToDecimal(attribute.Value);

 

                        if (newRank != Convert.ToDecimal(listItem[_rankFieldName]))

                        {

                            listItem[_rankFieldName] = newRank;

                            listItem.Update();

                        }

                    }

                }

            }

            finally

            {

                this.EnableEventFiring();

            }

        }

    }

}