If anyone knows a better way then let me know.

1
2
3
4
5
6
7
8
9
10
11
string smtp = string.Empty;

SPFarm.Local.Servers.ForEach(s => s.ServiceInstances.ForEach(i =>
{
if(i is SPOutboundMailServiceInstance)
{
smtp = s.Address;
}
}));

return smtp;

I use a ForEach extension which I’ve included below

1
2
3
4
5
6
7
public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
{
foreach(T item in enumerable)
{
action(item);
}
}

Comment and share

For a standard team site (STS#0) the GlobalNodes SPNavigationNodesCollection will contain three SPNavigationNode objects.

Home, Quick Launch and SharePoint Top Navigation Bar, these each have Ids of 1000, 1002 and 1025 respectively.

The problem occurs if you delete Quick Launch or SharePoint Top Navigation nodes. Even if you then add them back in a new ID is generated and SPWeb.Navigation.TopNavigationBar looks for child nodes under a node with an Id of 1002 (This is hard coded).

Through the API this means that SPWeb.Navigation.TopNavigationBar will always be null and through the SharePoint UI an error will be returned to the user (“Value does not exist in that range”).

The only ways I have found to fix this is to delete and re-create the site or (Microsoft please close your ears) get the site id and web id and query the NavNodes table in the content database the site collection sits in and add the following records

SiteId WebId Eid EidParent NumChildren RankChild ElementType Url DocId Name DateLastModified NodeMetaInfo NonNavPage NavSequence ChildOfSequence
C803FD13-27FA-475F-909C-2CE5B2048E1B A31E8093-1505-4E1C-B7CC-04D6B952A33F 1000 0 0 0 0 NULL A31414EE-953B-44CC-A37E-4D5D643B002A Home 2008-04-04 11:28:21.000 NULL False False False
C803FD13-27FA-475F-909C-2CE5B2048E1B A31E8093-1505-4E1C-B7CC-04D6B952A33F 1002 0 0 2 1 NULL NULL SharePoint Top Navigation 2008-04-04 11:28:21.000 NULL False True False
C803FD13-27FA-475F-909C-2CE5B2048E1B A31E8093-1505-4E1C-B7CC-04D6B952A33F 1025 0 0 1 1 NULL NULL Quick Launch 2008-04-04 11:28:21.000 NULL True True False

Comment and share

To match the ScopeId in the EventData property of the SPAuditEntry to a SPListItem you need to use the hidden column ScopeId and not the Id property of SPListItem.

Here is what I mean.

The following XML is returned in the EventData proprty for a SPAuditEventType of SecRoleBindUpdate. To match the Scope returned in the XML to SPSite, SPWeb and SPList object you would use the Id property of this object, but not for the SPListItem.

1
2
3
<roleid>1073741826</roleid>
<principalid>11</principalid>
<scope>72EEC412-B14B-4EFB-AB95-EA821A3A4C63</scope>

You need to use the ScopeId column of the SPListItem

So why can’t I just use the SPAuditQuery.RestrictToListItem method to return the audit entries for that SPListItem.

It doesn’t work, that’s why and this has been confirmed by MS Support.

Here is a workaround that will link the ScopeId to the SPListItem that triggered the audit entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using System; 
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace ConsoleApplication1
{  
class Program  
{  
static void Main(string[] args)  
{
string siteUrl = "http://url";  
string listName = "TheList";  
string scopeIdFromItem = string.Empty;  
string scopeIdFromChange = string.Empty;

try  
{  
using (SPSite site = new SPSite(siteUrl))  
{  
using (SPWeb web = site.OpenWeb())  
{
SPList list = web.Lists[listName];
SPRegionalSettings regionalSettings = web.RegionalSettings;  
SPTimeZone timeZone = regionalSettings.TimeZone;
SPAuditQuery query = new SPAuditQuery(site);  
SPAuditEntryCollection auditEntries = site.Audit.GetEntries(query);

foreach (SPAuditEntry auditEntry in auditEntries)  
{  
if (IsSecurityEvent(auditEntry))  
{  
try  
{  
if (auditEntry.Event.ToString().ToUpper() == "SECROLEBINDUPDATE")  
{
// Should really do a proper XML query here, but for demo just do some string stuff
scopeIdFromChange = auditEntry.EventData.ToString().Substring((auditEntry.EventData.ToString().Length - 44), 36);
foreach (SPListItem item in list.Items)  
{ // Square brackets round the scope id need to be removed
scopeIdFromItem = item["ScopeId"].ToString().Substring(1, 36);

if (scopeIdFromChange == scopeIdFromItem)  
{  
Console.Write("Event Data : ");
Console.WriteLine(auditEntry.EventData.ToString());  
Console.Write("Occurred : ");
Console.WriteLine(timeZone.UTCToLocalTime(auditEntry.Occurred));

if (list.BaseType == SPBaseType.DocumentLibrary)  
{  
Console.WriteLine("Security changed on file: " + item.File.ToString());  
}  

if (list.BaseType == SPBaseType.GenericList)  
{  
Console.WriteLine("Security changed on item: " + item.Name.ToString());  
}  
}  
else  
{  
Console.WriteLine("No item found in list " + siteUrl + "/" + listName + " for " + auditEntry.EventData.ToString());  
}  
}  
}  
}  
catch (Exception ex)  
{  
Console.WriteLine(ex.Message.ToString());  
}
}  
}  
}  
}  
}  
catch (Exception ex)
{  
Console.WriteLine(ex.Message);  
}  
}

private static bool IsSecurityEvent(SPAuditEntry entryToTest)  
{  
SPAuditEventType[] SECURITY_EVENTS = new SPAuditEventType[]  
{  
SPAuditEventType.SecGroupCreate,  
SPAuditEventType.SecGroupDelete,  
SPAuditEventType.SecGroupMemberAdd,  
SPAuditEventType.SecGroupMemberDel,  
SPAuditEventType.SecRoleBindBreakInherit,  
SPAuditEventType.SecRoleBindInherit,  
SPAuditEventType.SecRoleBindUpdate,  
SPAuditEventType.SecRoleDefBreakInherit,  
SPAuditEventType.SecRoleDefCreate,  
SPAuditEventType.SecRoleDefDelete,  
SPAuditEventType.SecRoleDefModify  
};

foreach (SPAuditEventType eventType in SECURITY_EVENTS)  
{  
if (eventType == entryToTest.Event)
return true;  
}
return false;
}
}
}
}
}

