Friday, October 12, 2018

Using of relative and absolute urls when create Modern Sharepoint site via PnP PowerShell

Modern Team and Communication sites can be created through PnP PowerShell cmdlet New-PnPSite. Here are examples how you may create these sites:

Team: New-PnPSite -Type TeamSite -Title Test -Alias test
Communication: New-PnPSite -Type CommunicationSite -Title Test -Url https://{tenant}.sharepoint.com/sites/test

Note that for Team site we use Alias parameter while for Communication site we use Url. It is mandatory to use relative url when you create Team site and absolute url when you create Communication site. If you will try to create Team site with absolute url:

Team: New-PnPSite -Type TeamSite -Title Test -Alias https://{tenant}.sharepoint.com/sites/test

you will get the following error:

Invalid value specified for property 'mailNickname' of resource 'Group'.

And if you will try to create communication site with relative url:

Communication: New-PnPSite -Type CommunicationSite -Title Test -Url test

there will be another error:

This operation is not supported for a relative URI.

Hope that it will help someone.

Wednesday, October 10, 2018

Access denied when try to delete folder in Modern Sharepoint site’s doclib

Recently I faced with the following problem: when you create OTB Modern Team or Communication site (see How to create modern Team or Communication site in Sharepoint) you can create sub folders in document libraries, e.g. in Style library:

image

(BTW with modern experience currently it is possible to create sub folders of only first level. If you want to create sub folders of deeper levels you have to switch to classic experience and create sub folder from there)

However if you will try to delete this folder you will get the following error:

Sorry, something went wrong
The server has encountered the following error(s):
Test
Access denied. You do not have permission to perform this action or access this resource.

image

In order to avoid this error you need to enable customizations of pages and scripts on the site. You may do it with the following PowerShell command:

Connect-SPOService -Url https://{tenant}-admin.sharepoint.com
Set-SPOSite {url} -DenyAddAndCustomizePages 0

After that you will be able to delete sub folders in Style library doclib on the modern site.

Thursday, September 27, 2018

Problem with getting user token for MS Graph using Azure AD app of Web app type registered in v1 portal.azure.com

As you probably know currently it is possible to register Azure AD apps in 2 places:

  1. https://portal.azure.com – v1 portal
  2. https://apps.dev.microsoft.com – v2 portal

There is number of differences between apps registered in these 2 portals – you may check them e.g. here: About v2.0. For this article let’s notice that apps registered in v2 may support both web app and native platforms while apps in v1 may be either web app or native but not both. If you need them both you have to register 2 apps in v1 portal.

Recently we faced with a problem of getting user token for MS Graph i.e. token based on user credentials. We used the following code for that and it works properly for the app registered in v2 portal with native platform support:

var credentials = new Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential("username", "password");       
var token = Task.Run(async () =>
{
    var authContext = new AuthenticationContext(string.Format("https://login.microsoftonline.com/{0}", "mytenant.onmicrosoft.com"));
    var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com", appId, credentials);
    return authResult.AccessToken;
}).GetAwaiter().GetResult();

where for appId we used Azure AD app id registered in v2. When we tried to run the same code for the app registered in v1 portal with web app type the following error was shown:

Error: index was outside the bounds of the array

The same code also works properly for the app from v1 portal but with native type. I.e. it looks like AuthenticationContext.AcquireTokenAsync() method may fetch user token only for native app. If you know how to get user token for web app from v1 portal please share it in comments.

Monday, September 24, 2018

Access denied when try to get Azure AD group’s unseencount through Graph API

Some time ago I faced with interesting problem: when tried to get properties of Azure AD group using app token with app permissions (without available user context) through Graph API:

https://graph.microsoft.com/v1.0/groups/{groupId}?$select=visibility,unseencount

the following error was shown:

{
     "error": {
         "code": "ErrorAccessDenied",
         "message": "Access is denied. Check credentials and try again.",
         "innerError": {
             "request-id": "…",
             "date": "…"
         }
     }
}

Here is example from Postman:

01

Investigation showed that problem was caused by unseencount property. When I tried to remove it – another selected property (visibility) was returned successfully:

https://graph.microsoft.com/v1.0/groups/{groupId}?$select=visibility

{
     "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups(visibility)/$entity",
     "visibility": "Public"
}

02

What was even more stranger is that in Graph explorer it worked:

03

Communication with MS support both on forums (see Can't get group's unseenCount) and via Azure support ticket helped to figure out the reason of this problem: in Postman I used app token with app permissions, while in Graph explorer I was authenticated with my own account (see above). I.e. in Graph explorer delegated permissions were used. And there is known issue in MS Graph (see Known issues with Microsoft Graph): unseencount may be retrieved only using delegated permissions:

“Examples of group features that support only delegated permissions:

  • Group conversations, events, photo
  • External senders, accepted or rejected senders, group subscription
  • User favorites and unseen count
  • Microsoft Teams channels and chats”

Hope that this information will help someone.

Tuesday, September 11, 2018

Several problems when follow/unfollow Sharepoint site via REST API

If you need to implement follow/unfollow site functionality via javascript you most probably will find the following MS article Follow documents, sites, and tags by using the REST service in SharePoint which contains the following example which uses REST API:

$.ajax( {
	url: followingManagerEndpoint + "/isfollowed",
	type: "POST",
	data: JSON.stringify( { 
		"actor": {
			"__metadata": {
				"type":"SP.Social.SocialActorInfo"
			},
			"ActorType":2,
			"ContentUri":siteUrl,
			"Id":null
		} 
	}),
	headers: { 
		"accept":"application/json;odata=verbose",
		"content-type":"application/json;odata=verbose",
		"X-RequestDigest":$("#__REQUESTDIGEST").val()
	},
	success: function (responseData) { 
		...
	},
	error: requestFailed
});

However currently if you will try to run it you will get the following 400 Bad request error:

{"error":{"code":"-1, Microsoft.OData.Core.ODataException","message":"The property '__metadata' does not exist on type 'SP.Social.SocialActorInfo'. Make sure to only use property names that are defined by the type."}}

