Friday, April 26, 2019

Provision multilingual sites with PnP templates

Sharepoint PnP Powershell library (which is also available on Nuget) has own provisioning engine. It is quite powerful engine which allows to provision MUI sites (although it has own problems with stability). In order to do that you need to use several basic elements:

1. Have all literals which should be localized in resx file

2. Specify supported languages inside <pnp:SupportedUILanguages>…</pnp:SupportedUILanguages> section of PnP template. After provisioning these languages will be set in Site settings > Language settings > Alternate languages. You need to have resx file for each supported language in standard resources.xx-XX.resx format

3. Resx files with translations should be specified inside <pnp:Localizations>…</pnp:Localizations> section of PnP template. Note that PnP will provision all these languages to your site regardless of what alternate languages are set in Site settings > Language settings on the moment when this template is applied to target site

4. For localized strings use {resource:ResourceKey} format inside template

Here is example of how PnP template for MUI site provisioning may look like:

<?xml version="1.0"?>
<pnp:Provisioning xmlns:pnp="http://schemas.dev.office.com/PnP/2018/01/ProvisioningSchema">
  <pnp:Localizations>
 <pnp:Localization LCID="1033" Name="English" ResourceFile="resources.en-US.resx"/>
    <pnp:Localization LCID="1031" Name="German" ResourceFile="resources.de-DE.resx"/>
  </pnp:Localizations>
  <pnp:Templates ID="Test-Container">
    <pnp:ProvisioningTemplate ID="Test" Version="1">

      <pnp:SupportedUILanguages>
        <pnp:SupportedUILanguage LCID="1033" />
        <pnp:SupportedUILanguage LCID="1031" />
      </pnp:SupportedUILanguages>

   <pnp:SiteFields xmlns:pnp="http://schemas.dev.office.com/PnP/2018/01/ProvisioningSchema">
  <Field ID="..." Name="MyField" DisplayName="{resource:MyFieldTitle}" Type="Text" Group="Custom" SourceID="..." StaticName="MyField"></Field>
   </pnp:SiteFields>
   
    </pnp:ProvisioningTemplate>
  </pnp:Templates>
</pnp:Provisioning>

It will provision file with English and German UI languages.

Thursday, April 18, 2019

Can’t get groups created from MS Teams from Graph endpoint /beta/me/joinedGroups

With MS Graph API you may use /beta/me/joinedGroups endpoint for getting list of groups where current user is a member. With the same endpoint you may also get isFavorite attribute for the group which shows whether or not user added group to favorites. However this endpoint has own issues: recently we found that it doesn’t return groups which were created from MS Teams: when you create new Team there also related Group is created. It is possible to get details of this group using basic groups endpoint

https://graph.microsoft.com/v1.0/groups/{id}

But if you will try to get list of user’s groups via beta endpoint such groups created from MS Teams won’t be returned:

https://graph.microsoft.com/beta/me/joinedgroups/?$select=id,isfavorite,displayName&$top=200

One possible explanation could be that internally /me/joinedgroups end point is routed to Outlook services which is not integrated with Teams well enough yet: when I tried to add createdDateTime attribute to the REST url (this attribute is returned for groups from basic endpoint - see above)

https://graph.microsoft.com/beta/me/joinedgroups/?$select=id,isfavorite,displayName,createdDateTime &$top=200

it returned error saying that returned entities have Microsoft.OutlookServices.Group type:

May be this is a bug or such functionality is not implemented in beta endpoint yet. For now I asked this question in StackOverflow – hope that somebody from MS Graph product team will answer it.

Friday, April 5, 2019

Use Sharepoint search API using HTTP POST requests

As you probably know it is possible to use Sharepoint search API programmatically by calling /_api/search/query endpoint with HTTP GET and provide KQL query and details (like selected managed properties) in query string. Popular Search Query Tool uses the same technique. However query string approach has own limitation, e.g. max 4kb length limit which is common for ASP.Net applications. If your query is built dynamically and you don’t know the actual length on compile time you may use search API with HTTP POST and provide KQL query in request body. If you want to use search API with HTTP POST you need to use slightly different endpoint: /_api/search/postquery. Let’s see how it works in Postman tool.

