Archive

Posts Tagged ‘SharePoint 2010’

SharePoint: Resolve user through the particular Membership Provider

April 28th, 2014 Admin No comments

    We developed a few SharePoint-based applications comprised of two parts: internal and public. The internal one is accessible for Domain users only, while the public one points to the Internet and virtually available for everyone. Each of the applications uses the Claims Based Authentication and is extended to have two zones: the Default zone represents the internal part, while the Internet zone is for the public one. The Claims Based Authentication of the Default and Internet zones operates over the NTLM Integrated Windows Authentication and the Forms Based Authentication (FBA), respectively. For FBA we used our custom Membership provider derived from the SqlMembershipProvider and Role manager derived from the SqlRoleProvider, while the users’ email addresses served as the logins to sign in to the system. The problem came out when users having a Domain account had used their Domain emails to register and sign in to the system through the public part.

Let’s say there is a Domain user SOMEDOMAIN\firstname.lastname with the email lastname@somedomain. The user decides to test our application and registers in the public part, entering his email lastname@somedomain. During the registration the code similar to the listed below is being performed:

string userEmail    = "lastname@somedomain";
string userPassword = "1234567";
string providerName = "SomeCustomProvider";
...
SqlMembershipProvider customProvider =
                           Membership.Providers[providerName] as SqlMembershipProvider;
...
MembershipCreateStatus membershipCreateStatus;
MembershipUser membershipUser = customProvider.CreateUser(userEmail,
									userPassword, userEmail,
									null, null, true, Guid.NewGuid(),
									out membershipCreateStatus);
...
if (membershipUser != null && membershipCreateStatus == MembershipCreateStatus.Success)
{
	SPWeb spWeb = ...;
	...
	SPUser spUser = spWeb.EnsureUser(membershipUser.UserName);
	...
	// add user to a SharePoint group
	SPGroup publicUsersGroup = ...;
	publicUsersGroup.AddUser(spUser);
	...
	// give spUser unique permissions and so on
}

So, right after the customProvider.CreateUser has been called, we still have the Domain account SOMEDOMAIN\firstname.lastname with the email lastname@somedomain plus we have a new user with the login lastname@somedomain and the identical email. The latter one is stored in database and managed by the custom membership provider.

To get the proper instance of SPUser, we call EnsureUser (it else could be SafeUserEnsure) and pass user’s login which, in the given case, is actually the user’s email. Behind the scenes SharePoint attempts to sequentially resolve the user through all of available membership providers. Besides our custom membership provider, there is another one provided by SharePoint itself, namely, SPClaimsAuthMembershipProvider (Microsoft.SharePoint.Administration.Claims.SPClaimsAuthMembershipProvider defined in Microsoft.SharePoint) which is the default provider (when you add your custom membership provider to the web.config files it’s strongly recommended to keep the SPClaimsAuthMembershipProvider as a default provider to avoid unexpected behaviour). Apparently the default provider is the first one resorted to resolve the user. The pitfall here is that the SPClaimsAuthMembershipProvider finds and returns the Domain user (evidently in this case the user is resolved by email). So, following our example, the received SPUser will reference the SOMEDOMAIN\firstname.lastname with the email lastname@somedomain. Having gotten the wrong SPUser, we add it to groups and grant some permissions. Of course, once the registered user tries to sign in to the public part of the application, he stumbles upon Access Denied error as he doesn’t have any permissions since all required ones were provided to the Domain user. So, below is how to resolve this ambiguity.

If we take a look at how the user registered through the custom membership provider looks like in the SharePoint we’ll see something like the following:

i:0#.f|somecustomprovider|lastname@somedomain

