Friday, April 22, 2016

Get current Sharepoint list or document library via javascript object model

In server object model in Sharepoint there is simple way to get current list (i.e. list which is currently opened in browser window):

   1: var list = SPContext.Current.List;

In javascript object model it is not so obvious, but also possible. In order to get current list you have to use not very well known function GetCurrentCtx() defined in core.js. With this function we can get current list like this:

   1: var ctx = SP.ClientContext.get_current();
   2:  
   3: var currentCtx = GetCurrentCtx();
   4: var list = ctx.get_web().get_lists().getByTitle(currentCtx.ListTitle);
   5: ctx.load(list);
   6:  
   7: ctx.executeQueryAsync(
   8:     Function.createDelegate(this, function (sender, args) {
   9:         // use list here
  10:     }),
  11:     Function.createDelegate(this, function (sender, args) {
  12:         console.log("Error occured: " + args.get_message() }));

In this example the main difference between context created with SP.ClientContext.get_current() and context returned from GetCurrentCtx() is that last one has ListTitle property set to title of the current list or document library. Having it we can easily get list object from the web. Also this context contains other useful properties like listBaseType, listName, listTemplate and listUrlDir.

Tuesday, April 19, 2016

Avoid “Value does not fall within the expected range” error when read list item with taxonomy fields via javascript in Sharepoint Online

When list contain many taxonomy fields and you try to read list item with all these fields using CAML query in javascript:

   1: var ctx = SP.ClientContext.get_current();
   2: var list = ctx.get_web().get_lists().getByTitle("MyList");
   3: var query = new SP.CamlQuery();
   4: query.set_viewXml('<View>' +
   5:                       '<Query>' +
   6:                           '<Where>' +
   7:                           '</Where>' +
   8:                       '</Query>' +
   9:                       '<RowLimit>1</RowLimit>' +
  10:                   '</View>');
  11: var items = list.getItems(query);
  12: ctx.load(items, 'Include(Field1,Field2,Field3,...)');
  13: ctx.executeQueryAsync(Function.createDelegate(this, function (sender, args) { /* */ }),
  14:     Function.createDelegate(this, function (sender, args) { console.log("Error: " +
  15:         args.get_message() + "\n" + args.get_stackTrace()); }));

you may get the following error:

Value does not fall within the expected range error

It happens because of List Lookup Threshold Limit which is set to 12 in Sharepoint Online (see Increased site collection and list lookup limits). In order to avoid this error use the following workaround: at first read necessary item to get it’s id. Then load this item using List.getItemById method which will return list item containing all fields without need to explicitly enumerate them in “Include(…)” statement:

   1: var ctx = SP.ClientContext.get_current();
   2: var list = ctx.get_web().get_lists().getByTitle("MyList");
   3: var query = new SP.CamlQuery();
   4: query.set_viewXml('<View>' +
   5:                       '<Query>' +
   6:                           '<Where>' +
   7:                           '</Where>' +
   8:                       '</Query>' +
   9:                       '<RowLimit>1</RowLimit>' +
  10:                   '</View>');
  11: var items = list.getItems(query);
  12: ctx.load(items);
  13: ctx.executeQueryAsync(
  14:     Function.createDelegate(this, function (sender, args) {
  15:         getPropsSuccess(ctx, list, items);
  16:     }),
  17:     Function.createDelegate(this, function (sender, args) {
  18:         console.log("Error: " + args.get_message() + "\n" + args.get_stackTrace());
  19:     })
  20: );
  21:  
  22: function getPropsSuccess(ctx, list, items) {
  23:     var itemId = null;
  24:     var enumerator = items.getEnumerator();
  25:     while (enumerator.moveNext()) {
  26:         var item = enumerator.get_current();
  27:         itemId = item.get_id();
  28:         break;
  29:     }
  30:  
  31:     if (itemId == null) {
  32:         return;
  33:     }
  34:  
  35:     var item = list.getItemById(itemId);
  36:     ctx.load(item);
  37:     ctx.executeQueryAsync(
  38:         Function.createDelegate(this, function (sender, args) {
  39:             // here you may get values of all fields in the list
  40:             // item.get_item("Field1"), item.get_item("Field2"), item.get_item("Field3"), ...
  41:         }),
  42:         Function.createDelegate(this, function (sender, args) { console.log("Error: " +
  43:             args.get_message() + "\n" + args.get_stackTrace()); }));
  44: },

In this example we read all items from the list first and get id of the 1st item only, but you may retrieve item using your own criteria of course by specifying appropriate CAML query. With this approach it is possible to get values of all fields from the list in list item.

Thursday, April 14, 2016

Issue with JQuery ajax request which uses CORS when JSONP is explicitly specified in request options

Sometime ago we faced with interesting issue: we use JSONP request via jQuery.ajax() to send data to another domain:

   1: jQuery.ajax({
   2:     url: url,
   3:     data: { ... },
   4:     dataType: "jsonp",
   5:     success: function(msg) {
   6:         ...
   7:     }
   8: });

On most sites it works as expected, i.e. it sends HTTP GET request and passes JSONP callback function name in query string parameter. But on one site because of some reason it sends HTTP OPTION verb to the same url, which as you probably know, happens when using CORS. Since our endpoint didn’t support CORS we got errors:

Chrome:

XMLHttpRequest cannot load http://example1.com. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin http://example2.com is therefore not allowed access. The response had HTTP status code 404.

FF:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://example1.com. (Reason: CORS header 'Access-Control-Allow-Origin' missing).

IE:

XMLHttpRequest: Network Error 0x80070005, Access is denied.

In order to fix the issue, i.e. in order to force jQuery to use JSONP instead of CORS additional parameter “crossDomain: true” should be added to request options:

   1: jQuery.ajax({
   2:     url: url,
   3:     data: { ... },
   4:     dataType: "jsonp",
   5:     crossDomain: true,
   6:     success: function(msg) {
   7:         ...
   8:     }
   9: });

After that jQuery will start to use JSONP. However the question why it implicitly uses CORS on some sites even if we pass dataType: “jsonp” in request options is still open. If you will find the reason please share it in comments.

Wednesday, April 13, 2016

Fix problem with ScriptEditorWebPart not available for contributors in Sharepoint sites

By default in Sharepoint users with Contribute permissions are not able to use scripting web parts. This is done because of security reasons, because otherwise regular contributors would be able to add e.g. ScriptEditorWebPart on the page and execute any javascript code on the page. This is how Media and Content web parts look like for contributors in this case:

As you can see neither ScripEditor nor ContentEditor web parts present here. At the same site users with Full control or Manage hierarchy permissions may add these web parts on the page:

However sometimes it is necessary to enable scripting web parts also for contributors. Fortunately in Sharepoint it is possible to enable this functionality for users with Contribute permissions. For on-premise Sharepoint steps are described in the following Technet article: Allow or prevent Contributors ability to edit scriptable Web Parts. You need to go to Central administration > Manage web applications > Select your web application and in the ribbon under Security category click Web part security:

Then under Scriptable web parts select “Allows contributors to add or edit scriptable Web Parts” option:

For Sharepoint Online it is also possible to enable scripting web parts for contributors using the following instructions: Turn scripting capabilities on or off. As it says, scripting capabilities are disabled by default for:
  • Personal sites
  • Self-service created sites
  • The Root Site Collection of the Tenant

In admin center it is possible to enable scripting capabilities for the personal and self-service created sites:

For enabling this possibility on other site collections you need to execute the following PowerShell script:

Set-SPOsite <SiteURL> -DenyAddAndCustomizePages 0

But it should match to the settings specified in admin center, otherwise it will be overridden within 24h (admin center settings have priority over PowerShell). Hope that this informat

Wednesday, April 6, 2016

One workaround when People picker doesn’t resolve FBA users in Sharepoint 2013

There are many guides of how to configure FBA in Sharepoint 2013 (e.g. here). One of the common problem with FBA in Sharepoint is that People picker doesn’t resolve FBA user names. How to fix this issue? Let’s use assumption that you followed configuration guide mentioned above and did all steps described there.

One of the most common solutions for this problem is that you need to add necessary permissions for identity of application pool of your web application to FBA database:

  • aspnet_Membership_FullAccess
  • aspnet_Personalization_FullAccess
  • aspnet_Profile_FullAccess
  • aspnet_Roles_FullAccess
  • aspnet_WebEvent_FullAccess

If you did it and it didn’t help try to search FBA user by full claims login name. E.g. suppose that we have user “superuser” in FBA database. Then claims login name will be “i:0#.f|fba_membership|superuser” (fba_membership is the name of membership provider configured in web.config of your web application, central administration and secure token service). It should find the user and after that People picker should successfully resolve FBA users.

Tuesday, April 5, 2016

Grant unique permissions to Sharepoint group on web site using client object model

Sometime we need to assign unique permissions on Sharepoint site. Suppose that we have Content Producers group and need to grant Contribute permission on the site. The following example shows how to do it using client object model:

   1: var clientContext = new ClientContext("http://example.com");
   2: clientContext.Credentials = credentials;
   3: var web = clientContext.Web;
   4:  
   5: clientContext.Load(web);
   6: clientContext.ExecuteQueryRetry();
   7:  
   8: var permissions = new List<PermissionEntity>();
   9: permissions.Add(new PermissionEntity
  10: {
  11:     GroupName = "Content Producers",
  12:     RoleType = "Contribute"
  13: });
  14: SetPermission(clientContext, web, permissions);
  15:  
  16: public static void SetPermission(ClientContext clientContext, Web newSubSite,
  17:     List<PermissionEntity> permissions)
  18: {
  19:     clientContext.Load(newSubSite, w => w.RoleAssignments,
  20:         w => w.RoleDefinitions,
  21:         w => w.SiteGroups,
  22:         w => w.HasUniqueRoleAssignments);
  23:     clientContext.ExecuteQueryRetry();
  24:  
  25:     foreach (var p in permissions)
  26:     {
  27:         var group = newSubSite.SiteGroups.FirstOrDefault(g =>
  28:             string.Compare(g.LoginName, p.GroupName, true) == 0);
  29:         if (group == null)
  30:         {
  31:             return;
  32:         }
  33:  
  34:         if (roleAssigned(clientContext, newSubSite, group, p.RoleType))
  35:         {
  36:             continue;
  37:         }
  38:  
  39:         if (!newSubSite.HasUniqueRoleAssignments)
  40:         {
  41:             newSubSite.BreakRoleInheritance(true, true);
  42:             clientContext.ExecuteQueryRetry();
  43:         }
  44:  
  45:         newSubSite.AddPermissionLevelToGroup(p.GroupName, p.RoleType);
  46:     }
  47: }
  48:  
  49: public static bool roleAssigned(ClientContext clientContext, Web web, Group group,
  50:     string roleType)
  51: {
  52:     foreach (var roleAssignment in web.RoleAssignments)
  53:     {
  54:         if (roleAssignment.PrincipalId == group.Id)
  55:         {
  56:             var rd = web.RoleDefinitions.GetByName(roleType);
  57:             var roleDefinitionBindings = roleAssignment.RoleDefinitionBindings;
  58:             clientContext.Load(rd);
  59:             clientContext.Load(roleDefinitionBindings);
  60:             clientContext.ExecuteQueryRetry();
  61:             foreach (var rdb in roleDefinitionBindings)
  62:             {
  63:                 if (rdb.RoleTypeKind == rd.RoleTypeKind)
  64:                 {
  65:                     return true;
  66:                 }
  67:             }
  68:         }
  69:     }
  70:     return false;
  71: }
  72:  
  73: public class PermissionEntity
  74: {
  75:     public string GroupName;
  76:     public string RoleType;
  77: }

In this example we used helper extension method AddPermissionLevelToGroup from Office365 Developer Patterns and Practices project, which at the end implemented like this:

   1: private static void AddPermissionLevelImplementation(
   2:     this SecurableObject securableObject,
   3:     Principal principal, RoleDefinition roleDefinition,
   4:     bool removeExistingPermissionLevels = false)
   5: {
   6:     if (principal == null)
   7:     {
   8:         return;
   9:     }
  10:     RoleAssignmentCollection roleAssignments = securableObject.RoleAssignments;
  11:     securableObject.Context.Load(roleAssignments);
  12:     securableObject.Context.ExecuteQueryRetry();
  13:     RoleAssignment roleAssignment = roleAssignments.FirstOrDefault(ra =>
  14:         ra.PrincipalId.Equals(principal.Id));
  15:     if (roleAssignment == null)
  16:     {
  17:         RoleDefinitionBindingCollection roleDefinitionBindingCollection =
  18:             new RoleDefinitionBindingCollection(securableObject.Context);
  19:         roleDefinitionBindingCollection.Add(roleDefinition);
  20:         securableObject.RoleAssignments.Add(principal,
  21:             roleDefinitionBindingCollection);
  22:         securableObject.Context.ExecuteQueryRetry();
  23:         return;
  24:     }
  25:     RoleDefinitionBindingCollection roleDefinitionBindings =
  26:         roleAssignment.RoleDefinitionBindings;
  27:     securableObject.Context.Load(roleDefinitionBindings);
  28:     securableObject.Context.ExecuteQueryRetry();
  29:     if (removeExistingPermissionLevels)
  30:     {
  31:         roleDefinitionBindings.RemoveAll();
  32:     }
  33:     if (!roleDefinitionBindings.Any(r =>
  34:         r.Name.Equals(roleDefinition.EnsureProperty(rd => rd.Name))))
  35:     {
  36:         roleDefinitionBindings.Add(roleDefinition);
  37:         roleAssignment.ImportRoleDefinitionBindings(roleDefinitionBindings);
  38:         roleAssignment.Update();
  39:         securableObject.Context.ExecuteQueryRetry();
  40:     }
  41: }

This approach may be used to grant permissions both on-premise and in Sharepoint Online.