The problem is with __metadata property of the actor which seems to be outdated nowadays. In order to avoid this error just remove or comment __metadata from actor object.

Another problem is related with odata=verbose specified in Accept header. If you will try to follow site with it API will return the following 406 Not acceptable error:

{"error":{"code":"-1, Microsoft.SharePoint.Client.ClientServiceException","message":"The HTTP header ACCEPT is missing or its value is invalid."}}

In order to resolve it change odata=verbose to odata.metadata=minimal in Accept header. Here is the working code written with Typescript and SPFx:

context.spHttpClient.post(context.pageContext.web.absoluteUrl + "/_api/social.following/follow",
  SPHttpClient.configurations.v1, {
	headers: {
	  'Accept': 'application/json;odata.metadata=minimal',
	  'Content-type': 'application/json;odata=verbose',
	},
	body: JSON.stringify({
	  "actor": {
		  /*"__metadata": {
			  "type": "SP.Social.SocialActorInfo"
		  },*/
		  "ActorType": 2,
		  "ContentUri": site.url,
		  "Id": null
	  }
	})
  });

Hope that it will help someone.

Friday, September 7, 2018

How to delete navigation nodes with AuthoringLink types programmatically in Sharepoint

If you use structural navigation on your publishing Sharepoint site and navigation nodes are created as headings (NodeType = Heading), then it is quite straightforward to delete such navigation nodes programmatically. Here is how it can be done:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test"));
if (nodePage != null)
{
	nodePage.Delete();
}

In this example we delete navigation node with title “Test”. However if you will try to delete navigation nodes which were created as AuthoredLink* (see NodeTypes Enum):

  • AuthoredLink
  • AuthoredLinkPlain
  • AuthoredLinkToPage
  • AuthoredLinkToWeb

using the same code you will find that link is not get deleted. The workaround is to change NodeType property first to Heading and then delete the node:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test" &&
	(n.Properties != null && n.Properties["NodeType"] != null && n.Properties["NodeType"] is string &&
		(n.Properties["NodeType"] as string == "AuthoredLink" || (n.Properties["NodeType"] as string).StartsWith("AuthoredLink"))));

if (nodePage != null)
{
	nodePage.Properties["NodeType"] = "Heading";
	nodePage.Update();

	// reinitialize navigation nodes
	pweb = PublishingWeb.GetPublishingWeb(web);
	globalNavigation = pweb.Navigation.GlobalNavigationNodes;
	nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test);
	if (nodePage != null)
	{
		nodePage.Delete();
	}
}

After that AuthoredLink navigation node will be successfully deleted.

Tuesday, September 4, 2018

How to get modern Team or Communication sites using Search API in Sharepoint

In one of my previous posts I wrote how to create modern Sharepoint sites: How to create modern Team or Communication site in Sharepoint (quite basic post but necessary if you just stated to work with modern sites). In this article we will continue exploring modern sites and will see how to get list of modern Team or Communication sites using Search API. Using of Search API is preferable in many scenarios as you have all sites at once with single API call.

Let’s create modern Team site and explore it’s Site Pages doclib. In default list view let’s add additional column “Content Type” to see what content type is used for the default front page:

image

As you can see it uses “Site Page” content type. Modern Communication site also uses the same content type for front page.

So in order to get list of all modern sites we may query for pages created with “Site Page” content type. In order to make our search query language-independent we will use content type id instead of the name. In order to get it go to Site Pages doclib settings and click Site Page content type. Then copy content type id from query string. You will have something like that:

0x0101009D1CB255DA76424F860D91F20E6C4118…

where the rest will be unique for your doclib. Our query string will look like that then:

ContentTypeId:0x0101009D1CB255DA76424F860D91F20E6C4118*

which means return all pages which content type starts with specified id. If we will test it in the Search Query Tool we will have list of all modern Team and Communication sites in the tenant:

image

In order to get distinct list don’t forget to check “Trim duplicates” option and select at least Title and SPWebUrl managed properties which contain site title and url.

Monday, September 3, 2018

How to explore Sharepoint REST API endpoints

Sometimes you need to get list of available operations in the Sharepoint REST API endpoint. Let’s say we want to check operations available for /_api/SitePages endpoint.

First of all we need to get authentication cookies. In order to get them lunch Fiddler and open e.g. Sharepoint landing page (/_layouts/15/Sharepoint.aspx) which is opened from App launcher > Sharepoint. On this page there will be several REST API calls which will contain Cookies header. E.g. /_api/GroupSiteManager/CanUserCreateGroup:

image

From this Fiddler view copy value of Cookie header.

After that launch Postman and create request on endpoint in question: /_api/SitePages. In Headers section add Cookie, put value copied from Fiddler and click Send:

image

In the response it will return list of relative endpoint operations available under selected endpoint. In this example they are:

  • /_api/SitePages/CommunicationSite
  • /_api/SitePages/Pages
  • /_api/SitePages/PublishingSite

Note that this method doesn’t return POST/PUT endpoints unfortunately.

How to create modern Team or Communication site in Sharepoint

Modern Team/Communication sites are not displayed on the classic create site page in Sharepoint together with other “classic” web templates. In order to create them you need first click App launcher icon in top left corner and choose Sharepoint link there:

image

It will open Sharepoint landing page which will have Create site icon on the top:

image

After clicking on this icon you will be able to choose which modern site to create: Team or Communication:

image

And on the last step you have to specify site name, privacy and classification (later one is shown if classifications are configured for your tenant):

image

After clicking Next your modern Team or Classification site will be created.

Monday, August 27, 2018

Combine async redux actions in SPFx react components

If you work with SPFx and e.g. implement web part which uses react/redux for its components then you may face with need to combine multiple async redux actions into single one. Let’s assume that we have components which shows O365 groups and has appropriate properties and actions for it:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import * as GroupActions from 'groupActions';

export interface GroupListProps {
  groups: IGroup[];
  actions: {
    getGroups: GroupActions.IGetGroups,
  };
}

class GroupList extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