More information regarding this format of Claims you’ll find here. The most valuable fact for us right now is that such encoded user name contains the name of the membership provider which manages the user (following the example above, it’s somecustomprovider). Obviously, if we pass such encoded user name into EnsureUser, the right membership provider will be applied and, therefore, the right SPUser will be returned. So, let’s find a way to turn the usual user name (lastname@somedomain) into the encoded one (i:0#.f|somecustomprovider|lastname@somedomain).

Such transformation might be easily done by means of SPClaimProviderManager that allows managing available claims providers and supplies various utility methods to encode and decode claims. So, the method below accepts usual user name and name of membership provider and returns the encoded user name:

public static string GetFbaEncodedUserName(string userName, string membershipProviderName)
{
	if (!SPClaimProviderManager.IsEncodedClaim(userName))
	{
		SPClaim claim = SPClaimProviderManager.CreateUserClaim(userName,
											SPOriginalIssuerType.Forms,
											membershipProviderName);
		return claim.ToEncodedString();
	}
	return userName;
}

The method creates a claim and returns its encoded representation. The Fba in the method’s name implies that we deal with the Form Based Authentication hence the SPOriginalIssuerType.Forms type is passed to the SPClaimProviderManager.CreateUserClaim.

The code below illustrates how to use the method:

string userName     = "lastname@somedomain";
string providerName = "SomeCustomProvider";

// get the user name resembling i:0#.f|somecustomprovider|lastname@somedomain
string encodedUserName = GetFbaEncodedUserName(userName, providerName);

SPWeb spWeb;

// get the right SPUser instance
SPUser spUser = spWeb.EnsureUser(encodedUserName);

SharePoint: How to add a new SPGroup through the code

March 9th, 2014 Admin No comments

    The below listed CreateSpGroup methods along with a few subsidiary ones could be used to add a new SPGroup to a SPWeb

public static SPGroup CreateSpGroup(SPWeb spWeb, string groupName,
                                    string description, bool isAssociated)
{
  SPGroup res = GetGroupByName(spWeb, groupName);
  if (res == null)
  {
    spWeb.SiteGroups.Add(groupName, spWeb.Site.SystemAccount, null, description);
    res = spWeb.SiteGroups[groupName];

    if (isAssociated)
    {
      spWeb.AssociatedGroups.Add(res);

      const string createdGroupsProperty = "vti_createdassociategroups";
      var createdAssociatedGroupsStr = spWeb.AllProperties[createdGroupsProperty] as string;
      if (!string.IsNullOrEmpty(createdAssociatedGroupsStr))
      {
        var createdAssociatedGroups = ParseAssociateGroupsFromString(createdAssociatedGroupsStr);
        if (!createdAssociatedGroups.Contains(res.ID))
        {
          createdAssociatedGroups.Add(res.ID);
          spWeb.AllProperties[createdGroupsProperty] =
                               ConvertAssociateGroupsToString(createdAssociatedGroups);
        }
      }
      spWeb.Update();
    }
  }
  return res;
}

public static SPGroup CreateSpGroup(SPWeb spWeb, string groupName)
{
	return CreateSpGroup(spWeb, groupName, string.Empty, false);
}

public static List<int> ParseAssociateGroupsFromString(string str)
{
  var list = new List<int>();
  if (!string.IsNullOrEmpty(str))
    foreach (string str2 in str.Split(new[] { ';' }))
    {
        int num2;
        if ((!string.IsNullOrEmpty(str2) &&
            int.TryParse(str2, NumberStyles.Integer, CultureInfo.InvariantCulture, out num2)) &&
                    ((num2 > 0) && !list.Contains(num2)))
          list.Add(num2);
    }
  return list;
}

public static string ConvertAssociateGroupsToString(List<int> groupIds)
{
	var builder = new StringBuilder();
	for (int i = 0; i < groupIds.Count; i++)
	{
		if (builder.Length > 0)
			builder.Append(';');
		builder.Append(groupIds[i].ToString(CultureInfo.InvariantCulture));
	}
	return builder.ToString();
}

public static SPGroup GetGroupByName(SPWeb spWeb, string groupName)
{
	foreach (SPGroup group in spWeb.SiteGroups)
		if (groupName.Equals(group.Name, StringComparison.OrdinalIgnoreCase))
			return group;
	return null;
}

At first the CreateSpGroup method makes sure whether the target group doesn’t exist by calling the GetGroupByName. If such group has been found, it’s returned, otherwise a new empty group is being created. The SPWeb.Site.SystemAccount aka Site Collection administrator will be the owner of the new group.

The isAssociated flag indicates if the group has to be associated with the site. Let’s linger on associated groups a bit longer.

When a site (or sub-site with unique permissions) is created, three default groups associated with the site become available. Their names usually are “YourSiteName Owners”, “YourSiteName Members” and “YourSiteName Visitors”, where the YourSiteName is the name you gave to your site. You can see them in the Groups section at Quick Launch area when going to the “People and Groups” page (Site Actions -> Site Settings -> People and groups). Through the code those groups are accessible as SPWeb.AssociatedOwnerGroup, SPWeb.AssociatedMemberGroup and SPWeb.AssociatedVisitorGroup respectively. We also can get their IDs from the AllProperties collection of a SPWeb instance like SPWeb.AllProperties["vti_associateownergroup"], SPWeb.AllProperties["vti_associatemembergroup"] and SPWeb.AllProperties["vti_associatevisitorgroup"] respectively. We are able to associate our own custom groups with the site by adding them to the SPWeb.AssociatedGroups collection, so that our groups would appear in the Quick Launch at the “People and Groups” page. So, that’s the first thing the CreateSpGroup does in case the isAssociated is set to true.

One more often-mentioned feature of associated groups is that they allegedly can be deleted automatically when the site/sub-site they are associated with is being deleted. Here, however, there is a pitfall: all groups are SiteCollection-level objects, therefore removing the site/sub-site, the groups (including associated ones) are still available as they are linked to the site collection. SharePoint, however, treats the associated default groups in a special way, namely the groups specified in the SPWeb.AssociatedOwnerGroup, SPWeb.AssociatedMemberGroup and SPWeb.AssociatedVisitorGroup will be actually deleted with the site/sub-site jointly. As regards custom associated groups, in the general case, they will never be automatically deleted with the site. So, don’t count on it. If, however, you are still eager to make your custom group deletable by default, you don’t have any choice but to override one of the associated default groups with the custom one. That’s not a complete solution though, you also have to add the id of the custom group to the spWeb.AllProperties["vti_createdassociategroups"]. I came to this, having reviewed such methods as Microsoft.SharePoint.Utilities.SPUtilityInternal.CreateDefaultSharePointGroups and Microsoft.SharePoint.ApplicationPages.DeleteWebPage.OnLoad by means of .Net Reflector. So, in case the isAssociated is set to true, the second thing the CreateSpGroup does is adding group’s id to the “vti_createdassociategroups” property. After that the only step you have to do to make your custom group deletable is something like this

spWeb.AssociatedVisitorGroup = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);
//spWeb.AssociatedMemberGroup  = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);
//spWeb.AssociatedOwnerGroup   = CreateSpGroup(spWeb, "my own custom group", string.Empty, true);