Comment and share

UPDATE

Don’t use the Sharepoint Solution Generator, so far we have not been able to upgrade the site definitions through a solution without it killing existing sites based on the site definition.


I have seen many different ways of doing this, but I found the following method seemed to work for me, using the “SharePoint Extensions for Visual Studio 2005” and the “SharePoint Solutions Generator

  • Use the “SharePoint Solution Generator” to create a site definition project.
  • Open the project and add your custom master page to the “Site Definition” folder

  • Open up “onet.xml”
  • Add the following node to the “ListTemplates” section
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<ListTemplate 
Name=”mplib”
DisplayName=”$Resources:MasterPageGallery;”
Description=”$Resources:global_onet_mplib_desc;”
SetupPath=”global\lists\mplib”
Type=”116”
BaseType=”1”
Path=”GLOBAL”
Hidden=”TRUE”
HiddenList=”TRUE”
NoCrawl=”TRUE”
Unique=”TRUE”
Catalog=”TRUE”
OnQuickLaunch=”FALSE”
SecurityBits=”11”
AllowDeletion=”FALSE”
AllowEveryoneViewItems=”TRUE”
Image=”/_layouts/images/itdl.gif”
AlwaysIncludeContent=”TRUE”
DocumentTemplate=”100”
/>
  • Add the “MasterUrl” attribute to the “Configuration” node with a value of “_catalogs/masterpage/project.master”, where project.master is the name of your custom master page. This means all the pages using the “~masterurl/default.master” token will use the URL defined to point to your custom master page
1
2
3
4
5
<Configuration 
ID=”0”
Name=”Default”
MasterUrl=”_catalogs/masterpage/project.master“
>
  • Add a “Module” node to the Modules section of the configuration called “MasterPage” that will reference the module definition in the “Modules” section of the onet file
1
<Module Name=”MasterPage” />
  • Add a module definition to the “Modules” section of the onet file. This will copy the custom master page file into the master page library when the site is provisioned.
1
2
3
4
5
6
7
8
9
10
11
<Module 
Name=”MasterPage”
List=”116”
Url=”_catalogs/masterpage”
RootWebOnly=”FALSE“>
<File
Url=”project.master”
Type=”GhostableInLibrary”
IgnoreIfAlreadyExists=”TRUE”
/>
</Module>
  • Save the onet file and then deploy the project into your environment using the deploy option in “SharePoint Extensions for VS 2005”
  • When you create a new site from your site definition it should now use your new master page.

Below is a screen grab of the onet.xml file with all the changes highlighted

Comment and share

Author's picture

Toby Statham

Independent Office 365 / SharePoint Specialist and an associate consultant at aiimi.com, an Information Management company.

Independent Office 365 / SharePoint Specialist

Brighton, UK