After that we decide to show both regular Sharepoint sites together with groups. We need to action for getting sites themselves (asynchronously using promises) and another action for combining groups and sites into single list. Both actions ae asynchronous. Let’s see how we may combine them to single one.

At first start with adding new action for sites:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  actions: {
    getGroups: GroupActions.IGetGroups,
	getSites: SiteActions.IGetSites,
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groups) {
      this.props.actions.getGroups();
    }

    if (!this.props.sites) {
      this.props.actions.getSites();
    }
  }

  public render() {
    return (
      this.props.groups ? <GroupsList items={this.props.groups} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
    getGroups: () => dispatch(GroupActions.getGroupsImpl()),
	getSites: () => dispatch(SiteActions.getSitesImpl()),
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

Then create new combined action which calls 2 other actions (getGroups and getSites) via dispatch and additional action which will combine 2 groups and sites to single list:

import * as React from 'react';
import { connect } from 'react-redux';
import { IGroup } from 'IGroup';
import { ISite } from 'ISite';
import { IGroupOrSite } from 'IGroupOrSite';
import * as GroupActions from 'groupActions';
import * as SiteActions from 'siteActions';
import * as GroupOrSiteActions from 'groupOrSiteActions';

export interface GroupListProps {
  groups: IGroup[];
  sites: ISite[];
  groupsOrSites: IGroupOrSite[];
  actions: {
	getGroupsAndSites: GroupOrSiteActions.IGetGroupsAndSites
	combineGroupsAndSites: GroupOrSiteActions.ICombineGroupsAndSites
  };
}

class GroupListContainer extends React.Component<GroupListProps, {}> {
  public componentWillMount(): void {
    if (!this.props.groupsOrSites) {
      this.props.actions.getGroupsAndSites();
    }
  }

  public componentDidUpdate(prevProps: GroupListProps) {
    // Combine groups and sites
    if ((!isEqual(this.props.groups, prevProps.groups) || !isEqual(this.props.sites, prevProps.sites)) &&
	  this.props.groups && this.props.sites) {
	  this.props.actions.combineGroupsAndSites(this.props.groups, this.props.sites);
    }
  }
  
  public render() {
    return (
      this.props.groupsOrSites ? <GroupsList items={this.props.groupsOrSites} /> : <LoadingSpinner />
    );
  }
}

const mapStateToProps = (state: GroupListState) => ({
  groups: state.groups,
  sites: state.sites,
  groupsOrSites: state.groupsOrSites
});

const mapDispatchToProps = (dispatch: Dispatch<any>) => ({
  actions: {
	getGroupsAndSites: () => {
		dispatch(GroupActions.getGroupsImpl());
		return dispatch(SiteActions.getSitesImpl());
	},
	getGroupsAndSites: (groups: IGroup[], sites: ISite[]) => dispatch(GroupOrSiteActions.combineSitesImpl(groups, sites))
  }
});

/**
 * Connecting the store to the component
 */
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(GroupList);

As result when you will call this combined action it will perform 2 async sub actions.

Tuesday, August 21, 2018

Enable SP Editor Chrome extension in private (incognito) Chrome mode

Some time ago I wrote about very convenient Chrome extension SP Editor (see this post: SP Editor Chrome extension: free open source alternative to Sharepoint Designer). It allows to perform many operations with your Sharepoint Online or on-prem site right in the browser without running any scripts or installing additional tools. However like usual Chrome extension it is disabled by default in private (incognito) Chrome mode. This is not very convenient because when you work with Sharepoint Online you often need to login to different sites with different accounts and incognito mode is often used for that.

The good thing is that it is quite easy to enable SP Editor for incognito mode: go to Chrome menu > More tools > Extensions > SP Editor > Details. In opened window click “Allow in incognito”:

image

After that extension will become available in private mode (you will need to re-open F12 developer tools to see SharePoint tab from SP Editor there).

Wednesday, July 11, 2018

Decouple from RSS feeds urls using IIS reverse proxy

Suppose that we have web site which shows information from multiple RSS feeds. In this example I will use Sharepoint site which consumes RSS feeds using standard RSS-viewer web part but it also can be site on any other technology:

01

In this case if external provider will decide to change RSS url – connection to your site will be also broken. And if you showed it in many places you will need to go through all of them and fix one by one:

02

In case of Sharepoint this may be real problem because you will need to go through all sub sites, locate all RSS-viewer web parts which consume broken RSS feed, edit page and fix RSS feed in web part properties.

It would be better if our site would be decoupled from external RSS urls via some intermediate reverse proxy: in this case on our site we would use proxy urls instead of real urls and in proxy we would just define mapping between proxy url and real RSS url:

03

In this case if url of one of RSS feeds will be changed we will only need to change appropriate mapping between proxy url and RSS url on proxy level – much simpler than go through the site and fix all places where RSS feed is used.

04

Such RSS reverse proxy can be configured using IIS reverse proxy: we will need Application request routing (AAR) and URL rewrite IIS modules installed. After installing AAR go to proxy settings (IIS manager > Server > Application request routing cache > Server proxy settings) and enable proxy there:

05

After that create new site in IIS (in Sharepoint instead of new site you may also create sub folder under /_layouts virtual folder and use it as proxy url. In this case you will need to define url rewrite rules on this sub folder level i.e. not on the root site level – see below), define its url via binding (suppose that it will be http://myproxy.com) or define port and add the following rewrite rule:

06

Here we tell IIS that all requests which come to http://myproxy.com/example should be rewritten to the real RSS feed url https://example.com/feed (this is not example of really working RSS feed – just for example). Now if external RSS feed’s url https://example.com/feed will be changed – you will only need to go there and change it in the URL rewrite rule. Your site still will use http://myproxy.com/example in all places and won’t require changes.

Monday, July 2, 2018

Sharepoint MVP 2018

As you probably know MS changed MVP award renewal dates and now renewal emails are coming 1th July. I’ve got the following exciting email yesterday: Congratulations 2018-2019 Microsoft MVPs. This is my 8th award and I’m very happy to be part of the great community. In my regular work I try to participate in Sharepoint and Office 365 communities and help people to solve technical problems. Big thanks to MS for recognizing these efforts. In last few years focus has been moved from on-prem Sharepoint to Sharepoint Online, Office 365 and Azure. This is very interesting to observe how these new platforms and services grow and evolve interacting with each other. And even more interesting to take part in this process by contributing to community life and communicating with MS product teams. Thanks to all readers of my blog and see you with new challenges and solutions :)

Thursday, June 14, 2018

Workaround for hanging local web server launched with gulp serve

If you develop Sharepoint Framework web parts (SPFx web parts) you probably familiar with one part of it’s development process – local web server which is launched with gulp:

gulp serve –nobrowser

It launches local web server on localhost:port which allows you to test SPFx web part on own dev environment (not release version) which consumes js and css files directly from this localhost:port address. So you may modify them and reload the page without redeploying app package to App catalog (which is time consuming if you develop js/css).

However sometimes local web server hangs without visible reasons. In order to avoid it you may stop it (Ctrl-C) and run again – but it also takes time and not always help. If you faced with this problem go to cmd window with launched gulp serve and try click Esc several times. If after that it will start to show output like this:

Request: ‘/dist/mywebpart-bundle.js’
Request: ‘…’
Request: ‘…’

it will mean that server unfreezes and should work again now.

Friday, June 8, 2018

Avoid error “Invalid property 'responseHeaders'” when try to update Azure AD group using Graph client library

Some time ago we faced with interesting problem: when tried to update group using UnifiedGroupsUtility.UpdateUnifiedGroup() method from OfficeDevPnP we got the following error:

HTTP/1.1 400 Bad Request

ef
{
  "error": {
    "code": "Request_BadRequest",
    "message": "Invalid property 'responseHeaders'.",
    "innerError": {
      "request-id": "...",
      "date": "..."
    }
  }
}

Under the hood this method uses .Net Graph client library:

var graphClient = CreateGraphClient(accessToken, retryCount, delay);

var groupToUpdate = await graphClient.Groups[groupId]
	.Request()
	.GetAsync();

if (!String.IsNullOrEmpty(description) && groupToUpdate.Description != description)
{
	groupToUpdate.Description = description;
	updateGroup = true;
}

bool existingIsPrivate = groupToUpdate.Visibility == "Private";
if (existingIsPrivate != isPrivate)
{
	groupToUpdate.Visibility = isPrivate == true ? "Private" : "Public";
	updateGroup = true;
}

if (updateGroup)
{
	var updatedGroup = await graphClient.Groups[groupId]
		.Request()
		.UpdateAsync(groupToUpdate);
}

When I checked in Fiddler request details of UpdateAsync() call I found that JSON representation of group object which is passed to HTTP PATCH method really has responseHeaders property:

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

{
  "classification": "Internal",
  "createdDateTime": "...",
  "description": "test",
  "displayName": "...",
  "groupTypes": [
    "Unified"
  ],
  "mail": "...",
  "mailEnabled": true,
  "mailNickname": "...",
  "proxyAddresses": [
    "SMTP:..."
  ],
  "renewedDateTime": "...",
  "securityEnabled": false,
  "visibility": "Public",
  "id": "...",
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups/$entity",
  "deletedDateTime": null,
  "onPremisesLastSyncDateTime": null,
  "onPremisesProvisioningErrors": [],
  "onPremisesSecurityIdentifier": null,
  "onPremisesSyncEnabled": null,
  "preferredDataLocation": null,
  "resourceBehaviorOptions": [],
  "resourceProvisioningOptions": [],
  "responseHeaders": {
    "Transfer-Encoding": [
      "chunked"
    ],
    "request-id": [
      "..."
    ],
    "client-request-id": [
      "..."
    ],
    "x-ms-ags-diagnostic": [
      ...
    ],
    "OData-Version": [
      "4.0"
    ],
    "Duration": [
      "100.3015"
    ],
    "Strict-Transport-Security": [
      "max-age=31536000"
    ],
    "Cache-Control": [
      "private"
    ],
    "Date": [
      "..."
    ]
  },
  "statusCode": "OK"
}

Not sure why responseHeaders property is now added to the group object when it is first returned from graph. In order to avoid this error I used the following workaround: construct new Group object, specify group id and only those properties which should be updated. I.e. update object with minimal required specified properties:

var graphClient = CreateGraphClient(accessToken);
var existingGroup = await graphClient.Groups[groupId].Request().GetAsync();

var groupToUpdateMinimal = new Group();
groupToUpdateMinimal.Id = groupId;

bool updateGroup = false;
if (!string.IsNullOrEmpty(description) && existingGroup.Description != description)
{
	groupToUpdateMinimal.Description = description;
	updateGroup = true;
}

bool existingIsPrivate = existingGroup.Visibility == "Private";
if (isPrivate != null && existingIsPrivate != isPrivate.Value)
{
	groupToUpdateMinimal.Visibility = isPrivate.Value ? "Private" : "Public";
	updateGroup = true;
}

if (updateGroup)
{
	await graphClient.Groups[groupId].Request().UpdateAsync(groupToUpdateMinimal);
}

In this case only id, description and visibility properties are passed to HTTP PATCH method:

{
  "description": "test",
  "visibility": "Public",
  "id": "..."
}

and group is successfully updated.

Wednesday, May 30, 2018

How to hide Shared With ECB menu item for non-admin users via javascript

In Sharepoint lists and doclib there is possibility to check which users have access to list item or file using “Shared With”. Often this item should be hidden for non-admin users. Below javascript allows to hide “Shared With” ECB menu item for users which are not site admins:

SP.SOD.executeFunc("sp.js", "SP.ClientContext", function () {
	var ctx = SP.ClientContext.get_current();
	var user = ctx.get_web().get_currentUser();
	ctx.load(user);
	ctx.executeQueryAsync(
		Function.createDelegate(this, function () {
			var isSiteAdmin = user.get_isSiteAdmin();
			if (isSiteAdmin) {
				return;
			}
			jQuery("td.ms-list-itemLink-td").click(function(){
				setTimeout(function(){
					jQuery("span.js-callout-ecbActionDownArrow a").click(function(){
						setTimeout(function(){
							jQuery("li[text='Shared With']").hide();
						}, 200);
					});
				}, 200);
			});
		}),
		Function.createDelegate(this, function (sender, args) {
			// log error
		}));
});

Here we first check whether current user site admin or not. After that we add our own event handler for ECB click item which hides “Shared With” command. Little delay is needed in order to allow ECB sub menu to be displayed after original click handler will be triggered.

Monday, May 28, 2018

Set O365 group classification via Graph API

Some time ago MS announced possibility to specify groups classifications – see e.g. Classifications for Office365 Groups and Microsoft Teams. After you configured classifications on tenant level like shown in mentioned article when you go to Sharepoint app from your Office 365 App launcher (it will lead to https://{tenant}.sharepoint.com/_layouts/15/sharepoint.aspx) and choose “+ Create site” from the header – you will see dropdown list with specified classifications:

2018-05-28_17-14-43

When you will create new site classification will be shown in the header:

2018-05-28_17-16-26

Also if you will get related group via Graph API – classification will be returned with other group’s properties:

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

will return

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups('{id}')/classification",
  "value": "internal"
}

But is it possible to update group’s classification programmatically? If we just try to send PACTH request on https://graph.microsoft.com/v1.0/groups/{id} endpoint we will get the following error:

{
    "error": {
        "code": "Request_BadRequest",
        "message": "Property 'classification' is read-only and cannot be set.",
        "innerError": {
            "request-id": "...",
            "date": "2018-05-28T14:22:55"
        }
    }
}

The answer is however yes it is possible: in order to set classification of O365 group via Graph API you need to use beta endpoint instead of v1.0: https://graph.microsoft.com/beta/groups/{id}. In this case property should be successfully updated.

Note that team site classification is stored in own property Site.Classification. So if you updated group’s classification you may also want to update classification of related site. If you use SiteExtension.SetSiteClassification from OfficeDevPnP both actions will be done automatically.

Friday, May 25, 2018

Solve problem with missing Get-AzureADDirectorySetting and Set-AzureADDirectorySetting PowerShell cmdlets

If you want to play with Office 365 classifications you will need to configure tenant-level directory settings which contains available values which can be used for groups’ classifications. You may do it using Get-AzureADDirectorySetting and Set-AzureADDirectorySetting cmdlets like shown in the following article: Classifications for Office365 Groups and Microsoft Teams. But if you will follow this article and install both AzureAD and AzureADPreview modules from PowerShell gallery:

Install-Module AzureAD
Install-Module AzureADPreview

and then will connect to Azure AD using command:

Connect-AzureAD

and after that will try to use Get-AzureADDirectorySetting cmdlet you may get the following error:

The term 'Get-AzureADDirectorySetting' is not recognized as the name of a cmdlet,function, script file, or operable program

The problem is that on the moment of writing this post Get-AzureADDirectorySetting cmdlet was defined only in AzureADPreview module. You may check it if will execute the following command:

man Get-AzureADDirectory*

For me it produced the following result:

Name                              Category  Module                    Synopsis
----                              --------  ------                    --------
Get-AzureADDirectoryRole          Cmdlet    AzureADPreview            Gets a directory role.
Get-AzureADDirectoryRoleMember    Cmdlet    AzureADPreview            Gets members of a directory role.
Get-AzureADDirectoryRoleTemplate  Cmdlet    AzureADPreview            Gets directory role templates.
Get-AzureADDirectorySetting       Cmdlet    AzureADPreview            Gets a directory setting.
Get-AzureADDirectorySettingTem... Cmdlet    AzureADPreview            Gets a directory setting template.
Get-AzureADDirectoryRoleMember    Cmdlet    AzureAD                   Get-AzureADDirectoryRoleMember...
Get-AzureADDirectoryRole          Cmdlet    AzureAD                   Get-AzureADDirectoryRole...
Get-AzureADDirectoryRoleTemplate  Cmdlet    AzureAD                   Get-AzureADDirectoryRoleTemplate..

As you can see Get-AzureADDirectorySetting cmdlet is defined in AzureADPreview.

So solution is to start new PowerShell session and connect to Azure AD using Connect-AzureAD from AzureADPreview module:

AzureADPreview\Connect-AzureAD

After that Get-AzureADDirectorySetting and Set-AzureADDirectorySetting cmdlets should become available.

Wednesday, May 16, 2018

Redirect Sharepoint WebDAV network locations from http to https

As you probably know it is possible to connect to Sharepoint sites and doclibs via WebDAV in Wnidows explorer view, by creating new network location or by mapping new network drive to Sharepoint site. If your site works via http WebDAV will use http, if site uses https – WebDAV also will work over https. What if you want to force users to use https instead of http?

First of all you have to configure your Sharepoint stie to use https (purchase SSL certificate, install it on your IIS server, add https binding to Sharepoint site in IIS manager, change alternate access mappings in Central administration). After that you need to redirect traffic from http to https. In order to do that use URL Rewrite IIS module and add new blank rule which looks like this:

2018-05-16_11-27-55

This rule will redirect http requests to https in browser. But also it will redirect http to https for WebDAV clients. If you will open fiddler and will try to open network location which uses http url then you will see something like that (test was done on Windows 10 client):

Request headers:

PROPFIND http://example.com
User-Agent: Microsoft-WebDAV-MiniRedir/10.0.16299

Response headers:

HTTP/1.1 301 Moved Permanently
Location: https://example.com

Where instead of example.com will be url of your Sharepoint site. I.e. URL Rewrite IIS module successfully redirects traffic also for WebDAV.

Tuesday, May 8, 2018

Problem with sync delay between Azure AD and Sharepoint Online when Rest API is used

When you create user or group in Azure AD it is not immediately available in Sharepoint Online. I wrote about this problem here: Problem with delayed propagation of Azure AD groups to Sharepoint Online. In this post I will describe another interesting problem which may occur because of this delay.

Azure AD group members and owners may be retrieved with Graph API and with Rest API:

Graph AP endpoint:

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

Rest API endpoint

http://example.com/_api/SP.Directory.DirectorySession/Group('{groupId}')/members?$select=displayName,id

where instead of http://example.com you need to use url of your Sharepoint site.

The problem is that until Azure AD data won’t be fully synced to Sharepoint Online Rest API may return not correct data. E.g. /members endpoint may return actually owners, while /owners endpoint may not return users at all. Depending on how fast MS data center will propagate changes it may take up to several hours. So be aware about this problem.

The main advantage of Rest API endpoint is that it returns members count. While in Gtaph API $count query string parameter is not supported for users and groups: Use query parameters to customize responses:

Note: $count is not supported for collections of resources that derive from directoryObject like collections of users or groups.

So you may want to use Rest but notice that it may work incorrectly first several hours.

Friday, April 20, 2018

List Azure AD groups via CSOM in Powershell

Recently I showed how to list Azure AD/O365 groups in Powershell using REST Graph API: see List Azure AD groups via Rest Graph API in Powershell. In this post I will show how to do the same thing using CSOM. It may be interesting especially in part of how to obtain access token based on client id and client secret via CSOM and how it differs from REST approach.

In order to fully understand the code you may also need to check another article: Avoiding StackOverflowException when use assembly binding redirect in PowerShell. It shows how to use C#-based assembly binding redirect in PowerShell (native Powershell assembly binding redirect which is described here Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection causes StackOverflowException when you try to use Graph API).

So here is the script:

param
(
	[Parameter(Mandatory=$true)]
	$Tenant,
	[Parameter(Mandatory=$true)]
	$ClientId,
	[Parameter(Mandatory=$true)]
	$ClientSecret
)

$currentDir = Convert-Path(Get-Location)
$dllCommonDir = resolve-path($currentDir + "\..\Assemblies\Common\")
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")
$newtonJsonDir = resolve-path($currentDir + "\..\packages\Newtonsoft.Json.10.0.3\lib\net45\")

$AssemblyNewtonJson = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($newtonJsonDir, "Newtonsoft.Json.dll"))
$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        //Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        //Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

function GetAccessToken($tenant, $clientId, $clientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $clientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($clientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

function ListGroups($accessToken)
{
	$existingGroups = [OfficeDevPnP.Core.Framework.Graph.UnifiedGroupsUtility]::ListUnifiedGroups($accessToken, "", "")
	foreach($group in $existingGroups)
	{
		Write-Host $group.DisplayName
	}
}

$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
ListGroups $accessToken

Tuesday, April 17, 2018

List Azure AD groups via Rest Graph API in Powershell

The following PowerShell snipped shows how to acquire acces token based on client id/client secret via REST Graph API and list all Azure AD groups in Powershell:

param
(
	[Parameter(Mandatory=$true)]
	[string]$Tenant,
	[Parameter(Mandatory=$true)]
	[string]$ClientId,
	[Parameter(Mandatory=$true)]
	[string]$ClientSecret
)

$currentDir = [System.IO.Directory]::GetCurrentDirectory()
$dllCommonDir = resolve-path($currentDir + "\..\..\Assemblies\Common\")
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($dllCommonDir, "Microsoft.Identity.Client.dll"))

function GetAccessToken($tenant, $clientId, $clientSecret)
{
	$appCredentials = New-Object Microsoft.Identity.Client.ClientCredential -ArgumentList $clientSecret
	$aadLoginUri = New-Object System.Uri -ArgumentList "https://login.microsoftonline.com/"
	$authorityUri = New-Object System.Uri -ArgumentList $aadLoginUri, $tenant
	$authority = $authorityUri.AbsoluteUri
	$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
	$clientApplication = New-Object Microsoft.Identity.Client.ConfidentialClientApplication($clientId, $authority, $redirectUri, $appCredentials, $null, $null)
	[string[]]$defaultScope = @("https://graph.microsoft.com/.default")
	$authenticationResult = $clientApplication.AcquireTokenForClientAsync($defaultScope).Result
	return $authenticationResult.AccessToken
}

function RetrieveGroupsRest($accessToken)
{
	$authHeader = @{
		"Content-Type"="application\json"
		"Authorization"="Bearer " + $accessToken
		}

	$uri = "https://graph.microsoft.com/v1.0/groups"
    $result = @()
    do{
        $objects = Invoke-RestMethod -Uri $uri -Headers $authHeader -Method Get
        $uri = $objects.'@odata.nextlink'
        $result = $result + $objects.value
       
    }until ($uri -eq $null)
	return $result
}

$accessToken = GetAccessToken $Tenant $ClientId $ClientSecret
$dataFromGraphAPI = RetrieveGroupsRest $accessToken
$dataFromGraphAPI | ft -Property id,displayName

Thursday, April 5, 2018

Fix error Failed to call GetTypes on assembly Microsoft.Office.TranslationServices after installing Sharepoint 2013 March 2013 CU

After installing Sharepoint 2013 March 2013 CU (and higher) you may get the following error when try to do something in the site:

Failed to call GetTypes on assembly Microsoft.Office.TranslationServices, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c. Method not found

In order to fix it try the following steps:

1. On your Sharepoint server go to the GAC and find folder of Microsoft.Office.TranslationServices.dll assembly (e.g. C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Office.TranslationServices\v4.0_15.0.0.0__71e9bce111e9429c)

2. Open folder in Explorer > right click on Microsoft.Office.TranslationServices.dll > Properties > Details and check actual assembly version:

2018-04-05_10-16-33

in this example full version is 15.0.4420.1017.

3. Go to virtual folder of your Sharepoint site (C:\inetpub\wwwroot\wss\VirtualDirectories\{Site})

4. Edit web.config > find <runtime>/<assemblyBinding> section and add the following section to the end:

<dependentAssembly xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity name="Microsoft.Office.TranslationServices" publicKeyToken="71e9bce111e9429c" culture="neutral" />
  <bindingRedirect oldVersion="1.0.0.0-15.0.0.0" newVersion="15.0.4420.1017" />
</dependentAssembly>
<dependentAssembly xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity name="Microsoft.Office.TranslationServices.ServerStub" publicKeyToken="71e9bce111e9429c" culture="neutral" />
  <bindingRedirect oldVersion="1.0.0.0-15.0.0.0" newVersion="15.0.4420.1017" />
</dependentAssembly>

Note that it is important to specify both assemblies - if you will only specify Microsoft.Office.TranslationServices you will still get same error when SharePoint will try to call /_vti_bin/client.svc (which in turn may cause other unclear problems like error "The server was unable to save the form at this time. Please try again" when you try to create or edit list item). After that error should disappear. If similar error will occur for different assembly try to add assembly bindnig redirection for it as well.

Wednesday, March 28, 2018

Avoid problem with redirecting Sharepoint sites which use .dev sub domains to https in Chrome and Firefox

As you probably heard some time ago Chrome and FF started to redirect sites which use .dev sub domains in URL to https. Issue is described e.g. here: Chrome & Firefox now force .dev domains to HTTPS via preloaded HSTS. Briefly .dev domain is owned by Google and they added HSTS rule for it which forces redirection to https. You may check it in Chrome: open chrome://net-internals#hsts and make query for dev domain:

2018-03-28_16-38-32

As you can see there is FORCE_HTTPS rule.

This problem may occur in local dev Sharepoint environments which often use .dev domain. So if you have e.g. site http://intranet.sp.dev and will try to open it in Chrome or Firefox it won’t open it with unclear error:

2018-03-28_16-37-56

But if you notice url address bar of the browser you will find that it actually tries to open https://intranet.sp.dev instead of http://intranet.sp.dev and it causes issue. You may try to add https binding to the site using self-signed certificate, but browser will still show error that connection is not secure.

The following solution may be used for avoiding this problem with Sharepoint site. It requires using new site address http://intranet.sp.local for your Sharepoint site instead of http://intranet.sp.dev:

  • add intranet.sp.local binding for your Sharepoint site in IIS Manager
  • in C:/Windows/System32/drivers/etc/host add
      127.0.0.1 intranet.sp.local
  • in Central administration > Alternate access mappings > Add internal url for not used zone: http://intranet.sp.local

After that site can be opened in Chrome and FF using http://intranet.sp.local url.

Monday, March 26, 2018

Avoiding StackOverflowException when use assembly binding redirect in PowerShell

Recently I faced with interesting problem with assembly binding redirect in PowerShell: in my script I needed to use the following versions of the assemblies (these exact versions were used in other components and I couldn’t add another versions because of number of reasons):

OfficeDevPnP.Core 2.22.1801.0
Microsoft.Graph 1.7.0.0
Microsoft.Graph.Core 1.7.0.0

The problem is that OfficeDevPnP.Core 2.22.1801.0 references different versions of Microsoft.Graph and Microsoft.Grap.Core:

Microsoft.Graph 1.1.1.0
Microsoft.Graph.Core 1.2.1.0

So I needed to use assembly binding redirect. At first I tried approach described in the following post: Use specific version of Sharepoint client object model in PowerShell via assembly binding redirection:

$currentDir = Convert-Path(Get-Location)
$pnpCoreDir = resolve-path($currentDir + "\..\packages\SharePointPnPCoreOnline.2.22.1801.0\lib\net45\")
$graphDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.1.7.0\lib\net45\")
$graphCoreDir = resolve-path($currentDir + "\..\packages\Microsoft.Graph.Core.1.7.0\lib\net45\")

$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
[System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($pnpCoreDir, "OfficeDevPnP.Core.dll"))


<#$OnAssemblyResolve = [System.ResolveEventHandler] {
	param($sender, $e)
	
	if ($e.Name.StartsWith("Microsoft.Graph,"))
	{
		$AssemblyGraph = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphDir, "Microsoft.Graph.dll"))
		return $AssemblyGraph
	}
	if ($e.Name.StartsWith("Microsoft.Graph.Core,"))
	{
		$AssemblyGraphCore = [System.Reflection.Assembly]::LoadFile([System.IO.Path]::Combine($graphCoreDir, "Microsoft.Graph.Core.dll"))
		return $AssemblyGraphCore
	}

	foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
	{
		if ($a.FullName -eq $e.Name)
		{
		  return $a
		}
	}
	Write-Host "Return null" -foregroundcolor red
	return $null
}
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

However attempt to call method from loaded assemblies caused StackOverflowException and closing PowerShell session.

After that I tried C#-based assembly redirector (found it in the following forum thread: Powershell - Assembly binding redirect NOT found in application configuration file):

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

try
{
    $redirector = [Redirector]::new($null)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    Write-Warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

And surprisingly this approach worked. It looks like a bug in PowerShell script-based assembly binding redirect. Hope it will help someone.

Wednesday, March 21, 2018

How to reset credentials for Sharepoint Designer

Here are instructions of how to reset credentials for Sharepoint Designer:

1. Go to Windows control panel and select User accounts:

01

2. In opened window click Manage your credentials:

02

3. Then choose Windows credentials:

03

4. On credentials window under Generic credentials there will be account which start with “MicrosoftOffice15_Data:” prefix (this is a prefix for Sharepoint Designer 2013. For other versions most probably prefix will be different):

04

Find those which was cached for your site and remove it from the list.

After that if you will open this site again in Sharepoint Designer it will ask to enter credentials.

Thursday, March 15, 2018

How to renew expired app in Sharepoint Online

As you probably know when you register new app in Sharepoint (using /_layouts/15/AppRegNew.aspx) it’s expiration date is set to 1 year from moment of registration. If your app is expired perform the following steps in order to renew it on 3 years:

1. Run Windows PowerShell and connect to Msol:

Connect-MsolService

2. Get list of all apps:

Get-MsolServicePrincipal -all | Where-Object -FilterScript { ($_.DisplayName -notlike "*Microsoft*") -and ($_.DisplayName -notlike "autohost*") -and  ($_.ServicePrincipalNames -notlike "*localhost*") } | Out-File log_apps.txt -Append

3. From generated log_apps.txt copy AppPrincipalId for expired app

4. Get list of all principals:

Get-MsolServicePrincipalCredential -AppPrincipalId {copied_app_principal_id} -ReturnKeyValues $true | Out-File log_principals.txt -Append
5. Check end dates for app principals. If they are expired run the following script which will generate new client secret and renew principals on 3 years:
# start script
$clientId = "{copied_app_principal_id}"
$bytes = New-Object Byte[] 32
$rand = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rand.GetBytes($bytes)
$rand.Dispose()
$newClientSecret = [System.Convert]::ToBase64String($bytes)
New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Symmetric -Usage Sign -Value $newClientSecret -StartDate (Get-Date) -EndDate (Get-Date).AddYears(3)
New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Symmetric -Usage Verify -Value $newClientSecret -StartDate (Get-Date) -EndDate (Get-Date).AddYears(3)
New-MsolServicePrincipalCredential -AppPrincipalId $clientId -Type Password -Usage Verify -Value $newClientSecret -StartDate (Get-Date) -EndDate (Get-Date).AddYears(3)
Write-Host "New client secret:"
$newClientSecret
# end script

Depending on permissions which are required for your app you may need to run the last script under tenant admin account (if app requires tenant full access).

Tuesday, March 13, 2018

Fix missing usage analytics logs in Sharepoint 2013 event store

Sharepoint 2013 usage analytics reports give to administrators view of site usage statistics. But it is not so simple to configure them to work. Most frequent issue is that Excel reports contain only zeros. There may be many reasons for this problem. One of them is missing log files in EventStore. EventStore is located in "C:\Program Files\Microsoft Office Servers\15.0\Data\Office Server\Analytics_{GUID}\EventStore\" folder on the server. If it is empty check the following in PowerShell:

$aud = Get-SPUsageDefinition | where {$_.Name -like "Analytics*"}
$aud | fl

It will show something like this:

01

Pay attention on EnableReceivers and Receivers – first should be set to True and second to Microsoft.Office.Server.Search.Analytics.Internal.AnalyticsCustomRequestUsageReceiver like shown on the picture. If EnableReceivers is set to False and Receivers is empty execute the following code also in PowerShell:

$aud.Receivers.Add("Microsoft.Office.Server.Search.Applications, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c","Microsoft.Office.Server.Search.Analytics.Internal.AnalyticsCustomRequestUsageReceiver")
$aud.EnableReceivers = $true
$aud.Update()

After that check also requests usage:

$prud = Get-SPUsageDefinition | where {$_.Name -like "Page Requests"}
$prud | fl

Which should look like this:

02

Here also if EnableReceivers is set to False and Receivers is empty execute the following code:

$prud.Receivers.Add("Microsoft.Office.Server.Search.Applications, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c", "Microsoft.Office.Server.Search.Analytics.Internal.ViewRequestUsageReceiver")
$prud.EnableReceivers = $true
$prud.Update()

After that go to the site and click some links to generate traffic. Log files should be created in EventStore folder.

Thursday, March 1, 2018

How to check does site collection exist by absolute url using CSOM in Sharepoint

Suppose that we need to check whether site collection with specified absolute url exists or not using client side object model. In OfficeDevPnP library there is convenient extension method for ClientContext WebExtensions.WebExistsFullUrl:

        public static bool WebExistsFullUrl(this ClientRuntimeContext context, string webFullUrl)
        {
            bool exists = false;
            try
            {
                using (ClientContext testContext = context.Clone(webFullUrl))
                {
                    testContext.Load(testContext.Web, w => w.Title);
                    testContext.ExecuteQueryRetry();
                    exists = true;
                }
            }
            catch (Exception ex)
            {
                if (IsUnableToAccessSiteException(ex) || IsCannotGetSiteException(ex))
                {
                    exists = true;
                }
            }
            return exists;
}

Unfortunately this method works properly only within single site collection i.e. when client context (which is extended with this extension method) and url to check belong to the same managed path.

Example:
context is created from the root site http://example.com. This site has http://example.com/test sub site. In this case WebExistsFullUrl returns true for http://example.com/test url and false for some non-existent sub site's url like http://example.com/test123. I.e. behavior is correct.

But if context and url to check belong to different managed path then it always returns true.

Example:
context is created from the root site http://example.com and we call WebExistsFullUrl for some url which may belong to other site collection (e.g. which uses different managed path like http://example.com/teams/some-not-real-url. Suppose that at the moment of call we don't know whether this collection exists or not - we want to determine it by WebExistsFullUrl call). In this case WebExistsFullUrl returns true even if site collection with specified url doesn't exists.

When I analyzed the code I found the following. When we call this method with context and url which belong to the same managed path it throws exception like expected. But when it is called with context and url which belong to different managed paths exception is not thrown. Instead context is created for the root site http://example.com and method returns true and caller thinks that site exists. In order to fix this problem for different managed paths I applied the following fix in our local version:

        public static bool WebExistsFullUrl(this ClientRuntimeContext context, string webFullUrl)
        {
            bool exists = false;
            try
            {
                using (ClientContext testContext = context.Clone(webFullUrl))
                {
                    testContext.Load(testContext.Web, w => w.Title, w => w.Url);
                    testContext.ExecuteQueryRetry();
                    exists = (string.Compare(testContext.Web.Url, webFullUrl, true) == 0);
                }
            }
            catch (Exception ex)
            {
                if (IsUnableToAccessSiteException(ex) || IsCannotGetSiteException(ex))
                {
                    // Site exists, but you don't have access .. not sure if this is really valid
                    // (I guess if checking if URL is already taken, e.g. want to create a new site
                    // then this makes sense).
                    exists = true;
                }
            }
            return exists;
}

I.e. before to return true method also checks whether loaded url is the same as url to be checked. If they are different (which happens in scenario with different managed paths) it will return false like it should.

Described behavior was found when root site collection (http://example.com) was host-named site collection, but most probably it will also work same way for regular site collections.

This problem is also submitted to OfficeDevPnP Core issues section on GitHub: WebExtensions.WebExistsFullUrl returns true for non-existent sites from different managed path in Sharepoint 2013 on-premise.