Note that, doing that, the overridden default group still remains and is linked to the Site Collection. In case it is not needed any more, remove it. So, before overriding the default group, you’d better save the reference to it and then, after overriding, remove it by calling SPWeb.SiteGroups.Remove.

Nevertheless I suggest you not override associated default groups as you are always able to customize them the way you need instead: change name and description, add users you want and so on.

One more important thing to be aware of is, deleting a site/sub-site, you likely just move it to the Site Collection Recycle Bin (unless the Recycle Bin is off). That means the associated default groups will not be deleted right away, they will be deleted only at that moment when the site is being erased from the Recycle Bin. Unlike the site they are associated with, the groups will not be moved to the Recycle Bin. Moreover SharePoint doesn’t even mark them as “being deleted”. Thus during the Recycle Bin retention period (30 days by default) the associated default groups, being the SiteCollection-level objects, remain available and can be used by someone else. So, somebody who rashly uses the default groups of the “deleted” site may be unpleasantly surprised some day as the groups suddenly disappear. To avoid this, I personally (of course if I have no doubts that the site/sub-site has to vanish completely) erase it from Recycle Bin right away or ask Site Collection administrators to do this as soon as possible.

Let’s go back to the rest of the methods listed above. The ParseAssociateGroupsFromString method parses the value of vti_createdassociategroups into an array of groups’ IDs. The ConvertAssociateGroupsToString, in turn, makes inverse operation and turns the array into a string to save it in the vti_createdassociategroups property. The format of the vti_createdassociategroups is very simple, it’s a set of groups’ IDs separated with semicolons like 1;2;3. Nothing is easier than to implement parse of such string. I, however, decided to use the methods found in SharePoint‘s interior. So, ParseAssociateGroupsFromString and ConvertAssociateGroupsToString are internal methods defined in the Microsoft.SharePoint.ApplicationPages.PeopleGroupPageBase and captured by .Net Reflector.

The GetGroupByName is intended to look for a group with a specified name and used to check if the group about to be created exists.

As I said before, the groups are SiteCollection-level objects, so groups have to be created under privileged account at least under Site Collection administrator. So, the CreateSpGroup could be called as shown below:

// use of system account
string siteUrl = "some site url";
SPUserToken spUserToken = GetSystemToken(siteUrl);
using (SPSite spSite = new SPSite(siteUrl, spUserToken))
{
	using (SPWeb spWeb = spSite.OpenWeb())
	{
		SPGroup newGroup = CreateSpGroup(spWeb, "new group", string.Empty, true);
		// do something with the new group
	}
}

*Note: the GetSystemToken method is described here – SharePoint: How to get SystemAccount token.

or

