Saturday, January 30, 2010

How Sharepoint sets CurrentThread.CurrentUICulture depending on Language of SPWeb

There are 2 properties in SPWeb class which can be used for localization purposes:

  1. Language – used for UI localization
  2. Locale – affects regional settings like date time, currency formatting, etc.

Sharepoint uses these properties in order to set CurrentUICulture and CurrentCulture of current thread when request is executed in context of Sharepoint site. SPWeb.Language property of current site is used to set CurrentThread.CurrentUICulture and SPWeb.Locale is used to set CurrentThread.CurrentCulture. In this post I’m going to describe internal mechanisms used by Sharepoint when it initializes localization properties of current thread.

The main class which should be considered here is SPRequestModule class which represents http module. This module exists in web.config of each web application running under Sharepoint:

   1:  
   2:  
   3: <httpModules>
   4:   <clear />
   5:   <add name="SPRequest"
   6: type="Microsoft.SharePoint.ApplicationRuntime.SPRequestModule,
   7: Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral,
   8: PublicKeyToken=71e9bce111e9429c" />
   9:   ...
  10: </httpModules>

Also important point is that SPRequestModule is located on 1st place in list of http modules so it will be executed 1st in pipeline. SPRequestModule makes many important things (e.g. it registers SPVirtualPathProvider) and exactly this module initializes CurrentUICulture and CurrentCulture properties of current thread. Lets look at SPRequest.Init() method:

   1: void IHttpModule.Init(HttpApplication app)
   2: {
   3:     if (app is SPHttpApplication)
   4:     {
   5:        ...
   6:     }
   7:     else
   8:     {
   9:         return;
  10:     }
  11:     app.BeginRequest +=
  12:         new EventHandler(this.BeginRequestHandler);
  13:     app.PostResolveRequestCache +=
  14:         new EventHandler(this.PostResolveRequestCacheHandler);
  15:     app.PostMapRequestHandler +=
  16:         new EventHandler(this.PostMapRequestHandler);
  17:     app.ReleaseRequestState +=
  18:         new EventHandler(this.ReleaseRequestStateHandler);
  19:     app.PreRequestHandlerExecute +=
  20:         new EventHandler(this.PreRequestExecuteAppHandler);
  21:     app.PostRequestHandlerExecute +=
  22:         new EventHandler(this.PostRequestExecuteHandler);
  23:     app.AuthenticateRequest +=
  24:         new EventHandler(this.AuthenticateRequestHandler);
  25:     app.PostAuthenticateRequest +=
  26:         new EventHandler(this.PostAuthenticateRequestHandler);
  27:     app.Error +=
  28:         new EventHandler(this.ErrorAppHandler);
  29:     app.EndRequest +=
  30:         new EventHandler(this.EndRequestHandler);
  31: }

As you can see it checks that application is Sharepoint http application (SPHttpApplication), makes some initialization and subscribes to number of application events. We are interesting currently only in PreRequestHandlerExecute event as exactly in handler of this event Sharepoint initializes localization properties of current thread. Lets looks inside PreRequestExecuteAppHandler:

   1:  
   2: private void PreRequestExecuteAppHandler(object oSender, EventArgs ea)
   3: {
   4:     if (this.RequestPathIndex != PathIndex._layouts)
   5:     {
   6:         if (s_isAdminWebApp &&
   7:             (this.RequestPathIndex == PathIndex._admin))
   8:         {
   9:             this._adminApp.Application_PreRequestHandlerExecute(oSender, ea);
  10:         }
  11:     }
  12:     else
  13:     {
  14:         this._layoutsApp.Application_PreRequestHandlerExecute(oSender, ea);
  15:     }
  16: }

