Second Life of a Hungarian SharePoint Geek

May 12, 2017

"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 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.

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.

‘The URL "[url]" is invalid. It may refer to a nonexistent file or folder, or refer to a valid file or folder that is not in the current Web.’ Error When Changing the URL of a Web Site

Filed under: SP 2013 — Tags: — Peter Holpar @ 06:17

Recently one of our SharePoint administrators wanted to change the address of a site via Site Settings / Title, description, and logo:

image

He got an error with a correlation ID. Based on this ID we found this entry in the ULS logs:

<nativehr>0x80004005</nativehr><nativestack></nativestack>The URL "/Sites/SiteX" is invalid. It may refer to a nonexistent file or folder, or refer to a valid file or folder that is not in the current Web.

We had the same error message in the PowerShell console, when we tried to change the URL of the site from PowerShell, as described in this post:

$web = Get-SPWeb http://YourSharePointServer/Sites/SiteX
$web.ServerRelativeUrl = ‘/SiteX_New’
$web.Update()

The same symptoms, if we try to do it as described here:

Get-SPWeb http://YourSharePointServer/Sites/SiteX | Set-SPWeb -RelativeUrl SiteX_New

This message was of course wrong and misleading, as we could access the web both from the UI and from script. As it turned out, an other error preceded the one above in the logs:

System.Data.SqlClient.SqlException (0x80131904): String or binary data would be truncated.     at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)     at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)     at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)     at System.Data.SqlClient.SqlDataReader.TryHasMoreRows(Boolean& moreRows)     at System.Data.SqlClient.SqlDataReader.TryHasMoreResults(Boolean& moreResults)     at System.Data.SqlClient.SqlDataReader.TryNextResult(Bool…
…ean& more)     at System.Data.SqlClient.SqlDataReader.NextResult()     at Microsoft.SharePoint.SPSqlClient.ExecuteQueryInternal(Boolean retryfordeadlock)     at Microsoft.SharePoint.SPSqlClient.ExecuteQuery(Boolean retryfordeadlock)  ClientConnectionId:71163353-b397-4ada-99fd-be1e09547586  Error Number:8152,State:13,Class:16
ExecuteQuery failed with original error 0x80131904

The real problem was a few file URLs in one of the document libraries. The length of these URLs was already originally near the limit, and after changing the site URL with a longer path name would be these new URLs beyond the limitation.

On the content database level, the properties of the documents are stored in the AllDocs table. The DirName field (nvarchar(256)) contains the full directory path, including the site structure (for example ‘Sites/SiteX/Documents/FolderA/FolderC‘). The LeafName field (nvarchar(128)) contains the file name (for example ‘DocumentZ.docx‘). It means, if a site URL is being changed, only the value of the DirName field would be changed, only in this field can be the new value truncated, if its length is beyond the 128 character limit.

You can query the files having the longest DirName from the content database via the SQL query:

SELECT TOP 100
  [DirName], LEN([DirName]) AS DirNameLength
  FROM [Your_Content_DB].[dbo].[AllDocs]
  WHERE DirName LIKE ‘Sites/SiteX/%’
  ORDER BY DirNameLength DESC

If you happen to need the overall path (including both DirName and LeafName), you can query it as well:

SELECT TOP 100
  [DirName] + ‘/’ + [LeafName] AS Path, LEN([DirName] + ‘/’ + [LeafName]) AS PathLength
  FROM [Your_Content_DB].[dbo].[AllDocs]
  WHERE DirName LIKE ‘Sites/SiteX/%’
  ORDER BY PathLength DESC

November 30, 2016

Using Edge.js as a Replacement for win32ole

Filed under: ActiveX, NodeJS, SP 2013 — Tags: , , — Peter Holpar @ 21:13

Last month I had to create a NodeJS script that invokes methods of ActiveX object. I work in a Windows-only environment, so it should be no problem. I find the win32ole package quickly. Based on its description and the samples I’ve found, it seemed to be the perfect tool for my requirements. However, as many others (see issues on GitHub, and a lot of  threads about the build problem on StackOverflow), I had issues installing the package in my environment:

OS: Windows Server 2008 R2 SP1, Windows Server 2012 R2
npm: 2.15.9
node: 4.5.0

The last two lines are from the output of the npm version command.

As far as I see, a package win32ole depends on (node-gyp) fails to build:

npm ERR! win32ole@0.1.3 install: ‘node-gyp rebuild’
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the win32ole@0.1.3 install script ‘node-gyp rebuild’.

As agape824 commented on Aug 28 2015 regarding a similar issue:

“I solved the problem
by installing node.js v0.8.18 & npm v1.4.28.
Previous erros were produced by different version of node files (eg. v8.h).”