// use of application pool identity
SPSecurity.RunWithElevatedPrivileges(delegate
{
    using (SPSite spSite = new SPSite("some site url"))
        using (SPWeb spWeb = spSite.OpenWeb())
        {
            SPGroup newGroup = CreateSpGroup(spWeb, "new group", string.Empty, true);
            // do something with the new group
        }
});

SharePoint: How to create a custom REST WCF Service available in _layouts folder

August 8th, 2013 Admin 5 comments

    In the given blogpost I’ll describe step by step how to create a simple REST WCF Service residing in the _layouts folder. To create a demonstration project we’ll use Visual Studio 2012. I believe, however, the similar steps could be done in Visual Studio 2010 as well.

* Don’t look for a meaning in the source code of the demonstration project as its main goal is just to show how to communicate with WCF Services operating under SharePoint and how they return and accept parameters of standard and custom types.

* If you are too lazy to go through all steps, you can download the demonstration project right away and play with it :)


1. Create a SharePoint project in the Visual Studio 2012

* Note that if you plan to add the service to an existent project, just skip this step.

Click to see how to create the SharePoint project in details

  1. Go to FILE -> New -> Project and select the SharePoint 2010 Project available under Templates/Visial C#/SharePoint (see the image below). Type the name and location of the project. Let’s name it CustomRestService.
    Create SharePoint Project In Visual Studio 2012
  2. Click OK button, then the SharePoint Customization Wizard opens (see the image below). Type the proper URL of the local site for debugging and choose the Deploy as a farm solution option.
    SharePoint Customization Wizard
  3. Click Finish button. In Solution Explorer the created project resembles the following:
    SharePoint Project After Creation



2. Add a WCF Service to the project

Click to see how to add WCF Service to the project in details

  1. Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the WCF Service among available templates. It’s usually under the Visual C# Items. Alternatively you can find the required template by typing WCF in the search box (Search Installed Templates) at the right top corner (see the picture below). Let’s name the service CustomRestService as well.
    Add WCF Service to the project
  2. Click Add button. After the WCF Service has been added, the project should look like the following:
    SharePoint Project After WCF Service has been added
    There are 3 new files in the project:

    • ICustomRestService.cs, defines the interface supported by the service, i.e. specifies the methods available in the service;
    • CustomRestService.cs, provides the actual implementation of the service’s methods;
    • app.config, presets the WCF Service configuration;

    Delete the app.config from the project as we don’t actually need it.
    * If you don’t have the WCF Service template available in Visual Studio or you don’t want to use it for some reason, you can just add two .cs files, ICustomRestService.cs and CustomRestService.cs, to the project. In this case don’t forget to add references to such assemblies as System.Runtime.Serialization and System.ServiceModel.



3. Add references to System.ServiceModel.Web and System.Web assemblies to the project

Click to see how to add references to the project in details

  1. Ensure the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add Reference…. In the opened dialog, find the System.ServiceModel.Web and System.Web assemblies (usually they are located under Assemblies/Framework) and check the checkboxs against them (see the picture below).
    Add References to System.ServiceModel.Web and System.Web to the project
  2. Click OK button. The References at that stage for the project created from scratch should look like the following:
    References After Adding System.ServiceModel.Web and System.Web Assemblies
    The highlighted assemblies are essential for developing WCF Services operating under SharePoint.



4. Modify the service’s interface

Open the ICustomRestService.cs and bring it to the following state:

using System.ServiceModel;
using System.ServiceModel.Web;

namespace CustomRestService
{
    /// <summary>
    /// Wraps the service result and provides the error message (if any)
    /// </summary>
    public class ServiceResult<T>
    {
        public bool   Success      { get; set; }
        public string ErrorMessage { get; set; }
        public T      Data         { get; set; }
    }

    /// <summary>
    /// Represents basic info about a book
    /// </summary>
    public class Book
    {
        public int    Id     { get; set; }
        public string Title  { get; set; }
        public string Author { get; set; }
    }

    /// <summary>
    /// Service's interface
    /// </summary>
    [ServiceContract]
    public interface ICustomRestService
    {
        /// <summary>
        /// Checks if book is already registered in the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST",
            RequestFormat  = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<bool> BookExists(string bookTitle);

        /// <summary>
        /// Returns book by id
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST",
            RequestFormat  = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<Book> GetBook(int id);

        /// <summary>
        /// Adds the passed book to the system
        /// </summary>
        [OperationContract]
        [WebInvoke(Method  = "POST",
            RequestFormat  = WebMessageFormat.Json,
            ResponseFormat = WebMessageFormat.Json,
            BodyStyle      = WebMessageBodyStyle.WrappedRequest)]
        ServiceResult<int> AddBook(Book book);
    }
}