So Sharepoint executes PreRequestHandlerExecute only if request goes to application _layout pages or if current request goes to Central Administration. This is interesting moment that for publishing pages (also known as site or content pages) PreRequestHandlerExecute is not called and as result CurrentUICulture and CurrentCulture properties of current thread are not set here (for publishing pages these properties are initialized in another place - I will describe it in another post. Update 2010-03-20: see http://sadomovalex.blogspot.com/2010/03/set-locale-of-current-thread-for.html).

Finally lets look inside _layoutsApp.Application_PreRequestHandlerExecute() method which is called for _layouts pages:

   1: internal void Application_PreRequestHandlerExecute(object sender, EventArgs e)
   2: {
   3:     SPWeb contextWeb;
   4:     ...
   5:     contextWeb = SPControl.GetContextWeb(application.Context);
   6:     ...
   7:     SPUtility.SetThreadCulture(contextWeb);
   8: }

Here Sharepoint retrieves SPWeb contextWeb for current request and calls SPUtility.SetThreadCulture(contextWeb) method which in turn calls the following method:

   1: internal static void SetThreadCulture(SPWeb spWeb, bool force)
   2: {
   3:     if (spWeb != null)
   4:     {
   5:         CultureInfo locale = spWeb.Locale;
   6:         CultureInfo uiCulture = new CultureInfo((int) spWeb.Language);
   7:         SetThreadCulture(locale, uiCulture, force);
   8:     }
   9: }
  10:  

And finally we come to the last method which initializes CurrentUICulture and CurrentCulture properties of current thread:

   1: internal static void SetThreadCulture(CultureInfo culture, CultureInfo uiCulture, bool force)
   2: {
   3:     HttpContext current = HttpContext.Current;
   4:     if ((force || (current == null)) || (current.Items[IsThreadCultureSet] == null))
   5:     {
   6:         if (!Thread.CurrentThread.CurrentCulture.Equals(culture))
   7:         {
   8:             Thread.CurrentThread.CurrentCulture = culture;
   9:         }
  10:         if (!Thread.CurrentThread.CurrentUICulture.Equals(uiCulture))
  11:         {
  12:             Thread.CurrentThread.CurrentUICulture = uiCulture;
  13:         }
  14:         if (!force && (current != null))
  15:         {
  16:             current.Items[IsThreadCultureSet] = true;
  17:         }
  18:     }
  19: }

This is the way Sharepoint uses to initialize localization properties of current thread depending on localization properties of current SPWeb.

Monday, January 25, 2010

Open SPSite under RunWithElevatedPrivileges in proper zone

Sometimes in Sharepoint development we need to run some code under SPSecurity.RunWithElevatedPrivileges() in order to execute it under System account. As you know if we use SPSecurity.RunWithElevatedPrivileges() we should reopen SPSite object in delegate passed to this method:

   1: SPSecurity.RunWithElevatedPrivileges(() =>
   2: {
   3:     using (var site = new SPSite(SPContext.Current.Site.ID))
   4:     {
   5:         ...
   6:     }
   7: });

This code works well if you have single zone configured for your web application (you can see zones for web applications in Central Administration -> Application Management -> Authentication Providers). But if you have several authentication zones for single web application it can lead to troubles: Sharepoint will always open SPSite in Default zone. I.e. even if code in example above is executed under Internet zone with FBA (e.g. under http://www.example.com) it will open SPSite object for Default zone which may use windows authentication (e.g. http://example). It in turn may lead to unclear bugs like Access denied, Operation is not valid due to the current state of object, etc.

In order to avoid this trouble use another constructor of SPSite class which receives additional parameter of type SPUrlZone:

   1: SPSecurity.RunWithElevatedPrivileges(() =>
   2: {
   3:     using (var site = new SPSite(SPContext.Current.Site.ID,
   4: SPContext.Current.Site.Zone))
   5:     {
   6:         ...
   7:     }
   8: });

With this code SPSite will be opened in proper zone.

Saturday, January 23, 2010

Runtime evaluation of lambda expressions

During working over Camlex.NET project in order to provide support of non-constant expressions we needed evaluate sub-expressions in runtime as results of these expression are required to construct CAML query. I.e. suppose that we have expression of the following general schema:

   1: x => (Type)x[expr1] == expr2

Here x is a parameter of lambda expression, Type – type allowed for casting in Camlex, and expr1 and expr2 – some expressions which should be evaluated in runtime. Expr1 and expr2 can be for example variables, method calls, ternary operator, etc.. For example:

   1: Func<object> expr1 = () => "Title";
   2: Func<object> expr2 = () => "Camlex";
   3:  
   4: var caml =
   5:     Camlex.Query()
   6:         .Where(x => (string)x[(string)expr1()] == expr2()).ToString();

The expression above will be translated in the following CAML:

   1: <Where>
   2:   <Eq>
   3:     <FieldRef Name="Title" />
   4:     <Value Type="Text">Camlex</Value>
   5:   </Eq>
   6: </Where>

As shown here results of expr1 and expr2 (“Title” and “Camlex”) are used for CAML query creation. So how do we evaluate exp1 and expr2:

   1:  
   2: private object evaluateExpression(Expression expr)
   3: {
   4:     // need to add Expression.Convert(..) in order to define Func<object>
   5:     var lambda =
   6: Expression.Lambda<Func<object>>(Expression.Convert(expr, typeof(object)));
   7:     return lambda.Compile().Invoke();
   8: }

First of all we need to represent expr1 and expr2 as Expression object using expression trees. In order to evaluate this expression we need to convert it to lambda expression () => expr which returns “object” type. Here little trick is made: we construct new unary expression which represents casting to object:

   1: Expression.Convert(expr, typeof(object))

Whatever actual expression was in expr1 or expr2, now the final operation for these expressions will be casting to object: (object)expr1 (of course it is not so efficiency if expressions return value type because of boxing/unboxing, but it will allow us to invoke expression in runtime). I.e. we added new operation to existing expressions.

Now it is not so hard to evaluate this expression. We create lambda expression: () => (object)expr1 with the following code:

   1:  
   2: var lambda =
   3: Expression.Lambda<Func<object>>(Expression.Convert(expr, typeof(object)));
And then we call Compile()  and Invoke() methods to get results. After that these results are used to create CAML query.

Wednesday, January 20, 2010

Camlex.NET for Sharepoint released

About a month ago I announced (see this post) Camlex.NET project. Today we (I and my friend and colleague Vladimir Timashkov – his blog) would like to announce the release of Camlex.NET project. You can download v. 1.0 from codeplex: http://camlex.codeplex.com. As said on official site Camlex.NET is a new tool for Sharepoint developers which allows to use lambda expressions and fluent interfaces in order to write dynamic CAML queries. By dynamic I mean queries which are built in runtime (although Camlex.NET can be used also to write static queries – i.e. queries which are created once and never modified). I.e. with Camlex you can do things like this:

image

Start page on codeplex contains quick start guide with examples of use cases. Also Documentation page contains deeper description of some Camlex features.

Currently we are most interested in community feedback on Camlex in order to add new features and fix found bugs. There are also some points in the plans for the future releases:

  • Add support for float, double and decimal native data types
  • Add more features available in CAML (e.g. <ProjectProperty> element inside <Value> tag) and which are not released in 1st version
  • SPMetal integration
  • what else?

If you think that some feature can be convenient for Sharepoint developers who will use Camlex – feel free to add comments here or on Discussion board on codeplex site http://camlex.codeplex.com.

Thursday, January 14, 2010

Anonymous access to folders with unique permissions in Sharepoint lists

Folders (SPFolder objects) in Sharepoint lists and document libraries can be used to restrict access to its content. I.e. you can assign different permissions to folders so users which have access to one folder will not have access to another. This is quite convenient solution. When you change folder permission settings you have to break role inheritance for this folder so it will not anymore inherit permissions from parent list:

   1: SPFolder folder = …;
   2: AssignRoleToSecurableObject(web, folder.Item, SPRoleType.Contributor, “Contributors”);
   3:  
   4: public static void AssignRoleToSecurableObject(SPWeb web, ISecurableObject securableObject,
   5:     SPRoleType roleType, string group)
   6: {
   7:     var siteGroup = web.SiteGroups[group];
   8:     var roleAssignment = new SPRoleAssignment(siteGroup);
   9:     var roleDefinition = web.RoleDefinitions.GetByType(roleType);
  10:     roleAssignment.RoleDefinitionBindings.Add(roleDefinition);
  11:     if (!securableObject.HasUniqueRoleAssignments)
  12:     {
  13:         securableObject.BreakRoleInheritance(true);
  14:     }
  15:     securableObject.RoleAssignments.Add(roleAssignment);
  16: }

Unfortunately there is an upsetting side effect - Sharepoint doesn’t allow anonymous users to access content inside folders with unique permissions (see http://yvonneharryman.wordpress.com/2007/11/23/follow-up-on-anonymous-access-and-item-level-permissions-from-sharepoint-connections-07). So what should we do if we have requirement that anonymous access has to be allowed to these folders?

In order to avoid this limitation of Sharepoint I made the following workaround: add special account into AD (lets call it anonymous@example.com). Then I force Sharepoint to use this account when non-authenticated request goes inside the folder with unique permissions. The main points of solution are the following:

  1. Add predefined account anonymous@example.com into AD
  2. All folders with unique permissions which should be available for anonymous are configured so anonymous@example.com account has Reader permission set on them
  3. Implement custom HttpModule which analyzes the target URL of http request. If request goes to the folder with unique permissions HttpModule temporary authenticates request with anonymous@example.com. As folders are accessible for this account (see step 2) Sharepoint successfully authorizes this “fake” anonymous request
  4. At the end of request we “rollback” temporary authentication – so other surface of the site is not touched and works by standard way

The simplified code of HttpModule is the following:

   1: namespace CustomHttpModules
   2: {
   3:     // This module authenticates anonymous user to predefined account
   4:     // when anonymous user requests for folder with unique permissions
   5:     public class AnonymAuthenticationModule : IHttpModule
   6:     {
   7:         private const string ANONYMOUS = "anonymous@example.com"
   8:  
   9:         public void Init(HttpApplication application)
  10:         {
  11:             application.PostAuthenticateRequest += application_PostAuthenticateRequest;
  12:             application.EndRequest += application_EndRequest;
  13:         }
  14:  
  15:         void application_EndRequest(object sender, EventArgs e)
  16:         {
  17:             if (!HttpContext.Current.Request.IsAuthenticated)
  18:                 return;
  19:  
  20:             // if current request was temporary authenticated
  21:             // by predefined anonymous account, call SignOut()
  22:             // in order to continue to work with portal as real
  23:             // anonymous
  24:             if (HttpContext.Current.User.Identity.Name == ANONYMOUS)
  25:             {
  26:                 FormsAuthentication.SignOut();
  27:             }
  28:         }
  29:  
  30:         void application_PostAuthenticateRequest(object sender, EventArgs e)
  31:         {
  32:             if (HttpContext.Current.Request.IsAuthenticated)
  33:                 return;
  34:  
  35:             if (this.shouldAnonymousBeAuthenticated(HttpContext.Current.Request.Url))
  36:             {
  37:                 // authenticate anonym user with special account.
  38:                 // From this point anonymous users will be authenticated
  39:                 FormsAuthentication.SetAuthCookie(ANONYMOUS, false);
  40:  
  41:                 // set auth cookies is not enough. Current request
  42:                 // is still not authenticated. To authenticate request
  43:                 // just redirect response to the same url. As we have
  44:                 // cookies already, this second request will be
  45:                 // authenticated
  46:                 HttpContext.Current.Response.Redirect(HttpContext.Current.Request.Url.OriginalString);
  47:             }
  48:         }
  49:  
  50:  
  51:         // Returns true if request goes to folder with unique
  52:         // permissions
  53:         private bool shouldAnonymousBeAuthenticated(Uri uri)
  54:         {
  55:            // implement your own logic here
  56:             ...
  57:         }
  58:     }
  59: }

It analyzes request URL (method shouldAnonymousBeAuthenticated()) and based on it authenticates request or leaves it untouched.

Notice that described technique works only for http get verbs. I.e. it wont be work if postbacks should be supported. For last case instead of simple Response.Redirect() you can try to construct HttpWebRequest inside http module and reexecute it with authentication cookies.