Second Life of a Hungarian SharePoint Geek

May 12, 2017

Getting a List of Checked-Out Projects from PowerShell via REST

Filed under: PowerShell, Project Server, REST — Tags: , , — Peter Holpar @ 05:22

We have an application running as a monthly scheduled batch process that updates enterprise fields of projects on our Project Server implementation based on values taken from various satellite systems. As a prerequisite, all affected projects should be checked-in. Projects checked-out to users won’t be updated. Of course, technically it would be possible to force check-in on those projects, but it was a business decision not to do that as we wanted to avoid data inconsistency by checking in a project that is not yet meant to be ready for that by the project manager.

Our application iterates through the projects, and if they are checked-in, it checks them out, updates the values, checks the project back and publishes it. If the project is checked-out, it sends a warning to the owner that the project was not updated due to its state. Occasionally project owners are doubting this warning, saying they are sure to check-in their projects, so I decided to create a sort-of report running just before starting the updating process to prove the list of projects left checked-out. It is something similar administrators already have on the Force Check-in Enterprise Objects page under PWA Settings.

image

Recently I wrote about how we can use PowerShell to create simple reports based on the data we query via the REST interface. This time I applied the very same technique to get the list of projects that are checked-out, including the name of the projects, the check-out description, checked-out time and the name and e-mail address of the user checked-out the project. The key was to assemble the REST query URL, including the $expand expression for the CheckedOutBy field.

$url = ‘http://YourProjectServerPWA/_api/ProjectServer/Projects?$expand=CheckedOutBy&$select=Name,CheckOutDescription,CheckedOutDate,CheckedOutBy/Title,CheckedOutBy/Email&$filter=IsCheckedOut’

$request = [System.Net.WebRequest]::Create($url)
$request.UseDefaultCredentials = $true
$request.Accept = ‘application/json;odata=verbose’

$response = $request.GetResponse()
$reader = New-Object System.IO.StreamReader $response.GetResponseStream()
$data = $reader.ReadToEnd()

$result = ConvertFrom-Json -InputObject $data

