Saturday, 19 May 2012

How to use BeforeProperties and AfterProperties in an RunWithElevatedPrivileges block

I came up against a tricky problem this week when I wrote code that would not work for a non-admin account and needed to wrap it up in a privilege block (note: I am not using RunWithElevatedPrivileges as it's dodgy, see earlier blog entries on the subject, but it's along similar lines.)

As explained before, an override method for an SPItemEventReceiver object always passes in the properties of the list item as a parameter. This allows for manipulation of the object. 

But we also know that the properties object cannot be used within the SPSecurity.RunWithElevatedPrivileges block as an entirely new site object needs to be created with separate web, list and item objects created with it. This is fine usually, but what if we need to compare the value of a column before being updated with the results after it has been changed? Usually we would be able to avail of properties.BeforeProperties["Fieldname"] and properties.AfterProperties["Fieldname"] and compare the two, but our new SPListItem object inside the privilege block does not possess either of these attributes. So, what to do?

The solution is below - important lines in bold:

SPItemEventDataCollection before = properties.BeforeProperties;
SPItemEventDataCollection after = properties.AfterProperties;
Guid siteID = properties.SiteId;
Guid webID = properties.ListItem.Web.ID;               
//this is for getting the user Token to log in as system
SPSite tempSite = new SPSite(siteID);
SPUserToken sysToken = tempSite.SystemAccount.UserToken;
//need to dump this as not declared in a using block
tempSite.Dispose();
//start the privilege block
using (SPSite site = new SPSite(siteID, sysToken))
{
    using (SPWeb web = site.OpenWeb(webID))
    {
        if (before["FieldName"].ToString() != after["FieldName"].ToString())
        { someBooleanVariable = true; }
        //do work
    }

}

Thursday, 26 April 2012

Argh! I Locked Myself Out!

But I didn't get back in by being given a key - more like kicking the door down, but anyway...

A few days ago I was testing something on the dev machine and removed a few user groups from the site permissions list on a site collection. They were still associated with the site, just had no site permissions. I was logged in as site admin. Went to do something on one of those sites and what do you know, I can't get in! Checked the site collection admin, where I was queen of everything and all looked well. Checked Central Admin and the Sharepoint:80 web application, found my errant site collection, and yes I was site admin. Still couldn't get in.

Here is how I got back in. I do not recommend using this as a method to get in. It is a horrible hack and will consign me to SharePoint Hell. I only did it because it was a dev machine and the outcome was not important. However - it is an example of how to dynamically add an existing SharePoint user group onto a Site Permissions list if it isn't on it already:


using (SPSite site = new SPSite("http://notmyemployerssitecoll/sc/sample/"))
{
     using (SPWeb web = site.OpenWeb())
     {
         foreach (SPGroup group in web.AssociatedGroups)
         {
             if (group.Name.Contains("My Little Lost Group"))
             {
                 //assign it permissions to control everything
                 //obviously when we do this for real, we want it to grab the
                 //existing role assignment
                 SPRoleAssignment roleAssignment = newSPRoleAssignment(web.SiteGroups[group.Name]);
                 SPRoleDefinitionBindingCollection roleDefinition = roleAssignment.RoleDefinitionBindings;
                 roleDefinition.Add(web.RoleDefinitions["Full Control"]);
                 web.RoleAssignments.Add(roleAssignment);
                 web.Properties[group.Name] = "Full Control";
                 web.Properties.Update();
             }
         }
     }
}

Wednesday, 18 April 2012

SPUtility.RunWithElevatedPrivileges Fails with getting SPUser from groups

Using SPUtility.RunWithElevatedPrivileges fails when trying to get SPUser objects from groups. It worked when I removed the privilege block, but this was not practical for non-admin users. The code below worked perfectly for me:


            Guid groupWebID = SPContext.Current.Web.ID;
            Guid groupSiteID = SPContext.Current.Site.ID;
            SPUserToken sysToken = SPContext.Current.Site.SystemAccount.UserToken;
 
            using (SPSite groupSite = new SPSite(groupSiteID, sysToken))
            {
                using (SPWeb groupWeb = groupSite.OpenWeb(groupWebID))
                {
 
                    SPGroup group = groupWeb.Groups["My Group"];
                    foreach (SPUser user in group.Users)
                    {
                        AllUsers.Items.Add(user.Email);
                    }
 
                }
            }

Sunday, 15 April 2012

Not SP-Related: Searching for a Proper Name Within Range of a Word

This is not related to the day job or to sharepoint but it is coding stuff so here is as good a place as any to put it. I rarely do geek stuff in my free time but I thought this might be of some interest.

I was having a discussion with a very nice journalist on the tweet machine a few weeks ago and proposed I write some code for her. She wanted to search a Very Large CSV file (which I won't name cos it's a bit political and this blog is not about politics :-) about 2GB in size for names surrounding various words. One of these words was the word "whistleblower".

I came up with an algorithm that would search either a 50-character radius either side of the instance of the word, or, if there weren't enough characters, the whole line (I was asking the filestream to ReadLine() each time) It would look for a word that had a capital letter that was not preceded by a space and a full stop. In order to make it run, I downloaded the freebie edition of Microsoft Visual Studio C# Express, which had everything I needed for the purpose.

Now this is not perfect and it needs quite a bit of work. For a start, I need to write some extra code to grab the initial code of each cable (yeah, it's becoming more obvious what I'm referring to here, for the record allow me to say that I have no interest in the cause this organisation espouses, or affection for its leaders, but given it is now in the public domain...::shrugs::) But if I wait till I do that, I might never get it done. Also the file has the annoying habit of containing a lot of acronyms and randomly capitalised words, a prob which I can't get around at present.

What this code does: Examines file and searches for every instance of the word "whistleblower" (word can be changed) then writes everything to a file called results.txt, which will be found in the \bin folder of your C# project.

What you need to do. Download VS, Create a console application, go to Program.cs and copy this in wholesale. Sorry about the brackets, it should format them properly for you once it's pasted in

Oh and you need to download the source file. There are plenty of web resources telling you how to do that :)


using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;

namespace ReadFromFile
{
class Program
{
static void Main(string[] args)
{
StreamReader reader = null;

//write to file - change YourFolder to your actual folder!
FileStream fs = new FileStream("C:\\YourFolder\\results.txt", FileMode.Create);
StreamWriter sw = new StreamWriter(fs);
TextWriter tmp = Console.Out;

try
{
//read from file - change YourFolder to your actual folder!
string fileName = "C:\\YourFolder\\cables.csv";
reader = File.OpenText(fileName);
Console.SetOut(sw);

while (!reader.EndOfStream)
{
string line = reader.ReadLine();
int position = -1;
//change to lower case version of word
position = line.IndexOf("whistleblower");
if (position == -1)
{
//change to capitalised version of word
position = line.IndexOf("Whistleblower");
if (position == -1)
{
//no instances, next row
continue;
}
}

//we have a value for required string
string subLine = line;
if ((position - 50) >= 0 && ((position + 50) <= line.Length))
{
subLine = line.Substring(position - 50, position + 50);
}
else
{
subLine = line.Substring(0, line.Length);
}

char[] characters = line.ToCharArray();

for (int i = 0; i < characters.Length; i++)
{
if (Char.IsLetter(characters[i]) && (i >= 2))
{

if ((!characters[i - 2].Equals(".")))
{

if (Char.IsUpper(characters[i]))
{ //do stuff
//start from i and go to next space
Console.Write("Found possible name string: ");
for (int j = i; j < characters.Length; j++)
{
if (!Char.IsWhiteSpace(characters[j]))
{ Console.Write(characters[j]); }
else
{ Console.WriteLine(); break; }
}
}
}
}

}
}

}
finally
{
//close in and out files
sw.Close();
fs.Close();
reader.Close();
}

//reset console output
Console.SetOut(tmp);
}
}
}


Wednesday, 11 April 2012

Deleting Items on a List using SPWeb.ProcessBatchData()

I had a situation a week ago where I needed to write a script to delete all items in a list while maintaining the list structure. Now for those familiar with the SharePoint API, the obvious solution would be something like this:

foreach (SPListItem item in list.Items)
{
    item.Delete();
}


However theIEnumerator interface does not like deleting things while iterating through them because the list count will change while you're doing it. This is mighty confusing for an iterating loop so that's handled as an error and SharePoint sez, no can do, sorry. I tried using list.Items[0].Delete() as I thought that might work since there will always be at least one item in the collection, but no - when it got to the end of the collection it threw an error and all my old VB programming days of On Error Resume Next could not help me in my attempts to keep the code running after the error.


But anyway. All this has happily become a moot point since discovering the SPWeb.ProcessBatchData() command, since all I have to do is loop through every list in the web and issue a delete command. At first I got to the Microsoft help for this (always, always a bad idea. Sorry, Microsoft, but there it is) and got scared off by talk of OWS files and piles of XML. How and ever, after a bit of digging I found a great entry on Stack Overflow that assures me one does not need to be rooting around obscure XML files in the Twelve Hive in ungraceful fashion, but can build the XML on the fly as a string, feed it in to the ProcessBatchData command and hey presto, you're off:



