Custom Note Board web part, SocialCommentManager, social security trimming and Search

If you have used My Sites in SharePoint 2010, you would have seen the new Note Board web part. The Note Board web part is a generic web part and can be used in other sites also such as publishing portals, team sites etc. It enables users to leave short, publicly-viewable notes or comments about the page, where the web part exists.

There are scenarios, where you’d like to customize the out-of-the-box (OOB) Note Board web part. For example, you might want to show dates in different format, or you want to provide ‘report abuse’ feature on each note. You might want to show the total number of notes within the web part. Unfortunately, the OOB web part offers very limited flexibility to customize its UI or provide other features on top of it. So, you would need to write your own custom Notes or Comments web part.

How to write a custom Notes or Comments web part? First option would be to extend the class used by OOB Note Board web part – SocialCommentWebPart class. Unfortunately again, the class is sealed and you can’t inherit from it. The only option left is to use SharePoint object model (OM) to view/add/delete/edit comments and write the whole UI from scratch. The OM provides a class  SocialCommentManager, which provides extensive methods to do work with social comments or notes. For example: AddComment, GetComments, GetCount. However, if you want to provide all features provided by OOB web part, you are going to face some challenges. In the following section, I would would explain those challenges and how to overcome them.

Most of social data in SharePoint (tag, comment, rating) is controlled by social security trimming. Before SocialCommentManager returns comments for a page, it uses a component called the security trimmer to determine whether the current user has permission to view that Web page. If the user is not permitted to view the Web page, SocialCommentManager does not returns the comment.

As the search service crawls Web pages, it records the permissions that are required to view each Web page. The security trimmer uses this information to determine whether a given user has permission to view a specific Web page. If the security trimmer has insufficient information to determine whether a user has permission to view a Web page, it errs on the side of caution and reports that the user does not have permission to view the Web page. You can read more about social security trimming here: Privacy and security implications of social tagging

As a result, if the search service has not crawled a Web page, SocialCommentsManager returns only current user’s comments on that page and does not return comments added by other users. So if you don’t have SharePoint Search in your environment, you would not be able to show comments by all users in your custom web part. Also, if your Search incremental crawl is scheduled every night (which happens most of the cases), when a new page is created, users would not be able to see other’s comments until the page is crawled in the night.

There are two ways to overcome this issue. SocialCommentsManager has an internal method overload for GetComments that takes a Boolean argument whether to consider security trimming or not. OOB web part also uses this method.

 internal SocialComment[] GetComments(Uri url, int maximumItemsToReturn, int startIndex, DateTime excludeItemsTime, bool needSecurityTrim)

You can use reflection to call this method to get comments. If you don’t want to use reflection, you can resort to the other method, which is to disable social security trimming. This can also help you with OOB web parts that are controlled through social security trimming such as  What’s New web part that shows user’s colleagues activities.

Be aware that by disabling security trimming, you override the SharePoint privacy controls. You disable social security trimming by using the following PowerShell 

 $upaProxyId = (Get-SPServiceApplicationProxy |? {$_.DisplayName -eq "<Name of your user profile SA proxy"}).Id
Remove-SPPluggableSecurityTrimmer -UserProfileApplicationProxyId $upaProxyId -PlugInId 0

Remember the social security trimming is enabled or disabled at the level of Proxy and NOT at the level of whole service application. 

Delete and Edit Comments

There is no method exposed by SocialCommentManager to Delete comments or Edit comments. Actually there are methods DeleteComment and UpdateComment, but those are internal. But, we have an asmx web service SocialDataService that exposes both of these methods: DeleteComment, and UpdateComment. So, we can use SocialCommentManager for adding and getting comments and use SocialDataService for deleting and editing comment. Otherwise, we can use SocialDataService for everything. However, there is still a catch. With OOB Notes web part, owner of comment as well as site owners can delete any comment on the web part. Whereas, the SocialDataService DeleteComment method deletes the social comment associated with the current user only. How do you provide the capability to site owners to delete any comment? Even if you try impersonation or try running code with RunWithElevatedPrivileges, it doesn’t allow to delete a comment of a user other than the current logged on user.