$result.d.results | % {
select -Input $_ -Prop `
    @{ Name=’Name’; Expression={$_.Name} },
    @{ Name=’User’; Expression={$_.CheckedOutBy.Title} },
    @{ Name=’EMail’; Expression={$_.CheckedOutBy.Email} },
    @{ Name=’Date’; Expression={[DateTime]::Parse($_.CheckedOutDate).ToString(‘g’)} },
    @{ Name=’Description’; Expression={$_.CheckOutDescription} }
    } | Export-Csv -Path CheckedOutProjects.csv -Delimiter ";" -Encoding UTF8 –NoTypeInformation

The result is a comma separated value (.csv) file, that one can open in Excel easily as well.

"The file name you specified is not valid or too long. Specify a different file name." Error When Using Redirection in IIS

Filed under: Explorer View, SP 2013, WebDAV — Tags: , , — Peter Holpar @ 05:18

Recently a user complained, that although he can create and copy files on a mapped drive on his Windows 7, linked to a SharePoint document library, the following error message was displayed to him in the Windows Explorer view of the library when he tried to rename any file:

The file name you specified is not valid or too long. Specify a different file name.

image

The error message was already known to us, it is typically a result of a special character or a space in the URL that is being encoded, and used in this encoded form to map the drive, or the mapped path might contain a trailing slash ‘/’, see threads here and here.

In this case there wasn’t any issue with the characters, but as we checked the mapping via the NET USE command, we noticed that the connection was listed as

\\YourServer\DocLib

although the SharePoint site was configured to use HTTPS (let’s say with URL https://YourServer), so the connection should have been actually:

\\YourServer@SSL\DocLib

On the SharePoint server (SharePoint 2013 on Window Server 2012 R2) we verified the configuration in Internet Information Services (IIS) Manager, and found the HTTPS binding all right.

There was however an other web site with the very same binding as the SharePoint site, but instead of HTTPS it was bound to HTTP (that means http://YourServer). The sole purpose of this web site was to forward any incoming HTTP request to the SharePoint site using HTTP Redirect with the settings below (see this page for configuration details):

Redirect requests to this destination option checked: https://YourServer$S$Q

Redirect all requests to exact destination (instead of relative to destination) option checked

image

The solution was in this case so simple as to disconnect the mapped folder and to reconnect it using HTTPS:

NET USE Y: "https://YourServer/DocLib"

Conclusion of the story: Redirection apparently works with WebDAV as well, however renaming files fails in this case.

Disabling SharePoint Alerts Temporarily for a Specific SharePoint List

Filed under: Alerts, PowerShell, SP 2013 — Tags: , , — Peter Holpar @ 05:16

Recently we extended a SharePoint list in our test environment with a few new fields. Users have been complained that they received immediate notifications due to their existing subscriptions on the list. To avoid the same situation in the live system, we decided to temporarily deactivate the alerts for the time of the list field extension. I find a solution for that in this thread, implemented in C#. Although I like C#, for administrative tasks like this one I prefer using PowerShell, so I transformed the code into a few-line script:

$url = ‘http://YourSharePoint/WebSite’
$listTitle = ‘Title of your list’
$targetStatus = [Microsoft.SharePoint.SPAlertStatus]::Off # or [Microsoft.SharePoint.SPAlertStatus]::On

$web = Get-SPWeb $url
$list = $web.Lists[$listTitle]

# to query the current status of the alerts only:
# $web.Alerts | ? { $_.List.ID -eq $list.ID } | % { $_.Status }

$web.Alerts | ? { $_.List.ID -eq $list.ID } | % {
  $_.Status = $targetStatus
  $_.Update()
}

After implementing the changes, you can reactivate the alerts (in this case you should use the value [Microsoft.SharePoint.SPAlertStatus]::On in $targetStatus), however, you should wait a few minutes, as the immediate alerts are sent every 5 minutes by default (see screenshot below). If you turn the alerts on before the next run of the job, your previous change to inactivate the notifications has no effect and the alerts would be sent to the user.

image

By letting the Immediate Alerts job to have a run after you make the changes in the list, the notification events waiting in the event queue will be purged and not included in the upcoming immediate alerts. They will be however included in the daily and weekly summaries, but that was not an issue in our case.

If you don’t want to wait for the next scheduled run, you can start the job from the UI (see Run Now button above), or via script like this:

Get-SPTimerJob | ? { $_.Name -eq "job-immediate-alerts"} | % { Start-SPTimerJob $_ }

March 29, 2017

Working with the REST / OData Interface from PowerShell

Filed under: OData, PowerShell, REST, SP 2013 — Tags: , , , — Peter Holpar @ 20:56

If you follow my blog you might already know that I am not a big fan of the REST / OData interface. I prefer using the client object model. However there are cases, when REST provides a simple (or even the only available) solution.

For example, we are working a lot with PowerShell. If you are working with SharePoint on the client side at a customer, and you are not allowed to install / download / copy the assemblies for the managed client object model (CSOM), you have a problem.

Some possible reasons (you should know, that the SharePoint Server 2013 Client Components SDK is available to download as an .msi, or you can get the assemblies directly from an on-premise SharePoint installation):

  • You might have no internet access, so you cannot download anything from the web.
  • If you happen to have internet access, you are typically not allowed to install such things without administrator permissions on the PC. It’s quite rare case, if you or the business user you are working with has this permission.
  • You have no direct access on the SharePoint server, so you cannot copy the assemblies from it.
  • You are not allowed to use your own memory stick (or other storage device) to copy the assemblies from it.
  • Even if there is no technical barrier, company policies might still prohibit you using external software components like the CSOM assemblies.

In this case, using the REST interface is a reasonable choice. You can have a quick overview of the REST-based list operations here.

The main questions I try to answer in this post:

  • Which object should I use to send the request?
  • How to authenticate my request?
  • How to build up the payload for the request?

First of all, I suggest you to read this post to learn some possible pitfalls when working with REST URLs from PowerShell and how to avoid them with escaping.

Reading data with the SharePoint REST interface

Reading data with a GET request

Sending a GET request for a REST-based service in PowerShell is not really a challenge, might you think, and you are right, it is really straightforward most of the cases. But take the following example, listing the Id and Title fields of items in a list:

$listTitle = "YourList"
$url = "http://YourSharePoint/_api/Web/Lists/GetByTitle(‘$listTitle‘)/Items?`$select=Id,Title"

$request = [System.Net.WebRequest]::Create($url)
$request.UseDefaultCredentials = $true
$request.Accept = ‘application/json;odata=verbose’

$response = $request.GetResponse()
$reader = New-Object System.IO.StreamReader $response.GetResponseStream()
# ConvertFrom-Json : Cannot convert the Json string because a dictionary converted from it contains duplicated keys ‘Id’ and ‘ID’.
#$response = $reader.ReadToEnd()
$response = $reader.ReadToEnd() -creplace ‘"ID":’, ‘"DummyId":’

$result = ConvertFrom-Json -InputObject $response
$result.d.results | select Id, Title

If you would use

$response = $reader.ReadToEnd()

instead of

$response = $reader.ReadToEnd() -creplace ‘"ID":’, ‘"DummyId":’

then you became this exception, when trying to convert the JSON response:

ConvertFrom-Json : Cannot convert the Json string because a dictionary converted from it contains duplicated keys ‘Id’ and ‘ID’.

The reason, that the JSON response of the server contains the fields Id and ID. JSON is case-sensitive, but PowerShell is not, so it is an issue if you want to convert the JSON response to a PowerShell object. You can read more about it in this post, although I don’t like the solution proposed there. Although it really helps to avoid the error, but it uses the case insensitive replace operator instead of the case sensitive creplace, so it converts both fields into a dummy field. PowerShell seems to have no problem with the duplicated properties.

Instead of using a System.Net.WebRequest object, we can achieve a shorter version using the Invoke-RestMethod cmdlet. Note, that we don’t select and display the Id property in this case to avoid complications. See my comments about that in the next section discussing the POST request.

$listTitle = "YourList"
$url = "http://YourSharePoint/_api/Web/Lists/GetByTitle(‘$listTitle‘)/Items?`$select=Title"
$headers = @{ ‘Accept’ = ‘application/json; odata=verbose’}
$result = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -UseDefaultCredentials
$result.d.results | select Title

Reading data with a POST request

There are cases when you have to use the POST method instead of GET to read some data from SharePoint. For example, if you need to filter the items via a CAML query. In the following example I show you how to query the file names all documents in a library recursively that are older than a threshold value:

$listTitle = "YourDocuments"
$offsetDays = -30

$urlBase = "http://YourSharePointSite/"
$urlAuth = $urlBase +"_api/ContextInfo"
$url = $urlBase + "_api/Web/Lists/GetByTitle(‘$listTitle’)/GetItems?`$select=FileLeafRef"

$viewXml = "<View Scope=’Recursive’><ViewFields><FieldRef Name=’Created’/><FieldRef Name=’FileLeafRef’/></ViewFields><Query><Where><Lt><FieldRef Name=’Created’ /><Value Type=’DateTime’><Today OffsetDays=’$offsetDays’ /></Value></Lt></Where></Query></View>"

$queryPayload = @{ 
                   ‘query’ = @{
                          ‘__metadata’ = @{ ‘type’ = ‘SP.CamlQuery’ };                      
                          ‘ViewXml’ = $viewXml
                   }
                 } | ConvertTo-Json

# authentication
$auth = Invoke-RestMethod -Uri $urlAuth -Method Post -UseDefaultCredentials
$digestValue = $auth.GetContextWebInformation.FormDigestValue

# the actual request
$headers = @{ ‘X-RequestDigest’ = $digestValue; ‘Accept’ = ‘application/json; odata=verbose’ }
$result = Invoke-RestMethod -Uri $url -Method Post -Body $queryPayload -ContentType ‘application/json; odata=verbose’ -Headers $headers –UseDefaultCredentials

# displaying results
$result.d.results | select FileLeafRef

Just for the case of comparison I include the same payload in JavaScript format:

var queryPayload = {
                     ‘query’ : {
                        
‘__metadata’ : { ‘type’ : ‘SP.CamlQuery’ },
                         ‘ViewXml’ : viewXml
                    
}
                   };

As you can see, these are the most relevant differences in the format we need in PowerShell:

  • We use an equal sign ( = ) instead of  ( : ) to separate the name and its value.
  • We use a semicolon ( ; ) instead of the comma ( , ) to separate object fields.
  • We need a leading at sign ( @ ) before the curly braces ( { ).

The Invoke-RestMethod tries to automatically convert the response to the corresponding object based on the content type of the response. If it is an XML response (see the authentication part above) then the result will be a XmlDocument. If it is a JSON response then the result will be a PSCustomObject representing the structure of the response. However, if the response can not be converted, it remains a single String.

For example, if we don’t limit the fields we need in response via the $select query option:

$url = $urlBase + "_api/Web/Lists/GetByTitle(‘$listTitle’)/GetItems"

then the response includes the fields Id and ID again. In this case we should remove one of these fields using the technique illustrated above with the simple GET request, before we try to convert the response via the ConvertFrom-Json cmdlet.

Note: If you still use PowerShell v3.0 you get this error message when you invoke Invoke-RestMethod setting the Accept header:

Invoke-RestMethod : The ‘Accept’ header must be modified using the appropriate property or method.
Parameter name: name

So if it is possible, you should consider upgrading to PowerShell v4.0. Otherwise, you can use the workaround suggested in this forum thread, where you can read more about the issue as well.

If you are not sure, which version you have, you can use $PSVersionTable.PSVersion to query the version number, or another option as suggested here.

Creating objects

In this case we send a request with the POST method to the server. The following code snippet shows, how you can create a new custom list:

$listTitle = "YourList"

$urlBase = "http://YourSharePoint/&quot;
$urlAuth = $urlBase +"_api/ContextInfo"
$url = $urlBase + "_api/Web/Lists"

$queryPayload = @{ 
                    ‘__metadata’ = @{ ‘type’ = ‘SP.List’ }; ‘AllowContentTypes’ = $true; ‘BaseTemplate’ = 100;
                    ‘ContentTypesEnabled’ = $true; ‘Description’ = ‘Your list description’; ‘Title’ = $listTitle                      
    } | ConvertTo-Json

$auth = Invoke-RestMethod -Uri $urlAuth -Method Post -UseDefaultCredentials
$digestValue = $auth.GetContextWebInformation.FormDigestValue

$headers = @{ ‘X-RequestDigest’ = $digestValue; ‘Accept’ = ‘application/json; odata=verbose’ }

$result = Invoke-RestMethod -Uri $url -Method Post -Body $queryPayload -ContentType ‘application/json; odata=verbose’ -Headers $headers –UseDefaultCredentials

The response we receive in the $result variable contains the properties of the list we just created. For example, the Id (GUID) of the list is available as $result.d.Id.

Updating objects

In this case we send a request with the POST method to the server and set the X-HTTP-Method header to MERGE. The following code snippet shows, how to change the title of the list we created in the previous step:

$listTitle = "YourList"

$urlBase = "http://YourSharePoint/&quot;
$urlAuth = $urlBase +"_api/ContextInfo"
$url = $urlBase + "_api/Web/Lists/GetByTitle(‘$listTitle’)"

$queryPayload = @{ 
                    ‘__metadata’ = @{ ‘type’ = ‘SP.List’ }; ‘Title’ = ‘YourListNewTitle’                      
    } | ConvertTo-Json

$auth = Invoke-RestMethod -Uri $urlAuth -Method Post -UseDefaultCredentials
$digestValue = $auth.GetContextWebInformation.FormDigestValue

$headers = @{ ‘X-RequestDigest’ = $digestValue; ‘Accept’ = ‘application/json; odata=verbose’; ‘IF-MATCH’ = ‘*‘; ‘X-HTTP-Method’ = ‘MERGE’ }

$result = Invoke-RestMethod -Uri $url -Method Post -Body $queryPayload -ContentType ‘application/json; odata=verbose’ -Headers $headers –UseDefaultCredentials

Deleting objects

In this case we send a request with the POST method to the server and set the X-HTTP-Method header to DELETE. The following code snippet shows, how you can delete a list item:

$listTitle = "YourList"

$urlBase = "http://YourSharePoint/&quot;
$urlAuth = $urlBase +"_api/ContextInfo"
$url = $urlBase + "_api/Web/Lists/GetByTitle(‘$listTitle’)/Items(1)"

# authentication
$auth = Invoke-RestMethod -Uri $urlAuth -Method Post -UseDefaultCredentials
$digestValue = $auth.GetContextWebInformation.FormDigestValue

# the actual request
$headers = @{ ‘X-RequestDigest’ = $digestValue; ‘IF-MATCH’ = ‘*’; ‘X-HTTP-Method’ = ‘DELETE’ }
$result = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -UseDefaultCredentials

Note: Although the documentation states, that “in the case of recyclable objects, such as lists, files, and list items, this results in a Recycle operation”, based on my tests it is false, as the objects got really deleted.

Final Note: This one applies to all of the operations discussed in the post. If the SharePoint site you are working with available via HTTPS and there is an issue with the certificate, you can turn off the certificate validation, although it is not recommended in a production environment. You should include this line in your code before making any web requests:

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }

How to Create a Simple “Printer Friendly” Display Form

Filed under: JavaScript, SP 2013, SPD — Tags: , , — Peter Holpar @ 05:44

Our users needed a simply way to print items in SharePoint, that mean only item properties without any ribbon or navigation elements.

Assuming you have a list ‘YourCustomList’ available at the URL http://YourSharePoint/Lists/YourCustomList, the standard display form of a list item (in this case the one with ID 1) would be:

http://YourSharePoint/Lists/YourCustomList/DispForm.aspx?ID=1

This page contains however the site navigation elements and the ribbon as well. Appending the query string parameter IsDlg=1 (like http://YourSharePoint/Lists/YourCustomList/DispForm.aspx?ID=1&IsDlg=1) helps to remove the navigation parts, but the ribbon remains.

Our solution to remove the ribbon was to add this very simple JavaScript block via a Script Editor Web Part to the display form page (DispForm.aspx). I suggest to insert the Script Editor Web Part after the existing List Form Web Part on the page.

// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
function getParameterByName(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, “\\$&”);
    var regex = new RegExp(“[?&]” + name + “(=([^&#]*)|&|#|$)”),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return ”;
    return decodeURIComponent(results[2].replace(/\+/g, ” “));
}

if (getParameterByName(‘IsPrint’) == ‘1’) {
  var globalNavBox = document.getElementById(‘globalNavBox’);
  if (globalNavBox) {
    globalNavBox.style.display = ‘none’;
  }
}

Note: You can switch the display form to page edit mode via the ToolPaneView=2 query string parameter (see more useful hints here), for example:

http://YourSharePoint/Lists/YourCustomList/DispForm.aspx?ToolPaneView=2

The main part of the solution, the getParameterByName method was borrowed from this forum thread. It helps to get a query string parameter value by its name. Using this method we check, if there is a parameter IsPrint, and if it is there having a value of 1, the we make the globalNavBox HTML element, that is actually a placeholder for the ribbon, invisible.

It means, if we call the display form by the URL http://YourSharePoint/Lists/YourCustomList/DispForm.aspx?ID=1&IsDlg=1&IsPrint=1 then there is no ribbon or other navigation element on the page. Using this URL format you can even add a custom action, for example, a new button to the ribbon or an edit control block (ECB) menu-item (see example later in the post), or refer a print form directly from a document or from an e-mail.

In the above case, the users can then print the page via right-clicking with the mouse and selecting Print… from the pop-up menu. Alternatively we could inject a Print button on the form itself. This technique will be demonstrated below.

In this case we use JQuery, and our JavaScript code is a bit more complex, so we store it into a separate file in the Site Assets library of the site, and refer only the files in the Script Editor Web Part:

/font/ema%20href=
http://../../SiteAssets/js/printForm.js

Our JavaScript code (printForm.js) would be in this case:

// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
function getParameterByName(name, url) {
    if (!url) url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
        results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return ”;
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

// https://davidwalsh.name/add-rules-stylesheets
var sheet = (function() {
    // Create the <style> tag
    var style = document.createElement("style");

    // Add a media (and/or media query) here if you’d like!
    style.setAttribute("media", "print")

    // WebKit hack 😦
    style.appendChild(document.createTextNode(""));

    // Add the <style> element to the page
    document.head.appendChild(style);

    return style.sheet;
})();

$(document).ready(function() {
  if (getParameterByName(‘IsPrint’) == ‘1’) {
    sheet.insertRule("#globalNavBox { display:none; }", 0);
    sheet.insertRule("input { display:none; }", 0);

    $(‘input[value="Close"]’).closest(‘tr’).closest(‘tr’).append(‘<td class="ms-toolbar" nowrap="nowrap"><table width="100%" cellspacing="0" cellpadding="0"><tbody><tr><td width="100%" align="right" nowrap="nowrap"><input class="ms-ButtonHeightWidth" accesskey="P" onclick="window.print();return false;" type="button" value="Print"></input></td></tr></tbody></table></td><td class="ms-separator">&nbsp;</td>’);
  }
});

In this case we inject a Print button dynamically and don’t hide the ribbon, but use the technique illustrated here to add CSS styles to hide UI elements (ribbon and the buttons) only in the printed version via the media attribute of the style sheet.

Note: The above code is for a SharePoint site with English UI. Since the value of the Close button is language dependent, you should change the code if you have a SharePoint site with another culture settings. For example, in a German version the JQuery selector would be:

input[value="Schließen"]

In this case you should have to save the script using Unicode encoding instead of ANSI to prohibit the loss of special character ‘ß’.

Finally, I show you how to create a shortcut to the form in the ECB menu using SharePoint Designer (SPD).

Select your list in SPD, and from the Custom Actions menu select the List Item Menu.

image

Set the fields as illustrated below:

image

The full value of the Navigate to URL field:

javascript:OpenPopUpPageWithTitle(ctx.displayFormUrl + ‘&ID={ItemId}&IsDlg=1&IsPrint=1′, RefreshOnDialogClose, 600, 400,’Print Item’)

We use the OpenPopUpPageWithTitle method and a custom made URL to show the printer friendly display form with the necessary query string parameters. See this article on more details of the OpenPopUpPageWithTitle method.

After saving the custom action, you can test it in your list:

image

This is the customized form having the extra Print button on it:

image

And that is the outcome of the print:

image

March 26, 2017

Generating Pseudo GUIDs for Your Project Server Entities

Filed under: PowerShell, Project Server, Tips & Tricks — Tags: , , — Peter Holpar @ 06:24

As you might have known, since the version 2013, Project Server utilizes pseudo-GUIDs to improve Project Server performance. These ones has the format of a “classical” GUID, but actually generated sequentially. As Microsoft states in this TechNet article:

"We handle GUIDs a little better in Project Server 2013 – and in many places they are sequential GUIDs which cause less index fragmentation"

This topic is quite good described in the Project Conference 2014 presentation Project Worst Practice – Learning from other peoples mistakes by Brian Smith. See the video recording between 6:08-13:54, or the slides 10-14.

One of the main components of the pseudo-GUID generation is the NewSequentialUid method of the Microsoft.Office.Project.Server.Library.PSUtility class:

public static Guid NewSequentialUid() 

  Guid guid; 
  if (NativeMethods.UuidCreateSequential(out guid) != 0) 
    return Guid.NewGuid(); 
  byte[] b = guid.ToByteArray(); 
  Array.Reverse((Array) b, 0, 4); 
  Array.Reverse((Array) b, 4, 2); 
  Array.Reverse((Array) b, 6, 2); 
  return new Guid(b); 
}

So if you want to use the same kind of pseudo-GUIDs for your own custom entities you create from code, you can get the IDs by invoking the method (for example, via PowerShell). The code sample below illustrates, how to get a single ID, or a batch of  IDs (in this case, 5 of them):

# load the necessary assembly
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Project.Shared")
# generate a single sequential ID
[Microsoft.Office.Project.Server.Library.PSUtility]::NewSequentialUid()
# or generate a range of sequential IDs, in this case, five of them
(1..5) | % { [Microsoft.Office.Project.Server.Library.PSUtility]::NewSequentialUid().Guid }

March 25, 2017

Microsoft.Workflow.Client.InvalidRequestException: Failed to query the OAuth S2S metadata endpoint – The remote server returned an error: (400) Bad Request

Filed under: Certificates, SP 2013, Workflow — Tags: , , — Peter Holpar @ 21:11

Recently we installed a new Workflow Manager farm (a single-server one) on the front-end server of one of our SharePoint farms.

I wanted to register the Workflow Manager for a web application in the SharePoint farm via the PowerShell cmdlet:

Register-SPWorkflowService -SPSite https://YourSharePointSite -WorkflowHostUri https://YourWorkflowManagerServer:12290 -ScopeName YourScope –Force

But I received an error like this one:

Register-SPWorkflowService : Failed to query the OAuth S2S metadata endpoint
at URI ‘https://YourSharePointSite/_layouts/15/metadata/json/1&#8217;.
Error details: ‘An error occurred while sending the request’. HTTP headers received from the server – ActivityId:
d10c4cbb-bde4-4040-b09f-1ace1491dc87. NodeId: YourWFNode. Scope: /YourScope.
Client ActivityId : b89c2ff9-8560-458e-9ea2-31ec6c8fde36.
At line:1 char:1
+ Register-SPWorkflowService -SPSite https://YourSharePointSite/&#160; -W …

In the Event Viewer (Application and Services Logs / Microsoft-Workflow / Operational) we had this error:

image

Failed to query the remote endpoint for the S2S metadata document. Details: System.Net.Http.HttpRequestException: An error occurred while sending the request. —> System.Net.WebException: The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel. —> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.
   at System.Net.TlsStream.EndWrite(IAsyncResult asyncResult)
   at System.Net.ConnectStream.WriteHeadersCallback(IAsyncResult ar)
   — End of inner exception stack trace —
   at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at System.Net.Http.HttpClientHandler.GetResponseCallback(IAsyncResult ar)
   — End of inner exception stack trace —

In the ULS logs we had this error message:

Microsoft.Workflow.Client.InvalidRequestException: Failed to query the OAuth S2S metadata endpoint at URI ‘https://YourSharePointSite/_layouts/15/metadata/json/1&#8217;. Error details: ‘An error occurred while sending the request.’. HTTP headers received from the server – ActivityId: d10c4cbb-bde4-4040-b09f-1ace1491dc87. NodeId: YourWFNode. Scope: /YourScope. Client ActivityId : b89c2ff9-8560-458e-9ea2-31ec6c8fde36. —> System.Net.WebException: The remote server returned an error: (400) Bad Request.     at Microsoft.Workflow.Common.AsyncResult.End[TAsyncResult](IAsyncResult result)     at Microsoft.Workflow.Client.HttpGetResponseAsyncResult`1.End(IAsyncResult result)     at Microsoft.Workflow.Client.ClientHelpers.SendRequest[T](HttpWebRequest request, T content)     — End of inner exceptio…

The SharePoint site https://YourSharePointSite and the Workflow Manager endpoint URL https://YourWorkflowManagerServer:12290 were both available without any issue (e.g. no problem with the certificate too), on both nodes (front-end and application servers) of the SharePoint farm, as well as from client computers.

The articles I found about the issue (like this one or this one) explained the problem with the reason, that the SharePoint endpoint URL (in our case ‘https://YourSharePointSite/_layouts/15/metadata/json/1‘) is not accessible, probably because of a name resolution issue. In our case that was definitely not the issue, because if I switched the SharePoint URL from HTTPS to HTTP (via changing the Alternate Access Settings for the site + bindings in IIS manager), I was able to run the registration script successfully:

Register-SPWorkflowService -SPSite http://YourSharePointSite -WorkflowHostUri https://YourWorkflowManagerServer:12290 -ScopeName YourScope –Force -AllowOAuthHttp

After switching back the URL to HTTPS we had the problem again.

My next assumption was, that the service account for the Workflow Manager does not have the root certificate of the SSL certificate under the Trusted Root Certification Authorities.

So I’ve started the Microsoft Management Console (mmc.exe) and added the Certificates snap-in for the service account of the Workflow Manager Backend service:

image

image

image

I found that the list of Trusted Root Certification Authorities contains the root certificate of the SSL, so it could not be a problem either.

As next step, I’ve logged in on the Workflow Manager server (that is the front-end server of the SharePoint farm) the using the Workflow Manager service account to test the connection to the SharePoint site interactively via Internet Explorer. In this case I was faced with the problem, that the SharePoint site https://YourSharePointSite has a certificate warning. As I opened the certificate for the site in Internet Explorer, I saw only the very last entry in the certificate chain (for example, the entry for YourSharePointSite), but none of the certificates above. I’ve found it either, that the account has configured not to use a proxy server. I enabled the proxy connection, then restarted Internet Explorer, and voila no more issues with the certificate. I was able to register the Workflow Manager as well. I don’t exactly know, what was the problem, but I assume, the certificate revocation list was not available without the proxy, and that prohibited the certificate validation necessary for the registration of the Workflow Manager.

March 4, 2017

How to Change the Service Account for the Workflow Manager

Filed under: SP 2013, Workflow — Tags: , — Peter Holpar @ 21:49

A few weeks ago we made a mistake when installing Workflow Manager in a new environment, as we have chosen a wrong account name as the service account for Workflow Manager.

As a first try, we simply changed the identity of the application pool assigned to the Workflow Manager (called WorkflowMgmtPool) in IIS and restarted the pool, but after the change we had an error when accessing the workflow related pages in SharePoint:

Application error when access /_layouts/15/Workflow.aspx, Error=The remote server returned an error: (500) Internal Server Error.   at Microsoft.Workflow.Common.AsyncResult.End[TAsyncResult](IAsyncResult result)     at Microsoft.Workflow.Client.HttpGetResponseAsyncResult`1.End(IAsyncResult result)     at Microsoft.Workflow.Client.ClientHelpers.SendRequest[T](HttpWebRequest request, T content)    9d19d89d-48f7-c052-732f-a59123539aa3
System.Net.WebException: The remote server returned an error: (500) Internal Server Error.    at Microsoft.Workflow.Common.AsyncResult.End[TAsyncResult](IAsyncResult result)     at Microsoft.Workflow.Client.HttpGetResponseAsyncResult`1.End(IAsyncResult result)     at Microsoft.Workflow.Client.ClientHelpers.SendRequest[T](HttpWebRequest request, T content)    9d19d89d-48f7-c052-732f-a59123539aa3

In the Workflow Manager event logs (Event Viewer/Applications and Services Logs/Microsoft-Workflow/Operational) we found this error message:

Error processing management request. Method: GET, RequestUri: https://YourSharePoint:12290/YourScope, Error: System.Security.Cryptography.CryptographicException: Keyset does not exist

   at System.Security.Cryptography.Utils.CreateProvHandle(CspParameters parameters, Boolean randomKeyContainer)
   at System.Security.Cryptography.Utils.GetKeyPairHelper(CspAlgorithmType keyType, CspParameters parameters, Boolean randomKeyContainer, Int32 dwKeySize, SafeProvHandle& safeProvHandle, SafeKeyHandle& safeKeyHandle)
   at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair()
   at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()
   at Microsoft.Workflow.Common.EncryptionHelper.DecryptStringWithCertificate(X509Certificate2 encryptionCertificate, String encryptedText)
   at Microsoft.Workflow.Management.WorkflowEncryptionSettings.InitializeInternal()
   at Microsoft.Workflow.Management.WorkflowServiceConfiguration.get_EncryptionSettings()
   at Microsoft.Workflow.Management.WorkflowServiceConfiguration.GetResourceManagementConnectionStringFromConfig()
   at Microsoft.Workflow.Management.WorkflowServiceConfiguration.get_ConfigProvider()
   at Microsoft.Workflow.Management.WorkflowServiceConfiguration.GetWorkflowServiceConfiguration()
   at Microsoft.Workflow.Gateway.HttpConfigurationInitializer.CreateServiceContext(String nodeId, NamespaceSender namespaceSender)
   at Microsoft.Workflow.Gateway.HttpConfigurationInitializer.EnsureInitialized(String nodeId, NamespaceSender namespaceSender)
   at Microsoft.Workflow.Gateway.HttpConfigurationInitializer.Initialize(HttpConfiguration config, String nodeId, NamespaceSender namespaceSender)
   at Microsoft.Workflow.Gateway.Global.EnsureConfigInitialized(String nodeId)
   at Microsoft.Workflow.Gateway.Global.Application_BeginRequest(Object sender, EventArgs e)

image

It seems the account had no permission to access a certificate or something like this, so we changed back the application pool identity an searched for a better solution.

We found a few useful resources on the web, discussing how the account change should be performed (see here, here and here).

So we run this script from Workflow Manager PowerShell console on our single-node workflow farm:

Stop-SBFarm
Set-SBFarm –RunAsAccount <YourDomain\UserName>
$RunAsPassword = ConvertTo-SecureString -AsPlainText -Force ‘<Password>’
Update-SBHost -RunAsPassword $RunAsPassword
Start-SBFarm

As the result of the script above, the identity of the following Windows services has been changed to the account specified in the script:

  • Service Bus Gateway
  • Service Bus Message Broker
  • Service Bus Resource Provider
  • Service Bus VSS
  • Windows Fabric Host Service

The identity of the Workflow Manager Backend service was not changed, nor the application pool identity of the Workflow Manager in IIS

The script grant the following database roles in the Service Bus databases:

  • Workflow_SB_Container (role granted: ServiceBus.Operators)
  • Workflow_SB_Gateway (roles granted: SBProjectStore.Operators, ServiceBus.Operators)
  • Workflow_SB_Management (role granted: Strore.Operators)

There was however no permission granted on the following workflow-related databases:

  • Workflow_Farm
  • Workflow_Instance
  • Workflow_Resource

As a next step of the identity change (following the suggestion from one of the above referenced forum threads), we changed manually the account of the Workflow Manager Backend service, and restarted it. It caused however further problems, granting permissions for the account on the before mentioned three WF databases (WFServiceOerators role, or db_owner) did not helped either.

The symptoms we faced to were:

  • We were able to start workflow (at least, no error message at this place) from the SharePoint UI, but happened  nothing, we can not stop the workflows from the UI.
  • At the web-endpoint of the Workflow Manager (https://YourSharePoint:12290/YourScope) we had this error message:

<Error xmlns:i="http://www.w3.org/2001/XMLSchema-instance"&gt;
  <Code>UnexpectedError</Code>
  <Message>The data or messaging layer is unavailable. Please retry after 300 seconds.</Message> 
</Error>

In the Event Viewer we had a lot of errors like:

The Workflow Manager cannot contact Service Bus service after retrying for ’28’ minutes. Please verify if the Service Bus service is up and running. The Workflow Manager failed at location ‘ServiceBusNamespaceListener.GetSessionAndStateWithRetryAsyncResult.HandleException’ due to exception: System.UnauthorizedAccessException: 40100: Unauthorized.TrackingId:b006a351-d6bc-4b4e-a178-a4a1d689fee9_GYourSharePoint_GYourSharePoint,TimeStamp:27.02.2017 11:04:31 —> System.ServiceModel.FaultException: 40100: Unauthorized.TrackingId:b006a351-d6bc-4b4e-a178-a4a1d689fee9_GYourSharePoint_GYourSharePoint,TimeStamp:27.02.2017 11:04:31

image

and warnings like:

Service Bus exception swallowed at location ServiceBusNamespaceListener.GetSessionAndStateWithRetryAsyncResult.HandleException. System.UnauthorizedAccessException: 40100: Unauthorized.TrackingId:c0f820e5-bc7f-4186-8d8f-41899f014c84_GYourSharePoint_GYourSharePoint,TimeStamp:27.02.2017 11:05:19 —> System.ServiceModel.FaultException: 40100: Unauthorized.TrackingId:c0f820e5-bc7f-4186-8d8f-41899f014c84_GYourSharePoint_GYourSharePoint,TimeStamp:27.02.2017 11:05:19

image

The few discussions related to similar problems we found on the web (like this one or this one) did not help to much, so we decided to set back the original  account of the Workflow Manager Backend service, and restarted it again. Our workflows are functioning now, but I am really keen to know, how we could change the identity of the Workflow Manager Backend service as well.

Using PowerShell and REST with Project Server (or SharePoint) for Reporting

Filed under: OData, Project Server, REST — Tags: , , — Peter Holpar @ 21:43

If you are working with Project Server or SharePoint Server, you should not ignore the potential provided by PowerShell and the REST (OData) interface to create simple reports. You should although at the same time be aware of  a few pitfalls of this combination as well.

Let’s see the next code example first. Its goal is to output the list of projects to the screen, including their Id, Name and ProjectSiteUrl properties:

$url = "http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,ProjectSiteUrl&quot;

$request = [System.Net.WebRequest]::Create($url)
$request.UseDefaultCredentials = $true
$request.Accept = "application/json;odata=verbose"

$response = $request.GetResponse()
$reader = New-Object System.IO.StreamReader $response.GetResponseStream()
$data = $reader.ReadToEnd()

$result = ConvertFrom-Json -InputObject $data
$result.d.results | select Id, Name, ProjectSiteUrl

If you test the URL http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,ProjectSiteUrl from the browser, you see, that all of these three properties are returned. However, if you run the above script from console, you find, that the ProjectSiteUrl column is empty for all of the projects.

If you use the ProjectData OData endpoint instead of the ProjectServer endpoint, and select the corresponding properties, all of the properties will be omitted by the script:

$url = "http://YourProjectServer/PWA/_api/Projects?$select=ProjectId,ProjectName,ProjectWorkspaceInternalUrl&quot;

$request = [System.Net.WebRequest]::Create($url)
$request.UseDefaultCredentials = $true
$request.Accept = "application/json;odata=verbose"

$response = $request.GetResponse()
$reader = New-Object System.IO.StreamReader $response.GetResponseStream()
$data = $reader.ReadToEnd()

$result = ConvertFrom-Json -InputObject $data
$result.d.results | select ProjectId, ProjectName, ProjectWorkspaceInternalUrl

Note: If you have a localized version of Project Server, you can either use an OData query including the localized entity and property names, like:

http://YourProjectServer/PWA/_api/ProjectData/Projekte?$select=ProjektID,ProjektName,ProjektArbeitsbereichInterneURL

or switch back to the English version by injecting [en-US] segment after the ProjectData endpoint:

http://YourProjectServer/PWA/_api/ProjectData/[en-US]/Projects?$select=ProjectId,ProjectName,ProjectWorkspaceInternalUrl

Of course, in the first case you should change the property names used in the select statement in the PowerShell script to match the names used in the REST query.

Let’s see another example. In the next case, our goal is to create a .csv file, that one can easily import to Excel, including the name and the RBS (resource breakdown structure) of the resources.

  1. $baseUrl = "http://YourProjectServer/PWA/_api/ProjectServer&quot;
  2. $rbsUrl = $baseUrl + "/LookupTables?$filter=Name eq 'RBS'&$expand=Entries&$select=Entries/InternalName,Entries/Value"
  3. $resourceUrl = $baseUrl + "/EnterpriseResources?$select=Name,Custom_000039b78bbe4ceb82c4fa8c0c400284"
  4.  
  5. #rbs
  6. $rbsRequest = [System.Net.WebRequest]::Create($rbsUrl)
  7. $rbsRequest.UseDefaultCredentials = $true
  8. $rbsRequest.Accept = "application/json;odata=verbose"
  9.  
  10. $rbsResponse = $rbsRequest.GetResponse()
  11. $rbsReader = New-Object System.IO.StreamReader $rbsResponse.GetResponseStream()
  12. $rbsData = $rbsReader.ReadToEnd()
  13.  
  14. $rbsResult = ConvertFrom-Json -InputObject $rbsData
  15. $rsbEntries = $rbsResult.d.results.Entries.results
  16.  
  17. #resources
  18. $resRequest = [System.Net.WebRequest]::Create($resourceUrl)
  19. $resRequest.UseDefaultCredentials = $true
  20. $resRequest.Accept = "application/json;odata=verbose"
  21.  
  22. $resResponse = $resRequest.GetResponse()
  23. $resReader = New-Object System.IO.StreamReader $resResponse.GetResponseStream()
  24. $resData = $resReader.ReadToEnd()
  25.  
  26. $resResult = ConvertFrom-Json -InputObject $resData
  27.  
  28. $resResult.d.results | % {
  29. select -Input $_ -Prop `
  30.     @{ Name='Name'; Expression={$_.Name} },
  31.     @{ Name='RBS'; Expression={$rbs = $_.Custom_x005f_000039b78bbe4ceb82c4fa8c0c400284; If ($rbs.results -is [System.Object[]]) {$rsbEntries | ? { $_.InternalName -eq $rbs.results[0] } | % { $_.Value } } Else {''} } }
  32.     } | Export-Csv -Path ResourceRBS.csv -Delimiter ";" -Encoding UTF8 -NoTypeInformation

Note: The –NoTypeInformation switch of Export-Csv ensures that no type information would be emitted as header into the .csv file. The -Delimiter ";" and the -Encoding UTF8 settings help to produce a .csv file in a format and encoding that can be opened in Excel simply by clicking on the file.

The symptoms are similar as in the first case, only the resource name is included in the file, but the RBS value not.

I’ve included this last code sample in a code block not just because it is a bit longer as the former ones, but because I help that the highlighting helps you to understand the base problem with our scripts, even if you did not catch it at the first example. Have you recognized, that the query options ($filter, $select and $expand) have a different color, as the rest of the query text? Actually, they have the very same color as the variable names (like $baseUrl or $resRequest) in the code. It is because they are handled really as variable names. Since we used double quotes in the code to define the string literals for URLs, and it means PowerShell should parse the string and replace possible variable names with the values of the variable. As we didn’t define variables like $filter, $select or $expand, they are simply removed from the string (replaced by an empty string). See this short explanation for details.

Instead of double quotation marks we should use single quotation marks to leave the query options intact, but in this case we should escape the single quotes (using two single quotation marks) used in the REST query itself.

For example, instead of:

$url = "http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,ProjectSiteUrl&quot;

we should simply use:

$url = ‘http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,ProjectSiteUrl&#8217;

and instead of::

$rbsUrl = $baseUrl + "/LookupTables?$filter=Name eq ‘RBS’&$expand=Entries&$select=Entries/InternalName,Entries/Value"

we should use:

$rbsUrl = $baseUrl + ‘/LookupTables?$filter=Name eq ”RBS”&$expand=Entries&$select=Entries/InternalName,Entries/Value’

Note, that the value RBS is enclosed by two single quotation marks on both sides, and not by a double quotation mark!

Alternatively, you can use the double quotation marks to define the strings for the REST queries (for example, if you still would like PowerShell to parse it from some reason), but in this case, you should escape the dollar sign in the query options to disable parsing them out from the string.

For example, instead of:

$url = "http://YourProjectServer/PWA/_api/ProjectServer/Projects?$select=Id,Name,ProjectSiteUrl&quot;

we should simply use:

$url = "http://YourProjectServer/PWA/_api/ProjectServer/Projects?`$select=Id,Name,ProjectSiteUrl"

and instead of::

$rbsUrl = $baseUrl + "/LookupTables?$filter=Name eq ‘RBS’&$expand=Entries&$select=Entries/InternalName,Entries/Value"

we should use:

$rbsUrl = $baseUrl + "/LookupTables?`$filter=Name eq ‘RBS’&`$expand=Entries&`$select=Entries/InternalName,Entries/Value"

See this description for more details about PowerShell string parsing and escaping methods.

If you compare our first two examples (the one with the ProjectServer and the other one with the ProjectData endpoint) the results are different, because in the first case the ProjectSiteUrl property is not part of the standard set of properties returned by default for projects via the ProjectServer endpoint, but ProjectData returns all properties, the ProjectWorkspaceInternalUrl property too, even if it is not specified in a $select query option.

In the third case, our query should have returned the entries of the RBS lookup table, but since the query options got lost, it simply return an overview about all lookup tables.

March 3, 2017

SharePoint Designer Workflow Gets Suspended after Task Completion – How to Get Field Value from a Workflow Task via Lookup

Filed under: SP 2013, SPD, Workflow — Tags: , , — Peter Holpar @ 06:21

Nowadays we are working quite a lot with SharePoint Designer 2013 based workflows. On workflows I mean the “new”, Workflow Manager based ones.

Recently we wanted to access a workflow task field beyond the standard outcome to use its value in another part of the workflow. For example, we need the value of the Description field, as the explanation of the decision made on the form (rejection vs. approval).

image

To achieve that, we stored the workflow task Id in a variable called TaskID (see above), and planned to use it as a lookup value from the task list (see below). Note, that we used the ID field in the lookup list, Data Source is Assocciation: Task List, that is the standard Worklow Tasks list in our case.

image

The value of the TaskID variable is returned as integer:

image

After publishing the workflow and creating an item to test it, the workflow task was created. We entered some text in the Description field, and approved the task. We found, that the workflow gets stuck in the Suspended status. Resuming it has not helped either.

image

The error description we had:

RequestorId: 3c361109-ce76-de39-0000-000000000000. Details: An unhandled exception occurred during the execution of the workflow instance. Exception details: System.FormatException: Input string was not in a correct format. at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at Microsoft.Activities.Expressions.ParseNumber`1.Execute(CodeActivityContext context) at System.Activities.CodeActivity`1.InternalExecute(ActivityInstance instance, ActivityExecutor executor, BookmarkManager bookmarkManager) at System.Activities.Runtime.ActivityExecutor.ExecuteActivityWorkItem.ExecuteBody(ActivityExecutor executor, BookmarkManager bookmarkManager, Location resultLocation)

The resources we found on the web here, here and there did not help to much, but the error message itself did.

The reason of the error was, that the TaskID (a variable of type String) we have from the Assign a task action is actually the Guid of the task item, but we wanted to use it to look up the task based on its ID field (an Integer). Of curse, the workflow engine was not able to convert the Guid to an integer value.

The correct lookup is illustrated below. We use the GUID field for as the lookup field, and TaskID is returned as a string:

image

image

With this “minor” modification the workflow runs as expected.

After we solved the problem I found that the the original requirement (getting field value from a specific workflow task as data source via lookup) was already discussed and solved earlier, see this thread and this one.

Older Posts »

Create a free website or blog at WordPress.com.