Here the ServiceResult is a simple class that wraps the service result and provides the error message in case it occurs. The Book class provides basic info about a book and is intended to show how REST WCF Service deals with custom types.


5. Modify the service implementation

Open the CustomRestService.cs and modify it so that it looks like the following:

using System;
using System.ServiceModel.Activation;
using System.Web;
using Microsoft.SharePoint;

namespace CustomRestService
{
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class CustomRestService : ICustomRestService
    {
        public ServiceResult<bool> BookExists(string bookTitle)
        {
            // allow access for anonymous users

            if (string.IsNullOrEmpty(bookTitle))
                return new ServiceResult<bool>()
                {
                    Success      = false,
                    ErrorMessage = "Book title is undefined!"
                };

            var result = new ServiceResult<bool>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPQuery spQuery = new SPQuery
                {
                    Query = "<Where><Eq><FieldRef Name='Title' /><Value Type='Text'>" +
                        bookTitle.ToLower() + "</Value></Eq></Where>",
                    ViewAttributes = "Scope='Recursive'"
                };

                SPListItemCollection books = spList.GetItems(spQuery);
                result.Data = books.Count > 0;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<Book> GetBook(int id)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<Book>()
                    {
                        Success      = false,
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (id <= 0)
                return new ServiceResult<Book>()
                    {
                        Success      = false,
                        ErrorMessage = "Invalid book id!"
                    };

            var result = new ServiceResult<Book>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.GetItemById(id);
                result.Data = new Book()
                {
                    Id     = spListItem.ID,
                    Title  = spListItem.Title,
                    Author = Convert.ToString(spListItem["BookAuthor"])
                };
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }

        public ServiceResult<int> AddBook(Book book)
        {
            // check if user is authenticated
            if (!HttpContext.Current.Request.IsAuthenticated)
                return new ServiceResult<int>()
                    {
                        Success      = false,
                        ErrorMessage = "Unauthenticated access!"
                    };

            if (book == null)
                return new ServiceResult<int>()
                    {
                        Success      = false,
                        ErrorMessage = "Invalid book!"
                    };

            // checking if the book is already presented is omitted

            var result = new ServiceResult<int>() { Success = true };

            try
            {
                SPList spList = SPContext.Current.Web.Lists["Books"];

                SPListItem spListItem = spList.AddItem();
                spListItem["Title"]      = book.Title;
                spListItem["BookAuthor"] = book.Author;

                SPContext.Current.Web.AllowUnsafeUpdates = true;
                spListItem.Update();
                SPContext.Current.Web.AllowUnsafeUpdates = false;

                result.Data = spListItem.ID;
            }
            catch (Exception ex)
            {
                result.Success      = false;
                result.ErrorMessage = ex.Message;
            }

            return result;
        }
    }
}

The implementation operates with the Books list and assumes that the list is available at the SharePoint Site in the boundaries of which the service is called. Note that we work with SharePoint objects in the same way as we would do this in code-behind of an .aspx-page.

The GetBook and AddBook methods require users to be authenticated before calling them, while the BookExists allows anonymous access (of course, in case it’s turned on for Web Application and Site). Since it’s a best practice, always put the checking whether user is authenticated and authorized in the beginning of every web method unless you have some contrary requirements (for example, to provide anonymous access).


6. Add Layouts mapped folder to the project

* Note that if you already have the folder, just skip this step.

Click to see how to add “Layouts” mapped folder to the project in details

    Ensure that the CustomRestService project is selected in Solution Explorer and then go to PROJECT -> Add SharePoint “Layouts” Mapped Folder. The Layouts folder along with the nested CustomRestService folder will be added to the project. For simplicity rename the nested CustomRestService to Wcf. After that the project should look as follows:
The project after the Layouts mapped folder has been added



7. Add .svc file to the Layouts folder

Click to see how to add .svc file to the Layouts folder in details