The first thing which we need to get is to obtain access token. It can be done by sending another HTTP POST request to the following address: https://accounts.accesscontrol.windows.net/{tenant_name}.onmicrosoft.com/tokens/OAuth/2. In request body we need to provide several parameters which are described in the following list:

  • grant_type = client_credentials
  • client_id – client id of your Sharepoint app (you should register it in advance using /_layouts/15/appregnew.aspx and then grant appropriate permissions using /_layouts/15/appinv.aspx) in the following form {cliend_id}@{tenant_id}. You may check tenant id in Azure portal > Azure Active Directory > Properties > Directory ID
  • client_secret – client secret of your Sharepoint app
  • resource – should have value in the form 00000003-0000-0ff1-ce00-000000000000/{tenant_name}.onmicrosoft.com@{tenant_id}

so request should look like this in Postman:

If everything was configured properly you should get success response which should contain access_token in the response body. Copy it’s value – it will be needed on the next step.

Now we are ready to send search POST requests to search API endpoint. We will use https://{tenant_name}.sharepoint.com/_api/search/postquery address for that. Let’s get list of all sites using the following KQL query:

contentclass:STS_Site

Request body should look like this:

{
   "request":{
      "Querytext":"contentclass:STS_Site",
      "RowLimit":100,
      "SelectProperties":{
         "results":[
            "Title",
            "SiteID",
            "OriginalPath"
         ]
      }
   }
}

After that switch to Authorization type and select Type = Bearer Token and specify value of access_token which was obtained on the previous step:

On the Headers tab add Accept and Content-type headers with “application/json;odata=verbose” (note that it is important to specify these headers exactly like this: if you will specify different odata version request may return error because parameters schema may be different.):

If everything was done properly when you will execute POST request in Postman you will get list of Sharepoint sites in your tenant.

Wednesday, March 13, 2019

Get login name of special group “Everyone except external users” programmatically in Sharepoint

In Sharepoint Online you may assign permissions to all employees of your organization using special group “Everyone except external users”. In order to add permissions to this group programmatically we need to know login name of the appropriate object in Sharepoint object model. In this article I will show how to get login name of this special group programmatically.

The main difficulty is that login name of “Everyone except external users” group is different per tenant. But the good thing is that it is built using known rule:

c:0-.f|rolemanager|spo-grid-all-users/{realm}

where instead of {realm} placeholder you need to use realm for your tenant. We can get realm using TokenHelper.GetRealmFromTargetUrl() method. So code will look like this:

protected virtual string GetEveryoneExceptExternalsLoginName(string siteUrl)
{
 var realm = TokenHelper.GetRealmFromTargetUrl(new Uri(siteUrl));
 return string.Format("c:0-.f|rolemanager|spo-grid-all-users/{0}", realm);
}

public static string GetRealmFromTargetUrl(Uri targetApplicationUri)
{
 #if ONPREMISES
 if (targetApplicationUri.Scheme.ToLower() == "https")
  ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
 #endif

 WebRequest request = WebRequest.Create(targetApplicationUri + "/_vti_bin/client.svc");
 request.Headers.Add("Authorization: Bearer ");

 try
 {
  using (request.GetResponse())
  {
  }
 }
 catch (WebException e)
 {
  if (e.Response == null)
  {
   return null;
  }

  string bearerResponseHeader = e.Response.Headers["WWW-Authenticate"];
  if (string.IsNullOrEmpty(bearerResponseHeader))
  {
   return null;
  }

  const string bearer = "Bearer realm=\"";
  int bearerIndex = bearerResponseHeader.IndexOf(bearer, StringComparison.Ordinal);
  if (bearerIndex < 0)
  {
   return null;
  }

  int realmIndex = bearerIndex + bearer.Length;

  if (bearerResponseHeader.Length >= realmIndex + 36)
  {
   string targetRealm = bearerResponseHeader.Substring(realmIndex, 36);

   Guid realmGuid;

   if (Guid.TryParse(targetRealm, out realmGuid))
   {
    return targetRealm;
   }
  }
 }
 return null;
}