So I’ve removed all NodeJS and npm installation on one of our systems, and installed the suggested versions. Downloaded the node-v0.8.18-x64.msi, and the right npm version was installed via the command:

npm install npm@1.4.28 -g

Now invoking the command npm version results:

node: 0.8.18
npm: 1.4.28

and we can install win32ole using:

npm install –save-dev win32ole

Having win32ole installed, we can create NodeJS scripts that interact with ActiveX object.

For example, you can get the content of a web page via the MSXML2.XMLHTTP object:

var win32ole = require(‘win32ole’);

var url = "http://www.yoursite.com";
var xhr = new ActiveXObject("MSXML2.XMLHTTP");
xhr.open("GET", url, false);
xhr.send();
console.log(xhr.responseText);

Of course, you can do it much easier and a platform-independent way using other NodeJS libraries, it is just to illustrate, how to invoke ActiveX object methods. However, you can perform other, not such trivial actions using the win32ole library as well, like interacting with the Windows application that support automation via ActiveX object (like Excel, Word, or Outlook, see the examples here), or even access the HTML DOM loaded into your Internet Explorer browser, and extract values from it.

In my case I needed Internet Explorer to perform Windows-integrated authentication against a SharePoint server. In this case, SharePoint returns an authentication ticket in the response page (in the hidden input field ‘__REQUESTDIGEST’), that one can include in subsequent requests.

var win32ole = require(‘win32ole’);
var uri = "
http://YourSharePointServer&quot;;