  1. Ensure that the Wcf folder nested in the Layouts is selected in Solution Explorer and then go to PROJECT -> Add New Item. In the opened dialog, locate the Text File among available templates. It’s usually under the Visual C# Items/General (see the image below). Name the file CustomRestService.svc.
    Add .svc file to the project
  2. Click Add button. After the .svc file has been added, the project should look like the following:
    The project after the .svc file has been added



8. Modify the .svc file

Open the CustomRestService.svc and bring it to the following:

<%@ ServiceHost Language="C#" Debug="true"
    Service="CustomRestService.CustomRestService, $SharePoint.Project.AssemblyFullName$"
    CodeBehind="CustomRestService.cs"
    Factory="Microsoft.SharePoint.Client.Services.MultipleBaseAddressWebServiceHostFactory,
             Microsoft.SharePoint.Client.ServerRuntime, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

The MultipleBaseAddressWebServiceHostFactory declares the REST type of the Web Service and creates proper endpoints with Web Bindings. The factory takes upon itself the configuring of the Web Service, that is why we don’t need the app.config deleted previously.

The Service attribute provides the Fully Qualified Name (including the assembly’s full name) of the class representing the service. Instead of putting in the proper full name of the assembly I used such replaceable parameter as $SharePoint.Project.AssemblyFullName$. When building the project, Visual Studio is supposed to replace the parameter with the full name of the output assembly. One nuisance here is that Visual Studio doesn’t perform replacement in .svc files by default. To make Visual Studio look into .svc, we have to manually alter the .csproj. So, open the project’s .csproj file in Notepad or other Notepad-like text editor, find the first <PropertyGroup> node, insert the following before the closing </PropertyGroup> and save the file:

<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>

* Note that, having modified the .csproj, you likely will be prompted by Visual Studio to reload the project.

After the alteration of the project’s file it should resemble the following (some tags and attributes are skipped for simplicity):

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" ...>
  <Import ... />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{16585D01-2995-48EF-8DFA-1371C9EF6EE9}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>CustomRestService</RootNamespace>
    <AssemblyName>CustomRestService</AssemblyName>
    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{BB1F664B-9266-4fd6-B973-E1E44974B511};...</ProjectTypeGuids>
    <SandboxedSolution>False</SandboxedSolution>
    <WcfConfigValidationEnabled>True</WcfConfigValidationEnabled>
	<TokenReplacementFileExtensions>svc</TokenReplacementFileExtensions>
  </PropertyGroup>
...
</Project>

* Note that for the project attached to the given article, the .csproj has been already modified.

Of course, instead of the $SharePoint.Project.AssemblyFullName$ you can use the real full name of your assembly. To get such name I usually use .Net Reflector. Alternatively you can make Visual Studio display the name, following instructions from the article How to: Create a Tool to Get the Full Name of an Assembly.


9. Build the project and deploy it

Right-click the CustomRestService project in Solution Explorer and then click Deploy. Behind the scenes, Visual Studio compiles the project, creates a .wsp file and deploys the package to the farm.


10. Test the service

  1. First of all try to open the service in a browser. You can use for that any SharePoint Site, just add the /_layouts/wcf/customrestservice.svc at the end of the URL. For example:
    
    http://someservername:1200/somesite/_layouts/wcf/customrestservice.svc
    

    If you see the message “Endpoint not found“, don’t worry, that’s what we need. That means all of the steps above have been done correctly.

  2. Let’s test the web methods of the service. As you remember, the service interacts with the Books list. It’s a list with two main fields: Title and BookAuthor, both are Single Line of Text (see the image below).
    Books SharePoint List
    * I do not cite the list’s schema here as you can find it (schema, instance, feature to create the instance) in the attached project.
    The test page named TestPage.aspx is added to the demonstration project as well. The page has been created as an Application Page, locates in the Layouts folder and contains one button that executes JavaScript functions to test each web method. Below is the shortened listing of the page to show how to call web methods using jQuery and Ajax:

    ...
    <%@ Page ... CodeBehind="TestPage.aspx.cs"
             Inherits="CustomRestService.Layouts.TestPage"
    		 DynamicMasterPageFile="~masterurl/default.master" %>
    
    <asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
     <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
    
     <script type="text/javascript">
      // makes POST json-request to a web method
      var getJsonData = function (url, param, callback, errorcallback) {
        $.ajax({
    	  url        : url,
    	  dataType   : "json",
    	  type       : "POST",
    	  contentType: 'application/json; charset=utf-8',
    	  data       : JSON.stringify(param),
    	  success    : function (data) {
    		  if (callback)
    			  callback(data);
    	  },
    	  error      : function (XMLHttpRequest, textStatus, errorThrown) {
    		  if (errorcallback)
    			  errorcallback(XMLHttpRequest, textStatus, errorThrown);
    	  }
        });
      };
    