After that you will be able to grant permissions to “Everyone except external users” programmatically.

Update 2020-02-10: method described here may not work for old tenants. See this article for workaround for old tenants: Resolve “Everyone except external users” group on old tenants

Wednesday, March 6, 2019

Strange behavior of Sharepoint Online when specify different users as primary site collection administrator

When you create new site collection (doesn’t matter modern or classic) using Tenant.CreateSite() method you need to specify primary site collection administrator in SiteCreationProperties.Owner property. So if you would check site collection administrators of the newly created site you would expect to see specified user there. However it is not always the case. Sometimes Sharepoint really shows specified user there:

But sometimes instead of actual user account there will be Company Administrator – special user which covers all users in the directory with Global Administrator rights (see e.g. Special SharePoint Groups):

It happens regardless of specified user directory role: it may have Global Administrator role and may not have it:

And even more – if user have more directory roles there may be both Company Administrator and Sharepoint Service Administrator:

I.e. looks like Sharepoint get’s user’s roles and assigns permissions to these roles instead of actual user account. But again – for some user accounts it just adds actual user account to Site collection administrators. Logic behind this behavior is not yet clear so if you have any thoughts on that please share it in comments.

Sunday, February 24, 2019

Camlex 5.1 and Camlex.Client 3.2 have been released: support for Includes/NotIncludes operations

Good news for Sharepoint developers which use Camlex and Camlex.Client in their work (Camlex is free open source library for simplifying creation of CAML queries in Sharepoint by using C# lambda expressions): today new versions Camlex 5.1 and Camlex.Client 3.2 have been released. In this version support of Include/NotInclude CAML operations was added to the library. Here is what MSDN says about Includes element:

If the specified field is a Lookup field that allows multiple values, specifies that the Value element is included in the list item for the field that is specified by the FieldRef element.

and about NotIncludes:

If the specified field is a Lookup field that allows multiple values, specifies that the Value element is excluded from the list item for the field that is specified by the FieldRef element.

I.e. these operations are used for multi lookup fields. And although there are known workarounds which allows to use another operations to make similar queries (see e.g. CAML query for field of type “Person or Group” which allows multiple selections) I’ve decided to add support of official operations for multiple lookup field types in order to have complete set of CAML operations in Camlex. Let’s see how it works.

In order to create CAML query with Includes operation with Camlex use the following syntax:

string caml = Camlex.Query().Where(x => ((int)x["Foo"]).Includes(1)).ToString();

Here we used Includes extension method defined in Camlex. It will produce the following CAML:

<Where>
  <Includes>
    <FieldRef Name="Foo" />
    <Value Type="Integer">1</Value>
  </Includes>
</Where>

If we need to perform query using lookup id (i.e. add LookupId=”TRUE” attribute to FieldRef element) – use overloaded version of Includes method with boolean parameter and pass true there:

string caml = Camlex.Query().Where(x => ((int)x["Foo"]).Includes(1, true)).ToString();

which will produce:

<Where>
  <Includes>
    <FieldRef Name="Foo" LookupId="True" />
    <Value Type="Integer">1</Value>
  </Includes>
</Where>

The same functionality is also added to Camlex.Client – Camlex version built for CSOM. Both Camlex and Camlex.Client are available in Nuget. You may add them to your projects by using the following commands:

Install-Package Camlex.NET.dll

or for CSOM version:

Install-Package Camlex.Client.dll

Hope that new feature will help in your work.

Saturday, February 9, 2019

Workaround for render error of SPFx web part

If on your Sharepoint site you use SPFx web part (ClientSideWebPart) you may face with the following error:

Failed to render client side web part. Web part … failed to render as manifest is not found

Web Part framework is not loaded

In order to  avoid this problem try the following workaround: go to App catalog, delete web part’s app package from there and upload it again.