try{
var ie = new ActiveXObject(‘InternetExplorer.Application’);
  // displaying the UI of IE might be useful when debugging
  //ie.Visible = true;
  console.log(uri);
  ie.Navigate(uri);
  while(ie.ReadyState != 4) {
    win32ole.sleep(1000);
  }
  var token = ie.Document.getElementById("__REQUESTDIGEST").value;
  console.log(token);
  ie.Quit();
}catch(e){
  console.log(‘*** exception cached ***\n’ + e);

So the win32ole package would be really great, but it has not been updated in the past 4 years or so, and we don’t work with obsolete node and npm versions just to be able to use this package. Instead of that, we tried to find a replacement solution for win32ole. And I think, we’ve found something that is even better than win32ole, and it is the Edge.js package. Edge.js enables interaction between your NodeJS and .NET code in both direction, and not only on the Windows platform, as it supports Mono and CoreCLR as well. It supports PowerShell, and other languages beyond C#, like F#, Lisp or Python just to name the most important ones.

To tell the truth, creating ActiveX object and invoking their methods only a very small subset of functionality enabled by this package. Obviously, you can not create ActiveX objects on operating systems that do not support them, but it is not the limitation of the package.

After you install the Edge.js package, for example:

npm install –save-dev edge

you can create NodeJS scripts that invokes your C# code. In the C# code you can create ActiveX objects and invoke their members as well. In the following simple example create an instance of the WScript.Shell ActiveX object, and displays a greeting message via its Popup method:

var edge = require(‘edge’);

var wshShell = edge.func(function () {/*
  async (input) => { 
       dynamic wshShell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell"));
       wshShell.Popup("Hello, " + input + "!");

        return string.Empty;
    }
*/});

wshShell("world", function (error, result) {
    if (error) throw error;
    console.log(result);
});

Or we can re-create our win32ole example showed above using Edge.js, and read the authentication token via the HTML DOM in Internet Explorer:

var edge = require(‘edge’);

var uri = "http://YourSharePointServer&quot;;

var getToken = edge.func(function () {/*
    async (uri) => { 

            dynamic ie = Activator.CreateInstance(Type.GetTypeFromProgID("InternetExplorer.Application"));
            // if you want to see the UI (for example, when debugging)
            //ie.Visible = true;
            ie.Navigate(uri);
            while (ie.ReadyState != 4)
            {
                System.Threading.Thread.Sleep(1000);
            }
            var token = ie.Document.getElementById("__REQUESTDIGEST").value;
            ie.Quit();

        return token.ToString();
    }
*/});

getToken(uri, function (error, result) {
    if (error) throw error;
    console.log(result);
});

An alternative solution to the above is to read a SharePoint web page via the MSXML2.XMLHTTP object and parse the HTML DOM via cheerio to get the hidden field that contains the request digest.

var edge = require(‘edge’);
var cheerio = require(‘cheerio’);

var uri = "http://YourSharePointServer&quot;;

var getToken = edge.func(function () {/*
    async (uri) => {

            dynamic xhr = Activator.CreateInstance(Type.GetTypeFromProgID("MSXML2.XMLHTTP"));
            xhr.open("GET", uri, false);
            xhr.send();

            return xhr.responseText;
    }
*/});

getToken(uri, function (error, result) {
    if (error) throw error;
    $ = cheerio.load(result);
        console.log($(‘#__REQUESTDIGEST’).val());  
});

I hope these scripts help other developers frustrated by the build issues of win32ole to create workarounds. I think Edge.js is a really useful NodeJS package, I am sure I will find a lot of application areas for it in the future. In contrast to win32ole, Edge.js is a living project, and that is very important to us. Many thanks to Thomas Janczuk for creating and supporting this gem! Keep up the excellent job!

August 29, 2016

Permission-based Rendering Templates, Part 2: The Synchronous Solution

Filed under: CSR, JavaScript, jQuery, REST, SP 2013 — Tags: , , , , — Peter Holpar @ 22:14

In my recent post I’ve illustrated how can you implement a permission-based custom rendering template using the JavaScript client object model (JSCOM)  and jQuery. That rendering template was implemented using the standard asynchronous JavaScript patterns via a callback method to not block the UI thread of the browser. In a fast network (in a LAN, for example) however, a synchronous implementation can function as well. Although there are some unsupported methods to make a JSCOM request synchronously, the JavaScript client object model was designed for asynchronous usage (see its executeQueryAsync method). To send our requests synchronously, we utilize the REST / OData interface in this post, and send the requests via the ajax function of jQuery.

To understand the original requirements and the configuration (field and list names, etc.), I suggest to read the first part first.

To enable using of jQuery selectors containing the dollar sign ($), we use the same escapeForJQuery helper function that we’ve created for the first part.

  1. var restrictedValues1 = ['Approved', 'Rejected'];
  2. var restrictedValues2 = ['Resubmit'];
  3.  
  4. var custom = custom || {};
  5.  
  6. custom.controlId = null;
  7.  
  8. var adminGroup = "MyGroup";
  9.  
  10. custom.escapeForJQuery = function (value) {
  11.     var newValue = value.replace(/\$/g, "\\$");
  12.     return newValue;
  13. }

Instead of simply wrapping the standard display template of choice fields (SPFieldChoice_Edit), the editFieldMethod function is responsible to get the HTML content of the field control, as it would be rendered without the customization by invoking the SPFieldChoice_Edit function, then we determine the group membership of the user by calling the synchronous isCurrentUserMemberOfGroup function (more about that a bit later), finally we alter the HTML content by hiding the adequate options by calling the hideOptions function (see it later as well).

  1. custom.editFieldMethod = function (ctx) {
  2.     var fieldSchema = ctx.CurrentFieldSchema;
  3.     custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  4.     var html = SPFieldChoice_Edit(ctx);
  5.  
  6.     var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  7.     if (isCurrentUserInGroup) {
  8.         html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  9.     }
  10.     else {
  11.         html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  12.     }
  13.  
  14.     return html;
  15. }

The hideOptions function loads the HTML source of the control into the DOM and removes the options that should be hidden for the given group. Finally it returns the HTML source of the altered control:

  1. custom.hideOptions = function (html, ctrlId, restrictedValues) {
  2.     var parsedHtml = $(html);
  3.     restrictedValues.forEach(function (rv) {
  4.         var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  5.         $(parsedHtml).find(selector).remove();
  6.     });
  7.     var result = $(parsedHtml).html();
  8.  
  9.     return result;
  10. }

The isCurrentUserMemberOfGroup function sends a synchronous REST request via the the ajax function of jQuery to determine the group membership of the current user:

  1. var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  2.  
  3. custom.isCurrentUserMemberOfGroup = function (groupName) {
  4.     var isMember = false;
  5.  
  6.     $.ajax({
  7.         url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  8.         type: "GET",
  9.         async: false,
  10.         contentType: "application/json;odata=verbose",
  11.         headers: {
  12.             "Accept": "application/json;odata=verbose",
  13.             "X-RequestDigest": $("#__REQUESTDIGEST").val()
  14.         },
  15.         complete: function (result) {
  16.             var response = JSON.parse(result.responseText);
  17.             if (response.error) {
  18.                 console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  19.             }
  20.             else {
  21.                 var groups = response.d.results;
  22.                 groups.forEach(function (group) {
  23.                     var loginName = group.LoginName;
  24.                     console.log(String.format("Group name: {0}", loginName));
  25.                     if (groupName == loginName) {
  26.                         isMember = true;
  27.                     }
  28.                 });
  29.             }
  30.         }
  31.     });
  32.  
  33.     return isMember;
  34. }

In this case we simply register the editFieldMethod for both the ‘EditForm’ and for the ‘NewForm’ mode of the Status field, there is no need for the OnPostRender method:

  1. var customOverrides = {};
  2. customOverrides.Templates = {};
  3.  
  4. customOverrides.Templates.Fields = {
  5.     'Status': {
  6.         'EditForm': custom.editFieldMethod,
  7.         'NewForm': custom.editFieldMethod
  8.     }
  9. };
  10.  
  11. SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);

The full source code of the rendering template introduced in this post:

  1. 'use strict';
  2.  
  3. (function () {
  4.  
  5.     var restrictedValues1 = ['Approved', 'Rejected'];
  6.     var restrictedValues2 = ['Resubmit'];
  7.  
  8.     var custom = custom || {};
  9.  
  10.     custom.controlId = null;
  11.  
  12.     var adminGroup = "MyGroup";
  13.  
  14.     custom.escapeForJQuery = function (value) {
  15.         var newValue = value.replace(/\$/g, "\\$");
  16.         return newValue;
  17.     }
  18.  
  19.     custom.hideOptions = function (html, ctrlId, restrictedValues) {
  20.         var parsedHtml = $(html);
  21.         restrictedValues.forEach(function (rv) {
  22.             var selector = "#" + custom.escapeForJQuery(ctrlId) + " option[value='" + custom.escapeForJQuery(rv) + "']";
  23.             $(parsedHtml).find(selector).remove();
  24.         });
  25.         var result = $(parsedHtml).html();
  26.  
  27.         return result;
  28.     }
  29.  
  30.     custom.editFieldMethod = function (ctx) {
  31.         var fieldSchema = ctx.CurrentFieldSchema;
  32.         custom.controlId = fieldSchema.Name + '_' + fieldSchema.Id + '_$DropDownChoice';
  33.         var html = SPFieldChoice_Edit(ctx);
  34.  
  35.         var isCurrentUserInGroup = custom.isCurrentUserMemberOfGroup(adminGroup);
  36.         if (isCurrentUserInGroup) {
  37.             html = custom.hideOptions(html, custom.controlId, restrictedValues1);
  38.         }
  39.         else {
  40.             html = custom.hideOptions(html, custom.controlId, restrictedValues2);
  41.         }
  42.  
  43.         return html;
  44.     }
  45.  
  46.     var serverUrl = String.format("{0}//{1}", window.location.protocol, window.location.host);
  47.  
  48.     custom.isCurrentUserMemberOfGroup = function (groupName) {
  49.         var isMember = false;
  50.  
  51.         $.ajax({
  52.             url: serverUrl + "/_api/Web/CurrentUser/Groups?$select=LoginName",
  53.             type: "GET",
  54.             async: false,
  55.             contentType: "application/json;odata=verbose",
  56.             headers: {
  57.                 "Accept": "application/json;odata=verbose",
  58.                 "X-RequestDigest": $("#__REQUESTDIGEST").val()
  59.             },
  60.             complete: function (result) {
  61.                 var response = JSON.parse(result.responseText);
  62.                 if (response.error) {
  63.                     console.log(String.format("Error: {0}\n{1}", response.error.code, response.error.message.value));
  64.                 }
  65.                 else {
  66.                     var groups = response.d.results;
  67.                     groups.forEach(function (group) {
  68.                         var loginName = group.LoginName;
  69.                         console.log(String.format("Group name: {0}", loginName));
  70.                         if (groupName == loginName) {
  71.                             isMember = true;
  72.                         }
  73.                     });
  74.                 }
  75.             }
  76.         });
  77.  
  78.         return isMember;
  79.     }
  80.  
  81.     var customOverrides = {};
  82.     customOverrides.Templates = {};
  83.  
  84.     customOverrides.Templates.Fields = {
  85.         'Status': {
  86.             'EditForm': custom.editFieldMethod,
  87.             'NewForm': custom.editFieldMethod
  88.         }
  89.     };
  90.  
  91.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(customOverrides);
  92.  
  93. })();

Assuming your custom list is called PermBasedField, and both jQuery (in my case it is jquery-1.9.1.min.js) and our custom JavaScript rendering template (in my case it’s called permissionBasedFieldTemplate2.js) are stored in the root of the Site Assets library of the root web, you can register the template using the following PowerShell script:

$web = Get-SPWeb http://YourSharePointSite
$list = $web.Lists["PermBasedField"]

$field = $list.Fields.GetFieldByInternalName("Status")
$field.JSLink = "~sitecollection/SiteAssets/jquery-1.9.1.min.js|~sitecollection/SiteAssets/permissionBasedFieldTemplate2.js"
$field.Update()

Note, that (in contrast to the script introduced in the first part of this post) there is no need for the JSCOM JavaScript files (sp.runtime.js and sp.js) in this case.

Older Posts »

Create a free website or blog at WordPress.com.