The only solution I could find for this is to use reflection and execute internal methods of SocialCommentManager. The internal method DeleteComment, is used for deleting comments. It takes an argument as commentID. The SocialComment class has a property CommentID that returns an ID of a comment, but that also is internal. So, you would need reflection for getting value of this property also. The code looks like this:

 public void DeleteComment(string userAccountName, string pageUrl)
{

    using (SPSite site = new SPSite(pageUrl))
    {
        SPServiceContext serviceContext = SPServiceContext.GetContext(site);
        UserProfileManager upm = new UserProfileManager(serviceContext);
        UserProfile user = upm.GetUserProfile(userAccountName);

        SocialCommentManager scManager = new SocialCommentManager(serviceContext);

        Uri currUri = FixupPageUrl(new Uri(pageUrl));

        SocialComment[] scList = scManager.GetComments(user, currUri);
        // get the latest comment in the list, comments list is returned in 
        // descending order of modified date
        SocialComment sc = scList[0];

        PropertyInfo propertyInfo = typeof(SocialComment).GetProperty("CommentID",
            BindingFlags.NonPublic | BindingFlags.Instance);

        object o = propertyInfo.GetValue(sc, null);

        if (o != null)
        {
            Type type = scManager.GetType();

            MethodInfo mi = typeof(SocialCommentManager).GetMethod("DeleteComment",
                BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                new Type[] { typeof(long) },
                null);

            long id = Convert.ToInt64(o.ToString());
            mi.Invoke(scManager, new object[] { id });
        }

    }
}

I’ve already explained most of the code above except method FixupPageUrl, which brings us to our next challenge.

Welcome Page Comments

SharePoint stores each social comment in social DB against the URL of the page, on which comment has been given. However, if the page is welcome page of the site, it stores the URL of site instead of the actual page. For example, if you have some comments on the welcome page of your site: https://sitename/pages/default.aspx and you pass this URL to GetComments method, it would not return any result, because SharePoint would have stored those comments against the site URL: https://sitename. The method FixupPageUrl addresses this issue. It returns the site URL if the passed URL is a welcome page. OOB Note Board web part also uses the same method. It is available in an internal class SocialItemUrlNormalizer. You can either use reflection to call this method or paste the method code into your calls using reflector. The code look like this:

 private Uri FixupPageUrl(Uri url)
{
    SPSecurity.CodeToRunElevated secureCode = null;
    string strWelcomePage;
    if (url != null)
    {
        strWelcomePage = string.Empty;
        try
        {
            if (secureCode == null)
            {
                secureCode = delegate
                {
                    using (SPSite site = new SPSite(url.AbsoluteUri))
                    {
                        using (SPWeb web = site.OpenWeb())
                        {
                            strWelcomePage = web.RootFolder.WelcomePage;
                            if (!string.IsNullOrEmpty(strWelcomePage))
                            {
                                strWelcomePage = SPHttpUtility.UrlPathDecode(strWelcomePage, false);
                                strWelcomePage = SPHttpUtility.UrlPathEncode(strWelcomePage, false, true);
                            }
                        }
                    }
                };
            }
            SPSecurity.RunWithElevatedPrivileges(secureCode);
        }
        catch
        {
        }
        if (!string.IsNullOrEmpty(strWelcomePage))
        {
            string absoluteUri = url.AbsoluteUri;
            int startIndex = absoluteUri.LastIndexOf(strWelcomePage, StringComparison.OrdinalIgnoreCase);
            if (startIndex != -1)
            {
                url = new Uri(absoluteUri.Remove(startIndex));
            }
        }
    }

    return url;
}
Paging

You would have to use your own logic for paging also. The SocialCommentManager exposes a GetComments overloaded method, which takes arguments that would enable you to build paging: Number of Items to be displayed on the page and the start index.

The key thing to remember here is that use SocialCommentManager.GetCount() method to get total number of comments to calculate number of pages required to display all comments. If you use Length property of SocialComment[] returned by GetComments to get total number of pages, you might get into the issue, which is discussed in the next challenge.

Deleted User Profiles

When a user profile gets deleted from the user profile database (for example, after a user leaves the company), social comments added by that user are not deleted from the Social DB. However, the object model, does not return any comments added by such deleted profiles. Hence, Length property of SocialComment[] returned by GetComments can be lesser than that value returned by GetCount method. This happens because GetCount value also contains the comments by deleted user profiles whereas GetComments does not.  If you has used GetComments method to get total comments for paging, your Next button can get disabled even before showing all the comments. At the time of writing this was a bug in OOB Note Board web part, and is expected to get fixed soon.

The above issue won’t happen if you use GetCount method to get total comments. However, you might have other issue here. For example, if you are showing 5 comments on each page, you might have intermediate pages which would display less than 5 comments. This would happen because the comments not displayed would be the comments from delete user profiles. As this is still better than not showing all comments, I prefer this one.  

Technorati Tags: Note Board,SocialCommentManager,Social Security Trimming,SharePoint 2010