foreach (SPList list in web.Lists)
   {
     try
       {
        //decrement loop so as not to confuse the iEnumerator interface
       Console.WriteLine("Deleting list items in following list : " + list.Title);
       SPListItemCollection splic = list.Items;
       StringBuilder batchString = new StringBuilder();
       batchString.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>");
       foreach (SPListItem item in splic)
       {
         batchString.Append("<Method>");
         batchString.Append("<SetList Scope=\"Request\">" + Convert.ToString(item.ParentList.ID) + "</SetList>");
         batchString.Append("<SetVar Name=\"ID\">" + Convert.ToString(item.ID) + "</SetVar>");
         batchString.Append("<SetVar Name=\"Cmd\">Delete</SetVar>");
         batchString.Append("</Method>");
       }
       batchString.Append("</Batch>");
       web.ProcessBatchData(batchString.ToString());
    }
    catch { //whatever }

 }





Please note that I actually found this code somewhere on Stack Overflow, but have gone and lost the link. So Unknown Programmer, you have my greatest gratitude for this!

I believe there are Copy and Insert methods also but will have to check those out. I may have a requirement for these, due to SharePoint UI inflexibility, so stay tuned...

Monday, 13 February 2012

Retrieving MySite Info to a list via a User Value Field

Greetings. I have not updated this blog from some time because I was on leave all of January and was relaxing and enjoying myself. I follow SharePoint on my twitter feed and one of the tweets was "Do you dream about #SharePoint?" and I RT'ed the tweet with the additional comment, "You've got to be joking!" But now I am back and thoughts of such things return to my mind. This will be my first post for 2012, then. I spent quite some time struggling with this one. As far as I know it's the one place where the whole routine is present in one place so if anyone needs it they can use it.

If you want to get more info on a user, often you'll click on their MySite. This code effectively does the same thing. If you have a list or library that contains a "user or group" control, you can retrieve the user information from the field and you can use the UserProfileManager object to get all the info on that user. To do this, you're going to need some libraries that you might not normally have in your .NET store. So, first:

using System;
using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
using System.Web;

Now, if you can't see the Microsoft.Office.Server library in your usual reference store, don't panic. Go to your Twelve Hive folder and look for the ISAPI directory. The dll file should be in there. Add it in as a reference to your VB project.

Let's presume you are going to create a conventional event handler for the ItemAdded event and copy the resulting data into a text field called "Business Unit". We'll skip the event handler code as we've covered that already. I'm now going to use a privilege wrapper and get the string value of the user field. This was the bit that caused me by far the most grief. The ToString() method would not work on the user control field. Don't ask me why it wouldn't work, it just wouldn't. Kept returning nulls. So I did it like this:



Guid siteID = properties.SiteId;
Guid webID = properties.ListItem.Web.ID;
Guid listID = properties.ListId;
Guid listItemID = properties.ListItem.UniqueId;
SPSecurity.RunWithElevatedPrivileges(delegate()
{
     using (SPSite site = new SPSite(siteID))
     {
          using (SPWeb web = site.OpenWeb(webID))
          {
                            
                SPList list = web.Lists[listID];
                SPListItem item = list.Items[listItemID];
                string userValue = (string)item["Reported By User"];
                SPFieldUserValue fieldUserValue = new SPFieldUserValue(web, userValue);
                            
                SPUser user = fieldUserValue.User;
                string department = GetBusinessUnit(user, site);            
                item["Business Unit"] = department;
                item.Update();
                        }
                    }
                });

//...
//subroutine to get profile info here


private string GetBusinessUnit(SPUser user, SPSite site)
{
    string busUnit = string.Empty;
    try
    {        
        ServerContext serverContext = ServerContext.GetContext(site);
        UserProfileManager upm = new UserProfileManager(serverContext);
        UserProfile profile = upm.GetUserProfile(user.LoginName);
        busUnit = profile["Department"].ToString();
    }
    catch (Exception ex)
    {
         //add your own error handling here
        busUnit = ex.Message + " : " + errorArgs;
    }
            
    return busUnit;

}

This I can guarantee will work because I ran it successfully myself. One note with error trapping: if you create the profile and use a field that has a null value in the user's mySite, there's nothing here to gracefully trap that at present, unless you don't mind your control displaying a honking big error message :)