      // makes request to the BookExists method
      function testBookExists() {
        getJsonData('Wcf/CustomRestService.svc/BookExists',
          { "bookTitle": 'the adventures of tom sawyer' },
      	  function (data) {
    		  if (data.Success)
    			  alert('Success. Result: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function(XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
    
      // makes request to the GetBook method
      function testGetBook() {
        getJsonData('Wcf/CustomRestService.svc/GetBook', { "id": 2 },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Title: ' + data.Data.Title + ' Author: ' + data.Data.Author);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
    
      // makes request to the AddBook method
      function testAddBook() {
        getJsonData('Wcf/CustomRestService.svc/AddBook',
          { "book": { "Author": "Mark Twain", "Title": "The Adventures of Huckleberry Finn" } },
    	  function (data) {
    		  if (data.Success)
    			  alert('Success. Book Id: ' + data.Data);
    		  else
    			  alert('Fail. Error: ' + data.ErrorMessage);
    	  },
    	  function (XMLHttpRequest, textStatus, errorThrown) {
    		  alert(errorThrown);
    	  });
      }
     </script>
    </asp:Content>
    
    <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
     <asp:Button ID="test" Text="Test Web Methods" runat="server"
       OnClientClick="testBookExists(); testGetBook(); testAddBook(); return false;" />
    </asp:Content>
    ...
    

    Pay attention to the URLs used to send request to web methods. The given test page is located directly in _layouts, so to access the Web Service placed in the _layouts/Wcf I use the relative URL Wcf/CustomRestService.svc. If your page dealing with the Web Service resides somewhere outside of _layouts or within the folders nested in _layouts you have to provide the proper URL (relative or not) to the Web Service.
    Open the test page in a browser, the URL should be like the following

    
    http://someservername:1200/somesite/_layouts/TestPage.aspx
    

    Click Test Web Methods button. If everything works fine you’ll get 3 alert messages and each of them starts with the word “Success”. The order of the alerts may vary as requests are asynchronous.

The demonstration project you can download here – CustomRestService.zip. Note that the archive contains the .wsp package in the CustomRestService\bin subfolder, use it in case you are not able to compile/build the project for some reason.

SharePoint: How to add Links and Headings to Quick Launch programmatically

July 25th, 2013 Admin 1 comment

    The Quick Launch is a collection of links usually available on every page in the left sidebar (of course, in case you use a standard master page and don’t open the page within a modal dialog), see the image below:

Add Heading to Quick Launch

The links are pretty easily manageable through the UI using Site Settings -> Quick Launch. There Headings and links can be added, removed or reordered. Besides UI, manipulations of the Quick Launch can be done through the SharePoint Object Model. For example, to add a link to the Quick Launch I usually use the following method:

public static SPNavigationNode AddLinkToQuickLaunch(SPNavigationNode parentNode,
                                        string title, string url, bool isExternal)
{
	SPNavigationNode node = new SPNavigationNode(title, url, isExternal);
	parentNode.Children.AddAsLast(node);

	// refresh navigation node just in case
	node = parentNode.Navigation.GetNodeById(node.Id);
	return node;
}

The parentNode here represents either an existent link or the root SPWeb.Navigation.QuickLaunch object.

Unlike UI where the New Heading option is presented (see the image above), SharePoint Object Model doesn’t provide any special method or node class that would be intended to add a Heading to the Quick Launch. However, we still can turn a usual SPNavigationNode into Heading. For that we have to set some properties available through the SPNavigationNode.Properties. The listing below demonstrates two methods allowing to add a Heading:

public static SPNavigationNode AddHeadingToQuickLaunch(SPWeb spWeb, string headingName)
{
	SPNavigationNodeCollection quicklaunchNav = spWeb.Navigation.QuickLaunch;

	SPNavigationNode headingNode =
                      new SPNavigationNode(headingName, "javascript:window.goback(0)", true);
	headingNode = quicklaunchNav.AddAsLast(headingNode);

	//turn the node into Heading
	TurnIntoHeading(headingNode);

	headingNode.Update();

	// refresh navigation node just in case
	headingNode = spWeb.Navigation.GetNodeById(headingNode.Id);
	return headingNode;
}
public static void TurnIntoHeading(SPNavigationNode node)
{
	node.Properties["NodeType"]             = "Heading";
	node.Properties["BlankUrl"]             = "True";

	node.Properties["LastModifiedDate"]     = DateTime.Now;
	node.Properties["Target"]               = "";
	node.Properties["vti_navsequencechild"] = "true";
	node.Properties["UrlQueryString"]       = "";
	node.Properties["CreatedDate"]          = DateTime.Now;
	node.Properties["Description"]          = "";
	node.Properties["UrlFragment"]          = "";
	node.Properties["Audience"]             = "";
}

Note that the given implementation makes the Heading void, i.e. clicking the Heading neither leads to another page nor refreshes the current one. That’s possible due to the javascript:window.goback(0) that is passed as a URL of the node. I consider Heading as a name of the group of subjacent links, therefore I prefer having void links for Headings.

SharePoint: SqlMembershipProvider – Lock User

July 21st, 2013 Admin No comments

    In addition to the article SharePoint: SqlMembershipProvider – Get All Users In Role, here is one more method to extend the SqlMembershipProvider with. It’s found out that the SqlMembershipProvider doesn’t provide a method to lock user. By default a user can be automatically locked after several frequent and failed attempts to login. To unlock such users the SqlMembershipProvider supplies with the UnlockUser method. But what if administrator wants to temporarily lock user for some reason? Unfortunately, there is no such method out-of-box.

So, let’s try to implement our own LockUser method. Two obvious steps for that are as follows: to create a Stored Procedure in database; to extend a class derived from the SqlMembershipProvider with the proper method.


LockUser Stored Procedure

The stored procedure is very simple as we need just to update one field in the aspnet_Membership table for appropriate user. Below is the script to create such procedure. Run the script on MembershipProvider database, in my case it’s aspnetdb.

USE [aspnetdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

-- =============================================
-- Author:      .Net Follower
-- Description:	Locks User
-- =============================================
CREATE PROCEDURE [dbo].[aspnet_Membership_LockUser]
	@ApplicationName                         nvarchar(256),
    @UserName                                nvarchar(256)
AS
BEGIN
	DECLARE @UserId uniqueidentifier
    SELECT  @UserId = NULL
    SELECT  @UserId = u.UserId
    FROM    dbo.aspnet_Users u, dbo.aspnet_Applications a, dbo.aspnet_Membership m
    WHERE   LoweredUserName = LOWER(@UserName) AND
            u.ApplicationId = a.ApplicationId  AND
            LOWER(@ApplicationName) = a.LoweredApplicationName AND
            u.UserId = m.UserId

    IF ( @UserId IS NULL )
        RETURN 1

    UPDATE dbo.aspnet_Membership
    SET IsLockedOut = 1 WHERE @UserId = UserId

    RETURN 0
END


Custom Membership Provider

Now we can add the LockUser method to the custom Membership Provider called SqlMembershipProviderEx and shown in the article. The SqlMembershipProviderEx with the LockUser is listed below. Note that the methods mentioned in the previous article are skipped.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Globalization;
using System.Reflection;
using System.Web.Security;
using Microsoft.SharePoint;

namespace dotNetFollower
{
    public class SqlMembershipProviderEx : SqlMembershipProvider
    {
	    ...

        public bool LockUser(string username)
        {
            bool flag = false;
            CheckParameter(ref username, true, true, true, 0x100, "username");

            DoInSqlConnectionContext(delegate(SqlConnection connection)
            {
                //this.CheckSchemaVersion(connection.Connection);
                SqlCommand command = new SqlCommand("dbo.aspnet_Membership_LockUser", connection)
                {
                    CommandTimeout = CommandTimeout,
                    CommandType    = CommandType.StoredProcedure
                };
                command.Parameters.Add(CreateInputParam("@ApplicationName", SqlDbType.NVarChar, ApplicationName));
                command.Parameters.Add(CreateInputParam("@UserName", SqlDbType.NVarChar, username));
                SqlParameter parameter = new SqlParameter("@ReturnValue", SqlDbType.Int)
                {
                    Direction = ParameterDirection.ReturnValue
                };
                command.Parameters.Add(parameter);

                command.ExecuteNonQuery();
                flag = ((parameter.Value != null) ? ((int)parameter.Value) : -1) == 0;
            });

            return flag;
        }

        protected internal static void CheckParameter(ref string param, bool checkForNull, bool checkIfEmpty, bool checkForCommas, int maxSize, string paramName)
        {
            if (param == null)
            {
                if (checkForNull)
                    throw new ArgumentNullException(paramName);
            }
            else
            {
                param = param.Trim();
                if (checkIfEmpty && (param.Length < 1))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not be empty.", new object[] { paramName }), paramName);

                if ((maxSize > 0) && (param.Length > maxSize))
                    throw new ArgumentException(string.Format("The parameter '{0}' is too long: it must not exceed {1} chars in length.", new object[] { paramName, maxSize.ToString(CultureInfo.InvariantCulture) }), paramName);

                if (checkForCommas && param.Contains(","))
                    throw new ArgumentException(string.Format("The parameter '{0}' must not contain commas.", new object[] { paramName }), paramName);
            }
        }

        ...

    }
}

The latest version of the SqlMembershipProviderEx along with all used additional classes are available to download here.

Related posts: