Second Life of a Hungarian SharePoint Geek

April 23, 2014

Who Has Deployed This Solution?

Filed under: PowerShell, Reflection, SP 2010, Tips & Tricks — Tags: , , , — Peter Holpar @ 22:09

Recently we had a problem in one of our server farms that was caused by a sandboxed solution that was deployed by mistake as farm solution as well. I wanted to know who has deployed the farm solution, but in the Central Administration we can see only the time of the deployment, but there is no information regarding the person who performed the action:

image

If we submit the following SQL query in the configuration database of the farm (using the name of the solution as the filter)…

  1. SELECT [Id]
  2.       ,[ClassId]
  3.       ,[ParentId]
  4.       ,[Name]
  5.       ,[Status]
  6.       ,[Version]
  7.       ,[Properties]
  8.   FROM [SharePoint_Config].[dbo].[Objects]
  9.   WHERE [Name] = 'addispname.wsp'

… we should receive two records as result that include XML objects in the Properties fields like these ones:

  1. <object type="Microsoft.SharePoint.Administration.SPPersistedFile, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
  2.     <sFld type="Int64" name="m_FileSize">4748</sFld>
  3.     <fld type="System.Collections.Hashtable, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="m_UpgradedPersistedFields" />
  4.     <fld name="m_Properties" type="null" />
  5.     <sFld type="String" name="m_LastUpdatedUser">CONTOSO\Administrator</sFld>
  6.     <sFld type="String" name="m_LastUpdatedProcess">vssphost4 (5876)</sFld>
  7.     <sFld type="String" name="m_LastUpdatedMachine">DEMO2010A</sFld>
  8.     <sFld type="DateTime" name="m_LastUpdatedTime">2013-08-22T00:01:04</sFld>
  9. </object>
  10. <object type="Microsoft.SharePoint.Administration.SPSolution, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
  11.     <sFld type="Guid" name="m_Id">31079555-a0db-4dce-8ca4-51e3e40cb6d1</sFld>
  12.     <sFld type="Boolean" name="m_WebPartPackage">False</sFld>
  13.     <sFld type="Guid" name="m_Wppid">00000000-0000-0000-0000-000000000000</sFld>
  14.     <fld type="Microsoft.SharePoint.Administration.SPServerRole, Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" name="m_DeploymentServerType">WebFrontEnd</fld>
  15.     <sFld type="Boolean" name="m_HasWebAppResource">False</sFld>
  16.     <fld type="System.Collections.Hashtable, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" name="m_UpgradedPersistedFields" />
  17.     <fld name="m_Properties" type="null" />
  18.     <sFld type="String" name="m_LastUpdatedUser">CONTOSO\Administrator</sFld>
  19.     <sFld type="String" name="m_LastUpdatedProcess">vssphost4 (5876)</sFld>
  20.     <sFld type="String" name="m_LastUpdatedMachine">DEMO2010A</sFld>
  21.     <sFld type="DateTime" name="m_LastUpdatedTime">2013-08-22T00:01:04</sFld>
  22. </object>

As you can see, one of the objects (SPSolution) describes the solution itself, the other one (SPPeristedFile) describes the persisted file that belongs to the solution. From the properties of the persisted file we can read the information we need, see m_LastUpdatedUser.

If you don’t like the idea to query the SharePoint content database directly, there is an other way to achieve the same information, for example using PowerShell and a bit of Reflection.

First we get a reference to our solution either by ID (as long it is known):

$solution = Get-SPSolution -Identity ’31079555-a0db-4dce-8ca4-51e3e40cb6d1′

or by Name:

$solution = Get-SPSolution | ? { $_.Name -eq ‘addispname.wsp’ }

We can get the persisted file of the solution via the SolutionFile property, then using Reflection we can read its internal LastUpdateInfo property that contains the information regarding the user that deployed the solution:

$persistedFile = $solution.SolutionFile
$persistedFileType = $persistedFile.GetType()
$bindingFlags = [System.Reflection.BindingFlags]::NonPublic -bor [System.Reflection.BindingFlags]::Instance
$pi_LastUpdateInfo = $persistedFileType.GetProperty(‘LastUpdateInfo’, [System.Reflection.BindingFlags]($bindingFlags))
$lastUpdateInfo = $pi_LastUpdateInfo.GetValue($persistedFile, $null)
Write-Host $lastUpdateInfo

We should have an output similar to this one:

User: CONTOSO\Administrator
Process:vssphost4 (5876)
Machine:DEMO2010A
Time:August 22, 2013 12:01:04.0000

Beyond the user, one can find other potentially interesting information as well, including the host the solution was deployed from, and the process used to deploy the solution (executable + process ID a.k.a. PID). In the sample above the solution was deployed from Visual Studio, but in other cases the value can be for example powershell (4736) that means it was deployed using PowerShell.

April 14, 2014

How to Extend the SharePoint Ribbon to Enable Pushing Down Permissions to Child Items

Filed under: JavaScript, Permissions, Ribbon, Security, SP 2010 — Tags: , , , , — Peter Holpar @ 20:03

If you are familiar with the security options of the NTFS file system you should know the dialog below as well (accessible via the Security tab of the folder properties / Advanced button / Change Permissions… button):

image

By selecting the highlighted checkbox you can push down the permissions set on the current folder to all child items (folders and files). Unfortunately, there is no such option in the standard SharePoint toolbox. As you can see on the screenshot below, the Inheritance group of the Permission Tools on the ribbon includes the options Manage Parent and Stop Inheriting Permissions, but no way to force the inheritance of the current permissions to the child items.

image

In my recent posts I discussed how to check from code whether an site / list has child items with unique permissions, and how to to push down permissions from code.

In this post I will illustrate, how to create a Ribbon extension that enables the missing functionality from SharePoint.

In our CustomAction elements we register the loader ScriptBlock of the custom PageComponent (implemented in InheritPermissions.js, it will be loaded only we are on the permissions page, users.aspx). Next, we register our button (Ribbon.Permission.Manage.InheritDown) in the Inheritance button group (Ribbon.Permission.Parent, defined in 14\TEMPLATE\GLOBAL\XML\CMDUI.XML).

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction Location="ScriptLink" ScriptBlock="function startScript() { document.write('&lt;script type=&quot;text/javascript&quot; src=&quot;/_layouts/InheritPermissions/InheritPermissions.js&quot;&gt;&lt;/' + 'script&gt;'); } if (document.location.pathname.toLowerCase().indexOf('_layouts/user.aspx') > -1 ) { ExecuteOrDelayUntilScriptLoaded(startScript, 'sp.js'); }" Sequence="1000"/>
  4.   
  5.   <CustomAction Location="CommandUI.Ribbon">
  6.     <CommandUIExtension>
  7.       <CommandUIDefinitions>
  8.         <CommandUIDefinition Location="Ribbon.Permission.Parent.controls._children">
  9.           <Button
  10.               Id="Ribbon.Permission.Manage.InheritDown"
  11.               Command="Ribbon.Permission.Manage.InheritDown"
  12.               Sequence="50"
  13.               Image16by16="/_layouts/images/InheritPermissions/InheritDown16x16.png"
  14.               Image32by32="/_layouts/images/InheritPermissions/InheritDown32x32.png"
  15.               LabelText="Inherit to Child Objects"
  16.               ToolTipTitle="Inherit to Child Objects"
  17.               ToolTipDescription="Reset permissions on child objects to inherit the permissions of this object.&lt;br&gt;Any custom permissions configured on child objects will be lost.&lt;br&gt;This tool is available only to farm administrators."
  18.               TemplateAlias="o1"/>
  19.         </CommandUIDefinition>
  20.       </CommandUIDefinitions>
  21.     </CommandUIExtension>
  22.   </CustomAction>
  23.  
  24. </Elements>

The event handler functionality custom PageComponent is implemented in InheritPermissions.js. If the command ‘Ribbon.Permission.Manage.InheritDown’ is invoked, we make a page postback with the event target ‘inheritDown’:

__doPostBack(‘inheritDown’, ”);

we process the postback later in our WebControl. Similarly, whether the page component should handle the command ‘Ribbon.Permission.Manage.InheritDown’ is determined by the value of the permInheritEnabled variable, the value is injected by our WebControl as well (see the RegisterStartupScript in the OnInit method of the web control later).

  1. Type.registerNamespace('CustomPermission.UI');
  2.  
  3. CustomPermission.UI.PageComponent = function CustomPermissionUI_PageComponent() {
  4.     CustomPermission.UI.PageComponent.initializeBase(this);
  5. }
  6.  
  7. CustomPermission.UI.PageComponent.initialize = function () {
  8.     var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
  9.     if (null !== ribbonPageManager) {
  10.         ribbonPageManager.addPageComponent(this.instance);
  11.         ribbonPageManager.get_focusManager().requestFocusForComponent(this.instance);
  12.     }
  13. }
  14. CustomPermission.UI.PageComponent.refreshRibbonStatus = function () {
  15.     SP.Ribbon.PageManager.get_instance().get_commandDispatcher().executeCommand(Commands.CommandIds.ApplicationStateChanged, null);
  16. }
  17.  
  18. CustomPermission.UI.PageComponent.prototype = {
  19.     init: function () { },
  20.     getFocusedCommands: function () {
  21.         return ['Ribbon.Permission.Manage.InheritDown'];
  22.     },
  23.  
  24.     getGlobalCommands: function () {
  25.         return ['Ribbon.Permission.Manage.InheritDown'];
  26.     },
  27.  
  28.  
  29.     canHandleCommand: function (commandId) {
  30.         if (commandId === 'Ribbon.Permission.Manage.InheritDown') {
  31.             return permInheritEnabled;
  32.         }
  33.     },
  34.  
  35.     handleCommand: function (commandId, properties, sequence) {
  36.         if (commandId === 'Ribbon.Permission.Manage.InheritDown') {
  37.             var doInherit = confirm('You are about to inherit permissions from his object to all of the child objects having custom permissions. Any custom permissions on the child objects will be lost.');
  38.             if (doInherit) {
  39.                 __doPostBack('inheritDown', '');
  40.             }
  41.         }
  42.  
  43.     },
  44.  
  45.     getId: function () {
  46.         return "CustomPermission.UI.PageComponent";
  47.     }
  48. }
  49.  
  50. CustomPermission.UI.PageComponent.registerClass('CustomPermission.UI.PageComponent', CUI.Page.PageComponent);
  51.  
  52. CustomPermission.UI.PageComponent.instance = new CustomPermission.UI.PageComponent();
  53. CustomPermission.UI.PageComponent.initialize();
  54.  
  55. SP.SOD.notifyScriptLoadedAndExecuteWaitingJobs("InheritPermissions.js");

The InheritPermissions WebControl is injected into the pages via an AdditionalPageHead delegate control.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Control
  4.     ControlAssembly="$SharePoint.Project.AssemblyFullName$"
  5.     ControlClass="InheritPermissions.InheritPermissions"
  6.     Sequence="50" Id="AdditionalPageHead"/>
  7. </Elements>

We include our control into the Safe Control Entries:

image

We activate the functionality of the control, if the host page is a permission page (e.g. it is derived from the CBaseAclPage class).

  1. class InheritPermissions : WebControl
  2. {
  3.     bool _isInheritButtonEnabled = false;
  4.  
  5.     protected override void OnInit(EventArgs e)
  6.     {
  7.         base.OnInit(e);
  8.  
  9.         CBaseAclPage baseAclPage = this.Page as CBaseAclPage;
  10.         if (baseAclPage != null)
  11.         {
  12.             baseAclPage.ParseAclObjFromRequest();
  13.  
  14.             SPSecurableObject securable = this.Securable;
  15.  
  16.             if (securable != null)
  17.             {
  18.  
  19.                 if ((this.Page.IsPostBack) && (this.Page.Request["__EVENTTARGET"] == "inheritDown"))
  20.                 {
  21.                     InheritPermissionsDown(securable);
  22.                 }
  23.  
  24.                 _isInheritButtonEnabled = ((SPFarm.Local.CurrentUserIsAdministrator(true)) && (securable.HasSubItemWithUniquePermissions()));
  25.  
  26.                 // inject a JavaScript variable into the page to support client side PageComponent
  27.                 // see canHandleCommand method of CustomPermission.UI.PageComponent (InheritPemissions.js)
  28.                 string enabledScript = (_isInheritButtonEnabled) ? "true" : "false";
  29.                 string script = string.Format(@"<script type=""text/javascript"">
  30.                                var permInheritEnabled = {0}
  31.                             </script>", enabledScript);
  32.                 this.Page.ClientScript.RegisterStartupScript(typeof(SPRibbon), "perminherit", script, false);
  33.             }
  34.         }
  35.     }
  36.  
  37.     protected override void OnPreRender(EventArgs e)
  38.     {
  39.         if (!_isInheritButtonEnabled)
  40.         {
  41.             SPRibbon ribbon = SPRibbon.GetCurrent(this.Page);
  42.             ribbon.TrimById("Ribbon.Permission.Manage.InheritDown");
  43.         }
  44.     }
  45.  
  46.     private SPSecurableObject Securable
  47.     {
  48.         get
  49.         {
  50.             SPSecurableObject securable = null;
  51.             UserRoles userRoles = this.Page as UserRoles;
  52.  
  53.             if (userRoles != null)
  54.             {
  55.                 securable = userRoles.m_Securable;
  56.             }
  57.  
  58.             return securable;
  59.         }
  60.     }
  61.  
  62.     // see further details in the code download…
  63.     
  64. }

We reuse the former code samples mentioned above: code details about how to inherit down permissions are discussed in this post, how to detect items with unique permission is shown in this post.

In the ParseAclObjFromRequest method we invoke the protected method with the same name of the parent page of CBaseAclPage type, thus achieve we a reference in this.Securable to the securable object (web site, list, folder, etc.) the permissions were set on.

We push the permissions down to child object if the page was posted back with the ‘inheritDown’ event target.

The value of the _isInheritButtonEnabled determines if the PageComponent implemented in InheritPermissions.js support the ‘Ribbon.Permission.Manage.InheritDown’ command. The value depends on two factors: it is enabled if the current user is a farm administrator and if the securable object has sub items with unique permissions. See my notes in the former post regarding the shortcomings of the check of unique permissions on child items. Even this check is not perfect we apply the same logic to remain consistent with the standard features / messages.

In my first attempt I invoked the InheritPermissionsDown method from the OnPreRender method and not from the OnInit method. However, the standard JavaScript functions of the permissions page were already registered by this time, so the message ‘Some content on this site has unique permissions which are not controlled from this page.’ were displayed even if we just pushed down the permissions right now. I found this behavior pretty disturbing. Including the logic in the OnInit method we push the permissions down before the standard scripts get registered (see the SetExceptionStatus method of the UserRoles class, derived from the CBaseAclPage type, invoked from InitPage invoked from OnLoad), so there is no inconsistent message displayed.

The button should be hidden if the user is no farm admin or if the item has no subitem with unique permissions (in this case the value of the _isInheritButtonEnabled  is false). We should invoke the TrimById method of the current ribbon instance, but in this case from the OnPreRender method.

Let’s see the new button in action. It is displayed on the ribbon if the current item has child items with unique permissions :

image

If the user clicks on the button, a warning is displayed. After confirmation of the action, the permissions are pushed down to child items.

image

You can download the sample solution from here.

April 8, 2014

Mysterious “File Not Found” Error When Working With the ECMAScript Client Object Model

Recently I was debugging a very simple JavaScript in Internet Explorer 9 with the F12 Developer Tools, when received a “File Not Found” error. In the script I tried to open a site via

webToCheck = context.get_site().openWeb(‘/Subweb’);

and it seemed as the site did not exist. However, the same site could be opened from the Managed Client Object Model.

I launched Fiddler to check what happens in the background and was surprised to see another web site (let’s call it SubwebWrong) in the request. This web site really did not exist, so the response of the server was reasonable.

image

But where did that site name came from? I found a former entry in the Watch window of the Developer Tools, that referred to this site:

webToCheck = context.get_site().openWeb(‘/SubwebWrong’);

So it seems that this command was executed automatically by IE, that caused the script to malfunction. Pretty strange behavior, indeed.

During debugging I received an erratic “Access denied. You do not have permission to perform this action or access this resource.” error as well. It happened typically after working for a longer time on the same page. I assume that the reason is that the form digest on the page was timed out.

April 7, 2014

How to Use JavaScript to Delete Short-Term Locks from Documents Opened from SharePoint?

After I find a way to delete the short-term lock using PowerShell, I decided to check how Word clears the lock it put on the file, and try to simulate the same network traffic using JavaScript. See again this post regarding the details of the background communication when opening a document from a SharePoint library, but if you are really hardcore, you will find this documentation to be useful too.

As usual I utilized Fiddler to capture the network traffic between Word and SharePoint, and found the following steps:

First, Word calls _vti_bin/_vti_aut/author.dll of FrontPage Server Extensions (FSE) via HTTP POST and sends a package similar to this one (assuming the URL of your document is http://yoursite.company.com/subsite/library/yourdocument.docx):

method=getDocsMetaInfo%3a14%2e0%2e0%2e6009&url%5flist=%5bhttp%3a%2f%2fyoursite%2ecompany%2ecom%2fsubsite%2flibrary%2fyourdocument%2edocx%3bhttp%3a%2f%2fyoursite%2ecompany%2ecom%2fsubsite%2flibrary%5d&listHiddenDocs=false&listLinkInfo=false

The decoded version of the content:

method=getDocsMetaInfo:14.0.0.6009&url_list=[http://yoursite.company.com/subsite/library/yourdocument.docx;http://yoursite.company.com/subsite/library%5D&listHiddenDocs=false&listLinkInfo=false

Next to the getDocsMetaInfo method you can see the encoded version number of FSE (14.0.0.6009), as well the the url_list parameter that contains both the document and library URLs. (I  found later that it’s enough to include only the document URL, since we need only the metadata of the document, but not of the library).

The response of the server should be similar to this one (assumed that the file is already locked by the user):

Content-type: application/x-vermeer-rpc

<html><head><title>vermeer RPC packet</title></head>
<body>
<p>method=getDocsMetaInfo:14.0.0.6009
<p>document_list=
<ul>
<ul>
<li>document_name=library/yourdocument.docx
<li>meta_info=
<ul>
<li>vti_etag
<li>SW|"{A4E045C9-2160-47B6-A4E5-06B4A51DA0B4},1"
<li>vti_parserversion
<li>SR|14.0.0.6117
<li>vti_folderitemcount
<li>IR|0
<li>vti_timecreated
<li>TR|23 Aug 2012 11:00:23 -0000
<li>_Category
<li>SW|
<li>vti_canmaybeedit
<li>BX|true
<li>vti_author
<li>SR|domain\user1
<li>_dlc_DocIdItemGuid
<li>SW|a4e045c9-2160-47b6-a4e5-06b4a51da0b4
<li>vti_sourcecontrollockexpiresvalue
<li>TR|28 Mar 2014 09:32:06 -0000
<li>vti_approvallevel
<li>SR|
<li>vti_categories
<li>VW|
<li>vti_level
<li>IR|1
<li>vti_foldersubfolderitemcount
<li>IR|0
<li>vti_modifiedby
<li>SR|SHAREPOINT\system
<li>vti_assignedto
<li>SR|
<li>Keywords
<li>SW|
<li>_Status
<li>SW|
<li>vti_sourcecontrollockid
<li>SW|{3F9C6295-354F-449A-B38B-C7BA192E3EA2}

<li>vti_filesize
<li>IR|487936
<li>ContentTypeId
<li>SW|0x0101006727D7B3E7C4C24DB7D5B98B2F8901BB
<li>_dlc_DocId
<li>SW|SITE-351-479
<li>vti_title
<li>SR|Division X
<li>_Author
<li>SW|John
<li>vti_timelastmodified
<li>TR|23 Aug 2012 11:00:23 -0000
<li>vti_sourcecontrolmultiuserchkoutby
<li>VR|DOMAIN\\User2
<li>vti_candeleteversion
<li>BR|true
<li>_dlc_DocIdUrl
<li>SW|
http://yoursite.company.com/subsite/_layouts/DocIdRedir.aspx?ID=SITE-351-479, SITE-351-479
<li>_Comments
<li>SW|
<li>vti_sourcecontrolcheckedoutby
<li>SR|DOMAIN\User2

<li>vti_sourcecontroltimecheckedout
<li>TR|28 Mar 2014 08:32:06 -0000
<li>vti_sourcecontrollocktype
<li>IR|0
<li>vti_sourcecontrolversion
<li>SR|V1.0
<li>vti_sourcecontrolcookie
<li>SR|fp_internal
<li>Subject
<li>SW|
<li>vti_sourcecontrollockexpires
<li>TR|28 Mar 2014 09:32:06 -0000

<li>vti_rtag
<li>SW|rt:A4E045C9-2160-47B6-A4E5-06B4A51DA0B4@00000000001
</ul>
</ul>
</ul>
<p>urldirs=
<ul>
<ul>
<li>url=library
<li>meta_info=
<ul>
<li>vti_isexecutable
<li>BR|false
<li>vti_listenableminorversions
<li>BR|false
<li>vti_listenablemoderation
<li>BR|false
<li>vti_rss_ChannelDescription
<li>SW|RSS-Feed for the List ‘library’.
<li>vti_rtag
<li>SW|rt:69EB8F13-1D5E-4368-9897-4474C14FDA2E@00000000000
<li>vti_etag
<li>SW|"{69EB8F13-1D5E-4368-9897-4474C14FDA2E},0"
<li>vti_hassubdirs
<li>BR|false
<li>vti_rss_ItemLimit
<li>IW|25
<li>vti_folderitemcount
<li>IR|14
<li>vti_timecreated
<li>TR|07 Feb 2008 12:22:51 -0000
<li>vti_listname
<li>SR|{A434F6B1-ACDE-49F8-97F3-BAED36601F75}
<li>vti_listtitle
<li>SR|library
<li>vti_canmaybeedit
<li>BX|true
<li>vti_listrequirecheckout
<li>BR|false
<li>vti_rss_DisplayOnQuicklaunch
<li>IW|0
<li>vti_rss_DisplayRssIcon
<li>IW|1
<li>vti_rss_ChannelTitle
<li>SW|Editor: Library
<li>vti_rss_ChannelImageUrl
<li>SW|/subsite/_layouts/images/homepage.gif
<li>vti_rss_DayLimit
<li>IW|7
<li>vti_level
<li>IR|1
<li>vti_isbrowsable
<li>BR|true
<li>vti_isscriptable
<li>BR|false
<li>vti_listbasetype
<li>IR|1
<li>vti_modifiedby
<li>SR|SHAREPOINT\system
<li>vti_foldersubfolderitemcount
<li>IR|13
<li>vti_listservertemplate
<li>IR|101
<li>vti_listenableversioning
<li>BR|true
<li>vti_candeleteversion
<li>BR|true
<li>vti_dirlateststamp
<li>TW|28 Mar 2014 08:36:18 -0000
<li>docid_msft_hier_listid
<li>IW|351
<li>docid_msft_hier_list_siteid
<li>SW|bea05316-e059-48a2-890b-c1c22c765367
<li>vti_timelastmodified
<li>TR|19 Mar 2014 13:51:30 -0000
<li>vti_nexttolasttimemodified
<li>TR|06 Mar 2013 13:19:58 -0000
<li>vti_rss_LimitDescriptionLength
<li>IW|0
<li>docid_msft_hier_list_guid
<li>SW|a434f6b1acde49f897f3baed36601f75
</ul>
</ul>
</ul>
</body>
</html>

Regarding the format and content of the response, see this article. If we omit the library URL from the url_list parameter as stated above, the second part of the response, related to the library metadata won’t be included as well.

In this case, the most important information is in the vti_sourcecontrollockid, but vti_sourcecontrolcheckedoutby and vti_sourcecontrollockexpires contains useful information either.

In the second step, the SOAPAction http://schemas.microsoft.com/sharepoint/soap/ICellStorages/ExecuteCellStorageRequest of the CellStorage web service (/_vti_bin/cellstorage.svc/CellStorageService) is invoked, and a content like the one below is sent:

–urn:uuid:8cfcbb22-dd52-4889-b29d-9ff2dcf909b2
Content-ID: <f13ad06d-8530-4af1-8cf3-d6d75c1635d4@tempuri.org>
Content-Transfer-Encoding: 8bit
Content-Type: application/xop+xml;charset=utf-8;type="text/xml; charset=utf-8"

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><RequestVersion Version="2" MinorVersion="0" xmlns="http://schemas.microsoft.com/sharepoint/soap/"/><RequestCollection CorrelationId="{35E42C96-FE02-41FE-B4D8-F7DEC43AF784}" xmlns="http://schemas.microsoft.com/sharepoint/soap/"><Request Url="http://yoursite.company.com/subsite/library/yourdocument.docx" RequestToken="1"><SubRequest Type="ExclusiveLock" SubRequestToken="1"><SubRequestData ExclusiveLockRequestType="ReleaseLock" ExclusiveLockID="{3F9C6295-354F-449A-B38B-C7BA192E3EA2}"/></SubRequest></Request></RequestCollection></s:Body></s:Envelope>
–urn:uuid:8cfcbb22-dd52-4889-b29d-9ff2dcf909b2–

See the two important parameters (document URL and lock ID) above in the request.

In case of success, the server should respond with a package like this one:

–uuid:4f762581-5bb8-4391-95a4-7dc08878025a+id=12
Content-ID: <http://tempuri.org/0&gt;
Content-Transfer-Encoding: 8bit
Content-Type: application/xop+xml;charset=utf-8;type="text/xml"

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><ResponseVersion Version="2" MinorVersion="0" xmlns="http://schemas.microsoft.com/sharepoint/soap/"/><ResponseCollection WebUrl="http://yoursite.company.com/subsite&quot; xmlns="http://schemas.microsoft.com/sharepoint/soap/"><Response Url="http://yoursite.company.com/subsite/library/yourdocument.docx&quot; RequestToken="1" HealthScore="0"><SubResponse SubRequestToken="1" ErrorCode="Success" HResult="0"><SubResponseData/></SubResponse>
36
</Response></ResponseCollection></s:Body></s:Envelope>
37

–uuid:4f762581-5bb8-4391-95a4-7dc08878025a+id=12–

Having this request and response formats we can start creating our own custom JavaScript lock releaser.

I embedded the my JavaScript code into a SharePoint page via the following HTML snippet:

<script type="text/javascript" src="/_layouts/Unlock.js"></script>

<input id="documentUrl" type="text" style="width:600px" value="http://intranet.contoso.com/DocLib/Test.docx"/&gt;
<input type="button" id="submitRequest" onclick="getLockForDoc()" value="Check lock">

Let’s see the functionality in the Unlock.js. I defined the following helper function in JavaScript:

  1. // define String.format function
  2. if (!String.prototype.format) {
  3.     String.prototype.format = function () {
  4.         var args = arguments;
  5.         return this.replace(/{(\d+)}/g, function (match, number) {
  6.             return typeof args[number] != 'undefined'
  7.               ? args[number]
  8.               : match
  9.             ;
  10.         });
  11.     };
  12. }

Then introduced Properties to make parsing the FSE response easier. I had to create my own unescapeEx function to be able to decode the encoded value of the vti_sourcecontrollockid (that is {3F9C6295-354F-449A-B38B-C7BA192E3EA2} instead of the standard %7B3F9C6295-354F-449A-B38B-C7BA192E3EA2%7D), as the standard JavaScript methods (like unescape, decodeURI  or decodeURIComponent) had issues with that. In this function we replace the encoded chars with their decoded values based on the decimal code using a RegEx replace.

  1. function Properties(source) {
  2.     this.source = source;
  3.     this.offspring = [];
  4.  
  5.     this.getPropValue = function (propName) {
  6.         var propValue = "";
  7.         var propNameFullLine = '<li>' + propName + '\n';
  8.         var startPos = this.source.indexOf(propNameFullLine);
  9.         if (startPos > -1) {
  10.             var endPos = this.source.substr(startPos + propNameFullLine.length).indexOf('\n');
  11.             if (endPos > 0) {
  12.                 propValue = this.source.substr(startPos + propNameFullLine.length, endPos);
  13.                 // trim leading chars
  14.                 // http://msdn.microsoft.com/en-us/library/office/ms460937(v=office.14).aspx
  15.                 var strDummy = '<li>SW|';
  16.                 propValue = unescapeEx(propValue.substr(strDummy.length));
  17.             }
  18.         }
  19.  
  20.         return propValue;
  21.     }
  22.  
  23.     // this function is intended to be accessible from the object itself (e.g. private)
  24.     var unescapeEx = function (value) {
  25.         var result = value.replace(/&#([0-9]|[1-9][0-9]|[[01][0-9][0-9]|2[0-4][0-9]|25[0-5]);/g, function (str, match) { return String.fromCharCode(match); });
  26.  
  27.         return result;
  28.     }
  29. }

We get the lock status using the getLockForDoc method. Note, that the URL of the author.dll is hardcoded in this case, so you should update it.

  1. function getLockForDoc() {
  2.  
  3.     var docUrl = $('#documentUrl').val();
  4.     var escapedDocUrl = encodeURI(docUrl);
  5.  
  6.     $.ajax({
  7.         url: 'http://intranet.contoso.com/_vti_bin/_vti_aut/author.dll&#039;,
  8.         type: 'POST',
  9.         contentType: 'application/x-www-form-urlencoded',
  10.         headers: {
  11.             'MIME-Version': '1.0',
  12.             'User-Agent': 'MSFrontPage/14.0',
  13.             'Accept': 'auth/sicily',
  14.             'X-Vermeer-Content-Type': 'application/x-www-form-urlencoded'
  15.         },
  16.         data: 'method=getDocsMetaInfo%3a14%2e0%2e0%2e6009&url%5flist=%5b' + escapedDocUrl + '%5d&listHiddenDocs=false&listLinkInfo=false',
  17.         complete: function (result) {
  18.             if ((result.readyState == 4) && (result.status == 200)) {
  19.                 var rawResponse = result.responseText;
  20.                 var startPos = rawResponse.indexOf('<li>meta_info=\n<ul>');
  21.                 if (startPos > 0) {
  22.                     var endPos = rawResponse.substr(startPos).indexOf('</ul>');
  23.                     if (endPos > 0) {
  24.                         var props = new Properties(rawResponse.substr(startPos, endPos));
  25.                         var lockId = props.getPropValue('vti_sourcecontrollockid');
  26.                         var checkedOutBy = props.getPropValue('vti_sourcecontrolcheckedoutby');
  27.                         var lockExpires = props.getPropValue('vti_sourcecontrollockexpires');
  28.                         if (lockId == "") {
  29.                             alert("File is not locked.");
  30.                         }
  31.                         else {
  32.                             if (confirm(String.format("File is locked by '{0}' until '{1}', LockId = '{2}'.\r\nUnlocking the file can cause problems if the user is still editing the file and would like to save it later.\r\nDo you want to clear the lock?", checkedOutBy, lockExpires, lockId))) {
  33.                                 releaseLock(escapedDocUrl, lockId);
  34.                             }
  35.                         }
  36.                     }
  37.                 }
  38.             }
  39.         }
  40.     });
  41. }

Finally, the lock is released by calling the releaseLock method. The URL is hardcoded in this case as well, so please fix it if you would like to test. The error checking when processing the response is rather simple, you can improve it if you wish.

  1. // message template to unlock a document
  2. var releseLockReq = '\r\n';
  3. releseLockReq += '–urn:uuid:8cfcbb22-dd52-4889-b29d-9ff2dcf909b2\r\n';
  4. releseLockReq += 'Content-ID: <f13ad06d-8530-4af1-8cf3-d6d75c1635d4@tempuri.org>\r\n';
  5. releseLockReq += 'Content-Transfer-Encoding: 8bit\r\n';
  6. releseLockReq += 'Content-Type: application/xop+xml;charset=utf-8;type="text/xml; charset=utf-8"\r\n';
  7. releseLockReq += '\r\n';
  8. releseLockReq += '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><RequestVersion Version="2" MinorVersion="0" xmlns="http://schemas.microsoft.com/sharepoint/soap/"/><RequestCollection CorrelationId="{35E42C96-FE02-41FE-B4D8-F7DEC43AF784}" xmlns="http://schemas.microsoft.com/sharepoint/soap/"><Request Url="{0}" RequestToken="1"><SubRequest Type="ExclusiveLock" SubRequestToken="1"><SubRequestData ExclusiveLockRequestType="ReleaseLock" ExclusiveLockID="{1}"/></SubRequest></Request></RequestCollection></s:Body></s:Envelope>\r\n';
  9. releseLockReq += '–urn:uuid:8cfcbb22-dd52-4889-b29d-9ff2dcf909b2–\r\n';
  10.  
  11. function releaseLock(escapedDocUrl, lockId) {
  12.     $.ajax({
  13.         url: 'http://intranet.contoso.com/_vti_bin/cellstorage.svc/CellStorageService&#039;,
  14.         type: 'POST',
  15.         contentType: 'multipart/related; type="application/xop+xml"; boundary="urn:uuid:8cfcbb22-dd52-4889-b29d-9ff2dcf909b2"; start="<f13ad06d-8530-4af1-8cf3-d6d75c1635d4@tempuri.org>"; start-Info="text/xml; charset=utf-8"',
  16.         headers: {
  17.             'MIME-Version': '1.0',
  18.             'User-Agent': 'Microsoft Office Upload Center 2010 (14.0.6124) Windows NT 6.1',
  19.             'SOAPAction': 'http://schemas.microsoft.com/sharepoint/soap/ICellStorages/ExecuteCellStorageRequest&#039;
  20.         },
  21.         data: String.format(releseLockReq, escapedDocUrl, lockId),
  22.         complete: function (result) {
  23.             var succeeded = false;
  24.             if ((result.readyState == 4) && (result.status == 200)) {
  25.                 var rawResponse = result.responseText;
  26.                 // check the result the primitive way
  27.                 succeeded = (rawResponse.indexOf('ErrorCode="Success"') > -1);
  28.             }
  29.             if (succeeded) {
  30.                 alert('Lock is released.');
  31.             } else {
  32.                 alert('An error has occured during the request. Lock is not released, try again later.');
  33.             }
  34.         }
  35.     });
  36. }

The screenshots below illustrate the functionality of the scripts. First, the lock information is displayed.

image

We receive the feedback after successfully releasing the lock.

image

Note, that this method works only as long as the document is checked out by the current user, otherwise an error will be returned.

It might make not to much sense to use JavaScript to release the lock, since one can achieve the same using Word alone (via simply opening and closing the document). However, I have further plans with the script, and my goal was to get a deeper insight into the internals of the FSE communication protocol and the related stuff. Hopefully others find something useful in the sample as well.

April 3, 2014

How to Use PowerShell to Delete Short-Term Locks from Documents Opened from SharePoint?

Filed under: PowerShell, SP 2010 — Tags: , — Peter Holpar @ 21:58

Recently one of our users complained, that he can not open an Excel document for editing from a SharePoint document library, as he gets a warning that the file is locked by himself. It should be a known issue, as documented here,  however in our case it did not help waiting 10 minutes, or even hours. I check the AllDocs table in the content database, and found very strange values in the record corresponding to the locked document (filtered for DirName and LeafName fields). The document was really locked by the complaining user (CheckoutUserId field), the CheckoutDate field contained the current time, and (that was the surprise) the CheckoutExpires field contained a date value that was about 2-3 month before the current date.

Although one could delete the dirty data from the database, as shown for example in this article or in this post, that would be an unsupported solution, so I wanted to find an officially acceptable way. The information I found in this post was closer to my expectations.

As I checked the status of the file from PowerShell I get an output similar to the screenshot below, that suggests that the file is not locked:

$web = Get-SPWeb http://intranet.contoso.com
$list = $web.Lists["DocLib"]
$item = $list.GetItemById(2)
$file = $item.File
$file

image

It could be a reason, why SharePoint does not try to delete the lock, although I found it still rather odd.

As I tried to call the ReleaseLock method on the $file I got an error stating that the file is not locked. What helped in this case was not less surprising as the former situation. I checked out the file and then made an undo for the checkout:

$ft = New-Object System.TimeSpan(10000)
$file.Lock([Microsoft.SharePoint.SPFile+SPLockType]::Exclusive, "test lock", $ft)
$file.UndoCheckOut()

After this the lock status of the file was cleared in the database as well.

If the file happened to be locked by a user other than the current user, and we would like to release the lock, we get an error message stating that the file is locked by an other user. However, there is a way to release the lock even in this case using PowerShell and that is possible via impersonating the user that holds the lock:

$web = Get-SPWeb http://intranet.contoso.com
$list = $web.Lists["DocLib"]
$item = $list.GetItemById(2)
$file = $item.File
$userId = $file.LockedByUser.ID
$user = $web.AllUsers.GetByID($userId)
$impSite= New-Object Microsoft.SharePoint.SPSite($web.Url, $user.UserToken);
$impWeb = $impSite.OpenWeb();
$impList = $impWeb.Lists[$list.Title]
$impItem = $impList.GetItemById($item.ID)
$impFile = $impItem.File
$impFile.ReleaseLock($impFile.LockId)

April 2, 2014

How to Find the Right Tool in SharePoint Central Administration Faster

Probably it’s not just because I’m a developer and not an IT-Pro, but when working with the Central Administration of SharePoint I often ask myself questions like these ones:

Where should I search the incoming e-mail configuration? In the General Application Settings? Or on the System Settings page?

As you know, the items on the Central Administration pages are stored as custom actions and custom action groups in the background. That provides a great opportunity to extend these pages via registering your own custom actions, as well makes it easy to create tools that enumerate the items on the pages, as I illustrated in this post a few years ago. I planned to create a UI extension for the Central Administration already at that time, to make it easier to find the right admin page, but until now I had not fulfilled this plan: to add a simple text input field to the start page of the Central Administration where you can type a word and an autocomplete list would display all of the matching actions.

Let’s see how to achieve that goal!

First I created a console application (SPCustomActionsExtract) that helps us to export the custom actions and groups that we will use in a JavaScript later.

In this project I defined my CustomActionGroup class that should hold the information extracted from the corresponding SPCustomActionGroupElement object. The CustomActionGroup class has the properties we need to extract, and these properties have the very same name as their counterparts in SPCustomActionGroupElement. It is important, since it makes the extraction process via Reflection very simple as we will see it later in the GetCustomActionGroup method. The class is decorated with the DataContract attribute and the properties are decorated with the DataMember attribute, since we wish to serialize the object later.

  1. [DataContract]
  2. internal class CustomActionGroup
  3. {
  4.     [DataMember]
  5.     internal string Id { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Title { get; set; }
  9.  
  10.     [DataMember]
  11.     internal string ImageUrl { get; set; }
  12.  
  13.     [DataMember]
  14.     internal List<CustomAction> CustomActions { get; set; }
  15. }

We have a similar class called CustomAction that corresponds to the SPCustomActionElement class of SharePoint. In this case, the extraction process is implemented in the GetCustomAction method (see it later).

  1. [DataContract]
  2. internal class CustomAction
  3. {
  4.     [DataMember]
  5.     internal string Title { get; set; }
  6.  
  7.     [DataMember]
  8.     internal string Description { get; set; }
  9.     
  10.     [DataMember]
  11.     internal string GroupId { get; set; }
  12.     
  13.     [DataMember]
  14.     internal string Location { get; set; }
  15.  
  16.     [DataMember]
  17.     internal string UrlAction { get; set; }
  18. }

Since we would like to use the output in JavaScript, serialization into a JSON format seems to be a good idea. I borrowed the code of the JSON serializer from this CodeProject article.

  1. internal static class JsonHelper
  2. {
  3.     public static string JsonSerializer<T>(T t)
  4.     {
  5.         DataContractJsonSerializer ser = new DataContractJsonSerializer(typeof(T));
  6.         MemoryStream ms = new MemoryStream();
  7.         ser.WriteObject(ms, t);
  8.         string jsonString = Encoding.UTF8.GetString(ms.ToArray());
  9.         ms.Close();
  10.         return jsonString;
  11.     }  
  12. }

In the GetCustomActionGroups method we get all of the custom actions for a specific location. The SPCustomActionGroupElement instances are wrapped into CustomActionGroup objects in the GetCustomActionGroup method.

  1. private List<CustomActionGroup> GetCustomActionGroups(SPWeb web, String scope, String location)
  2. {
  3.     List<CustomActionGroup> customActionGroups = new List<CustomActionGroup>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionGroupElement> QueryForCustomActionGroups(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActionGroups = spElementProviderType.GetMethod("QueryForCustomActionGroups",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActionGroups != null)
  30.             {
  31.                 // result is List<SPCustomActionGroupElement>
  32.                 IEnumerable spCustomActionGroups = (IEnumerable)mi_QueryForCustomActionGroups.Invoke(spElementProvider,
  33.                     new Object[] { web, scope, location });
  34.  
  35.                 customActionGroups = spCustomActionGroups.Cast<Object>().AsQueryable().ToList().ConvertAll(
  36.                     spCag => GetCustomActionGroup(spCag));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActionGroups;
  42. }
  43.  
  44. private CustomActionGroup GetCustomActionGroup(object spCustomActionGroup)
  45. {
  46.     CustomActionGroup result = new CustomActionGroup();
  47.  
  48.     Type customActionGroupType = typeof(CustomActionGroup);
  49.     PropertyInfo[] cagPis = customActionGroupType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionGroupElement internal class
  54.     Type spCustomActionGroupElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionGroupElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomActionGroup.GetType() == spCustomActionGroupElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         cagPis.Where(cagPi => cagPi.PropertyType == typeof(String)).ToList().ForEach(cagPi =>
  61.         {
  62.             string propName = cagPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionGroupElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 cagPi.SetValue(result, pi.GetValue(spCustomActionGroup, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

In the GetCustomActions method we get all of the custom actions based on the location and group ID. The SPCustomActionElement instances are wrapped into CustomAction objects in the GetCustomAction method.

  1. private List<CustomAction> GetCustomActions(SPWeb web, SPList list, String scope, String location, String groupId)
  2. {
  3.     List<CustomAction> customActions = new List<CustomAction>();
  4.  
  5.     // hack to get the Microsoft.SharPoint assembly
  6.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  7.     // and a reference to the type of the SPElementProvider internal class
  8.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  9.  
  10.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  11.          null, new Type[0], null);
  12.  
  13.     if (ci_SPElementProvider != null)
  14.     {
  15.         // spElementProvider will be of type internal class
  16.         // Microsoft.SharePoint.SPElementProvider
  17.         // defined in Microsoft.SharePoint assembly
  18.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  19.  
  20.         if (spElementProvider != null)
  21.         {
  22.             // we call
  23.             // internal List<SPCustomActionElement> QueryForCustomActions(SPWeb web, SPList list, string scope, string location, string groupId)
  24.  
  25.             MethodInfo mi_QueryForCustomActions = spElementProviderType.GetMethod("QueryForCustomActions",
  26.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  27.                     new Type[] { typeof(SPWeb), typeof(SPList), typeof(String), typeof(String), typeof(String) }, null
  28.                     );
  29.             if (mi_QueryForCustomActions != null)
  30.             {
  31.                 // result is List<SPCustomActionElement>
  32.                 IEnumerable spCustomActions = (IEnumerable)mi_QueryForCustomActions.Invoke(spElementProvider,
  33.                     new Object[] { web, list, scope, location, groupId });
  34.  
  35.                 customActions = spCustomActions.Cast<Object>().AsQueryable().ToList()
  36.                                                                   .ConvertAll(spCa => GetCustomAction(spCa));
  37.             }
  38.         }
  39.     }
  40.  
  41.     return customActions;
  42. }
  43.  
  44. private CustomAction GetCustomAction(object spCustomAction)
  45. {
  46.     CustomAction result = new CustomAction();
  47.     
  48.     Type customActionType = typeof(CustomAction);
  49.     PropertyInfo[] caPis = customActionType.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance);
  50.  
  51.     // hack to get the Microsoft.SharPoint assembly
  52.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  53.     // and a reference to the type of the SPCustomActionElement internal class
  54.     Type spCustomActionElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionElement");
  55.  
  56.     // runtime check the type of the parameter
  57.     if (spCustomAction.GetType() == spCustomActionElementType)
  58.     {
  59.         List<String> propValues = new List<String>();
  60.         caPis.ToList().ForEach(caPi =>
  61.         {
  62.             string propName = caPi.Name;
  63.             System.Reflection.PropertyInfo pi = spCustomActionElementType.GetProperty(
  64.                 propName, BindingFlags.Public | BindingFlags.Instance);
  65.             if (pi != null)
  66.             {
  67.                 caPi.SetValue(result, pi.GetValue(spCustomAction, null), null);
  68.             }
  69.         });
  70.     }
  71.  
  72.     return result;
  73. }

The possible values of the locations for the Central Administration are defined in the locations list. You find these values on MSDN. In this case we ignored the start page (location "Microsoft.SharePoint.Administration.Default"), as it contains only duplications of custom actions from other pages.

In the ExtractCustomActions method we iterate through all of these locations, get the custom action groups, and each custom actions in the groups, accumulate them into the allCustomActionGroups list, and finally save the JSON serialized result into a file.

  1. // Central Administration Custom Action Locations
  2. //http://msdn.microsoft.com/en-us/library/bb802730.aspx
  3. List<String> locations = new List<String>() {
  4.                         "Microsoft.SharePoint.Administration.Applications",
  5.                         "Microsoft.SharePoint.Administration.Backups",
  6.                         "Microsoft.SharePoint.Administration.ConfigurationWizards",
  7.                         // we don't need duplicates, so we eliminate this one
  8.                         //"Microsoft.SharePoint.Administration.Default",
  9.                         "Microsoft.SharePoint.Administration.GeneralApplicationSettings",
  10.                         "Microsoft.SharePoint.Administration.Monitoring",
  11.                         "Microsoft.SharePoint.Administration.Security",
  12.                         "Microsoft.SharePoint.Administration.SystemSettings",
  13.                         "Microsoft.SharePoint.Administration.UpgradeAndMigration"
  14.                      };
  15.  
  16.  
  17. private void ExtractCustomActions()
  18. {
  19.     List<CustomActionGroup> allCustomActionGroups = new List<CustomActionGroup>();
  20.  
  21.     // get the site collection of the Central Administration web application
  22.     SPAdministrationWebApplication centralAdmin = SPAdministrationWebApplication.Local;
  23.     using (SPSite site = centralAdmin.Sites[0])
  24.     {
  25.         using (SPWeb web = site.OpenWeb())
  26.         {
  27.             locations.ForEach(location =>
  28.                 {
  29.                     List<CustomActionGroup> customActionGroup = GetCustomActionGroups(web, null, location);
  30.                     allCustomActionGroups.AddRange(customActionGroup);
  31.                     customActionGroup.ForEach(cag =>
  32.                         {
  33.                             cag.CustomActions = GetCustomActions(web, null, null, location, cag.Id);
  34.                         });
  35.                 });
  36.         }
  37.     }
  38.  
  39.     string customActions = JsonHelper.JsonSerializer(allCustomActionGroups);
  40.     File.WriteAllText("CustomActions.json", customActions);
  41. }

We should insert the following chunk of HTML code into the start page of the Central Administration, to achieve that I inserted a Content Editor Web Part (CEWP) to the right web part zone of the page. As you can see I utilized jQuery, the LINQ for JavaScript (ver.3.0.3-Beta4) library and jQuery UI autocomplete. The single HTML element is a text field to which we can attach the autocomplete behavior.

  1. <!– jQuery –>
  2. <script type="text/javascript" src="/_layouts/CACustomActions/js/jQuery/jquery-1.8.3.min.js"></script>
  3. <!– LINQ.js –>
  4. <script src="/_layouts/CACustomActions/js/linq.min.js" type="text/javascript"></script>
  5. <!– jQuery UI autocomplete –>
  6. <script type="text/javascript" src="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/js/jquery-ui-1.10.3.custom.min.js"></script>
  7. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/js/jquery-ui-1.10.3.custom/css/ui-lightness/jquery-ui-1.10.3.custom.min.css">
  8. <!– Our custom .js / .css components –>
  9. <script src="/_layouts/CACustomActions/js/CustomActions.js" type="text/javascript"></script>
  10. <link rel="stylesheet" type="text/css" href="/_layouts/CACustomActions/css/CustomActions.css">
  11.  
  12. <input id="autocompleteCustomActions" type="text"/>

In the CustomActions.js I have a variable called CustomActions.js that contains the JSON serialized output of the custom actions and groups from our SPCustomActionsExtract tool.

On page load we register an event handler that invokes the updateAutoComplete method whenever we type a text into the text field.

  1. $(document).ready(startScript);              
  2.         
  3. function startScript() {
  4.   registerEvents();
  5. }
  6.  
  7. function registerEvents() {     
  8.        $("#autocompleteCustomActions").keyup(function(e){
  9.        updateAutoComplete();
  10.     });
  11. }

In the first part of the updateAutoComplete method we compare the titles and descriptions of the existing custom actions to the filter value we typed in, and aggregating the result into the matchingCAs array. Next, the items are ordered alphabetically

  1. var searchedCA = $("#autocompleteCustomActions").val().toLowerCase();
  2.  
  3. var matchingCAs = new Array();
  4.  
  5. Enumerable.from(customActions).forEach(function (cag) {
  6.   Enumerable.from(cag.CustomActions).forEach(function (ca) {
  7.     // find the custom action based on the title and the description
  8.     // the comparision is case insensitive
  9.     if ((ca.Title.toLowerCase().indexOf(searchedCA) > -1) || ((ca.Description != undefined) && (ca.Description.toLowerCase().indexOf(searchedCA) > -1))) {
  10.       var desc = (ca.Description != undefined) ? ca.Description  : ""
  11.       matchingCAs.push({
  12.           // HACK?: we have to create a 'label' property that inlcudes the 'Title' and the 'Description'
  13.           // otherwise the item is not shown in the autocomplete list
  14.           // I don't know if it is a feature or a bug
  15.           label: ca.Title + " – " + desc,
  16.           caption: ca.Title,
  17.           description: desc,
  18.           groupTitle: cag.Title,
  19.           urlAction: ca.UrlAction,
  20.           imageUrl: cag.ImageUrl
  21.       });
  22.     }
  23.   });
  24. });
  25.  
  26. matchingCAs = Enumerable.from(matchingCAs).orderBy("$.label").toArray();

Then we display the result in the autocomplete box. We display the icon of the custom action group and the title of the custom action in the list, but as a tool tip the description of the custom action and the title of the group will be display as well. The URL of the custom action is set as a link on the item, so if you select an item, the corresponding page will be opened in the browser.

  1. $("#autocompleteCustomActions").autocomplete({
  2.     sortResults:true,
  3.     source: matchingCAs,
  4.     open: function() {
  5.         $('#autocompleteCustomActions').autocomplete('widget').width(350);
  6.     },
  7.     focus: function(event, ui) {
  8.         $('#autocompleteCustomActions').val(ui.item.label);
  9.         return false;
  10.     }
  11. })
  12. .data("ui-autocomplete")._renderItem = function(ul, item) {
  13.     return $("<li>")
  14.     .append("<a class='ca-text' href='" + item.urlAction + "' title='" + item.description + " (" + item.groupTitle + ")'><img class='ca-img' src='"+ item.imageUrl + "' /><span class='ca-wrapper'>" + item.caption + "</span></a>")
  15.     .appendTo(ul);
  16. };

The screenshots below illustrate the functionality of the sample. As we type the text, the autocomplete list changes dynamically:

image

If you hover over a list item, additionally information is displayed as tool tip, so we can easily decide if it is really the administrative option we need. If you click on an item in the list, the corresponding page will be opened.

image

You can download the sample application (the .json generator tool, web part, .js and .css files) from here.

The deployment steps:

Copy the content of the Layouts folder from the sample into the LAYOUTS folder (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\LAYOUTS) of your SharePoint server.

If your Central Administration contains special custom actions, you can (and should) update the value of the customActions variable at the very beginning of the CustomActions.js file (in the CACustomActions\js folder in LAYOUTS) with the content of the CustomActions.json generated by the SPCustomActionsExtract.exe tool. You should find the .json file in the bin\Debug folder of the project. The .json file I used (and that is included in the sample) was generated from a standard installation of the English version of SharePoint Server 2010 Enterprise edition, so if you have an other version (like Foundation or a German version) you should definitely regenerate the file and update the CustomActions.js file with the result.

Note: If you don’t like the idea of using a command line tool to (re)generate the structure queried from JavaScript, it is viable option to create a custom web part that performs the same actions on-demand (that means, on each page request) as this tool does, and includes the client side components (like JavaScript and css) as well. However, I found that since the registered custom actions do not change very frequently, so creating and deploying a web part seemed me a bit overkill in this case.

The web part (see WebPart\Find the Page You Need.dwp) can be imported to the start page of the the Central Administration, or if you find that easier, you can add a Content Editor Web Part (CEWP) to the page and set its source code based on the code snippet in this post above.

March 31, 2014

How to Integrate SignalR 2.0 in SharePoint 2010

Filed under: SignalR, SP 2010 — Tags: , — Peter Holpar @ 23:54

ASP.NET SignalR is a new library for ASP.NET developers that makes it incredibly simple to add real-time web functionality to your applications.” as stated on the official site of SignalR.

Recently I needed to find a solution that supports push-like notifications for a SharePoint 2010 web-based administration tool that should run in the background but should send notifications occasionally to the user started the request. Most of the forum discussions suggested SignalR, and the information and samples I found about this library on the web (like the above-mentioned official site or these tutorials) were really convincing.

Probably the most difficult part of the SharePoint 2010SignalR integration is the different .NET Framework version they use (.NET Framework 3.5, that is Common Language Runtime (CLR) 2.0 vs. .NET Framework 4.5, that is CLR 4.0). To bridge this gap we should include some kind of out-of-process communication channel (typically HTTP-based) that mediates between the parties.

Unfortunately, most of the SharePoint-related samples are either jokes (like the April Fools post about SharePointR exactly 2 years ago), outdated (for example using SignalR 1.x, like this solution), and / or based on SharePoint 2013 (like the posts here, here or this CodePlex solution).

To to make a long story short, I decided to create my own implementation for SharePoint 2010 using SignalR 2.0, and base the implementation on a WCF service as opposed to this HTTP Handler-based solution.

Solution Architecture

My solution contains four projects:

TaskNotifierService is an IIS hosted WCF service that acts as the SignalR Hub as well. It is a CLR 4.0  process.

SPTaskNotifier is a SharePoint 2010 application (that means CLR 2.0). There is an event receiver in this project, that calls the WCF service in the TaskNotifierService if a new task is assigned to a user. There are some simple JavaScript functions as well that support the SignalR integration on the client side, like registering the client and displaying notifications if a task was assigned to the current user.

TaskNotifierTestWeb is a simple web application. The project is based on the .NET Framework 4.5, but it is irrelevant as we use only client side code in an .html page. The goal of the project is to simplify testing the JavaScript code that interacts with SignalR without involving the SharePoint deployment process (like IISRESET etc.).

TaskNotifierTestConsole is a console application based on the .NET Framework 4.5. Again, the framework version is not important, any version that supports WCF would be OK. This application can be used to send test messages to the WCF host, and thus test the code that creates the WCF client proxy and calls the WCF service methods. Using this project we can test the functionality without SharePoint as well. If your goal is just to send test messages, but you are not interested in the code itself, you can use the WCF Test Client of Visual Studio either (more about that later).

Below is the architecture of these components including the possible connections between them. The core and indispensable component is the TaskNotifierService. The other components are optional. For example you can use either the TaskNotifierTestConsole, the SPTaskNotifier or the WCF Test Client to sent messages (e.g. call the WCF service method) to the TaskNotifierService, and it will forward the messages to the adequate client, let it be either the TaskNotifierTestWeb or the SPTaskNotifier.

image

TaskNotifierService

We start our code review with the core component, the TaskNotifierService. We define the data and service contracts of our WCF service in this project.

The TaskNotification is the data contract of the communication. It includes the Title of the new task, the LoginName of the user the task is assigned to and the URL of the display form of the task item.

  1. [DataContract]
  2. public class TaskNotification
  3. {
  4.   // the login name of the user the task is assigned to
  5.   [DataMember]
  6.   public string LoginName { get; set; }
  7.  
  8.   // the url of the task display form
  9.   [DataMember]
  10.   public string Url { get; set; }
  11.  
  12.   // the title of the task
  13.   [DataMember]
  14.   public string Title { get; set; }
  15. }

The ITaskNotifierService interface is the service contract, including only a single method called TaskCreated. This method has a single parameter of type TaskNotification.

  1. [ServiceContract]
  2. public interface ITaskNotifierService
  3. {
  4.   [OperationContract]
  5.   void TaskCreated(TaskNotification taskNotification);
  6. }

The TaskNotifierService class implements the ITaskNotifierService interface. In the implementation of the TaskCreated method we first get an instance of the current ConnectionManager then get the hub context via its GetHubContext<TaskNotifierHub> method. Finally we collects the client connections using the contextfor the user the task was assigned to (taskNotification.LoginName) and call the addTaskMessage method of these connections including the task title and URL as parameters (taskNotification.Title and taskNotification.Url):

  1. using System;
  2. using System.Diagnostics;
  3. using Microsoft.AspNet.SignalR;
  4. using Microsoft.AspNet.SignalR.Infrastructure;
  5.  
  6. namespace TaskNotifierService
  7. {
  8.   // NOTE: In order to launch WCF Test Client for testing this service, please select TaskNotifierService.svc or TaskNotifierService.svc.cs at the Solution Explorer and start debugging.
  9.   // NOTE:  WCF Test Client issue with '\' in LoginName. Escape it as '\\', like 'domain\\user' instead of 'domain\user'
  10.   // http://connect.microsoft.com/VisualStudio/feedback/details/632374/wcf-test-client-sends-incorrect-null-value-as-string-type-value-for-wrongly-escaped-entered-values
  11.   public class TaskNotifierService : ITaskNotifierService
  12.   {
  13.     public void TaskCreated(TaskNotification taskNotification)
  14.     {
  15.       Debug.Print("Task with title '{0}' was created.", taskNotification.Title);
  16.       IConnectionManager connectionManager = GlobalHost.ConnectionManager;
  17.       var context = connectionManager.GetHubContext<TaskNotifierHub>();
  18.  
  19.       context.Clients.Group(taskNotification.LoginName.ToUpper()).addTaskMessage(taskNotification.Title, taskNotification.Url);
  20.     }
  21.   }
  22. }

Note: We call the ToUpper method on the LoginName to make it case-insensitive, see the same for the Context.User.Identity.Name in the OnConnected method of the TaskNotifierHub below.

Our TaskNotifierHub class is a subclass of a SignalR Hub class. Whenever a new client is connected to the hub, we determine the user name corresponding the connection and assign the connection ID to the group identified by the user name.

  1. using System;
  2. using System.Threading.Tasks;
  3. using Microsoft.AspNet.SignalR;
  4.  
  5. namespace TaskNotifierService
  6. {
  7.   [Authorize]
  8.   public class TaskNotifierHub : Hub
  9.   {
  10.     public override Task OnConnected()
  11.     {
  12.       string name = Context.User.Identity.Name.ToUpper();
  13.  
  14.       Groups.Add(Context.ConnectionId, name);
  15.  
  16.       return base.OnConnected();
  17.     }
  18.   }
  19. }

The Startup class is responsible for the startup of our service hub. In its Configuration method we set up the configuration settings that enable accessing the hub from an external web site (see CORS, Cross-Origin Resource Sharing) and finally start the SignalR pipeline.

  1. using Microsoft.AspNet.SignalR;
  2. using Microsoft.Owin;
  3. using Microsoft.Owin.Cors;
  4. using Owin;
  5.  
  6. [assembly: OwinStartup(typeof(TaskNotifierService.Startup))]
  7.  
  8. namespace TaskNotifierService
  9. {
  10.   public class Startup
  11.   {
  12.     public void Configuration(IAppBuilder app)
  13.     {
  14.       // Branch the pipeline here for requests that start with "/signalr"
  15.       app.Map("/signalr", map =>
  16.       {
  17.         // Setup the CORS middleware to run before SignalR.
  18.         // By default this will allow all origins. You can
  19.         // configure the set of origins and/or http verbs by
  20.         // providing a cors options with a different policy.
  21.         map.UseCors(CorsOptions.AllowAll);
  22.         var hubConfiguration = new HubConfiguration
  23.         {
  24.           // You can enable JSONP by uncommenting line below.
  25.           // JSONP requests are insecure but some older browsers (and some
  26.           // versions of IE) require JSONP to work cross domain
  27.           EnableJSONP = true
  28.         };
  29.         // Run the SignalR pipeline. We're not using MapSignalR
  30.         // since this branch already runs under the "/signalr"
  31.         // path.
  32.         map.RunSignalR(hubConfiguration);
  33.       });
  34.     }
  35.   }
  36. }

TaskNotifierTestConsole

The second project, TaskNotifierTestConsole is a simple WCF client to test the WCF service hosted in IIS (see TaskNotifierService above), so a Service reference to the TaskNotifierService was added to this project.

  1. static void Main(string[] args)
  2. {
  3.   // TODO Update the login name to match the user's login name that is authenticated in the browser
  4.   SendTaskNotification("CONTOSO\\Administrator", "Test Task Title", "http://site/task&quot;);
  5. }

In the SendTaskNotification method we create a WCF proxy instance and call its TaskCreated method with the test parameters.

  1. private static void SendTaskNotification(string loginName, string title, string url)
  2. {
  3.   TaskNotifierServiceClient sc = TaskNotifierProxy;
  4.   sc.TaskCreated(new TaskNotification
  5.                         {
  6.                           LoginName = loginName,
  7.                           Title = title,
  8.                           Url = url
  9.                         });
  10. }

The configuration of the proxy (WCF bindings, etc.) are set from code, see TaskNotifierProxy property later at the TaskCreated event receiver of the SPTaskNotifier project. Of course, you could set the same values from a configuration file as well.

TaskNotifierTestWeb

The third project in the solution is the TaskNotifierTestWeb project. In this project we have a single page, default.htm that includes the communication JavaScript methods. We first include the references to the required JavaScript libraries, set up the hub URL, declare a proxy to reference the hub. Next a function is created that the hub can call to send notifications. In this case we simply append the information as a span to the current HTML page. Finally the connection is started.

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4.     <title>Task Notifier Test Page</title>
  5. </head>
  6. <body>
  7.     <div class="container">
  8.         <ul id="discussion">
  9.         </ul>
  10.     </div>
  11.     <!–Script references. –>
  12.     <!–TODO: Update the URLs to match your current configuration –>
  13.     <!–Reference the jQuery library. –>
  14.     <script src="http://localhost:57800/Scripts/jquery-1.6.4.min.js&quot; ></script>
  15.     <!–Reference the SignalR library. –>
  16.     <script src="http://localhost:57800/Scripts/jquery.signalR-2.0.2.min.js"></script>
  17.     <!–Reference the autogenerated SignalR hub script. –>
  18.     <script src="http://localhost:57800/signalr/hubs"></script>
  19.     <!–Add script to update the page and send messages.–>
  20.     <script type="text/javascript">
  21.         $(function () {
  22.             $.connection.hub.url = "http://localhost:57800/signalr&quot;;
  23.             // Declare a proxy to reference the hub.
  24.             var taskNotifier = $.connection.taskNotifierHub;
  25.             // Create a function that the hub can call to send notifications.
  26.             taskNotifier.client.addTaskMessage = function (title, url) {
  27.                 var anchor = $('<a />').attr('href', url).attr('target', '_blank').text(title);
  28.                 var anchorText = $('<span />').append(anchor).html();
  29.                 var text = "A task called '" + anchorText + "' was assigned to you.";
  30.                 $('#discussion').append(text).append('<br />');
  31.             };
  32.             // Start the connection.
  33.             $.connection.hub.start();
  34.         });
  35.     </script>
  36. </body>
  37. </html>

Important, that Windows Authentication should be enabled for the web site, otherwise the hub cannot determine the user name on the client connection.

SPTaskNotifier

The SPTaskNotifier is a simple SharePoint project that includes an event receiver that is triggered whenever a new item is created in a Tasks list.

First, we register our event handler to all lists created from the Tasks list template:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <Receivers ListTemplateId="107">
  4.       <Receiver>
  5.         <Name>TaskCreated</Name>
  6.         <Type>ItemAdded</Type>
  7.         <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
  8.         <Class>SPTaskNotifier.TaskCreated</Class>
  9.         <SequenceNumber>10000</SequenceNumber>
  10.       </Receiver>
  11.   </Receivers>
  12. </Elements>

In the ItemAdded method of the TaskCreated class we determine the Title and the owner of the new task, as well as the URL of the display form, and call the SendTaskNotification method with this parameters.

  1. public override void ItemAdded(SPItemEventProperties properties)
  2. {
  3.   Debug.Print("SPTaskNotifier.TaskCreated started.");
  4.  
  5.   try
  6.   {
  7.     SPListItem task = properties.ListItem;
  8.     if (task != null)
  9.     {
  10.       SPFieldUserValue userFieldValue = new SPFieldUserValue(properties.Web, task[SPBuiltInFieldId.AssignedTo] as string);
  11.       if ((userFieldValue != null) && (userFieldValue.User != null))
  12.       {
  13.         string url = string.Format("{0}{1}?ID={2}", properties.Web.Url, properties.List.DefaultDisplayFormUrl, task.ID);
  14.         this.SendTaskNotification(userFieldValue.User.LoginName, task.Title, url);
  15.       }
  16.       else
  17.       {
  18.         Debug.Print("SPTaskNotifier.TaskCreated: No user assigned to task ID={0}", task.ID);
  19.       }
  20.     }
  21.     else
  22.     {
  23.       Debug.Print("SPTaskNotifier.TaskCreated: No task found");
  24.     }
  25.  
  26.     base.ItemAdded(properties);
  27.   }
  28.   catch (Exception ex)
  29.   {
  30.     Debug.Print("SPTaskNotifier.TaskCreated exception: {0}\r\n{1}", ex.Message, ex.InnerException);
  31.   }
  32.  
  33.   Debug.Print("SPTaskNotifier.TaskCreated finished.");
  34. }

The SendTaskNotification method is very similar to the version we saw in the case of the TaskNotifierTestConsole project above, but includes a few lines that help debugging and tracing.

  1. private void SendTaskNotification(string loginName, string title, string url)
  2. {
  3.   Debug.Print("SPTaskNotifier.SendTaskNotification started. loginName='{0}'; title='{1}'; url='{2}'",
  4.     loginName, title, url);
  5.  
  6.   TaskNotifierServiceClient sc = TaskNotifierProxy;
  7.  
  8.   sc.TaskCreated(new TaskNotification
  9.   {
  10.     LoginName = loginName,
  11.     Title = title,
  12.     Url = url
  13.   });
  14.  
  15.   Debug.Print("SPTaskNotifier.SendTaskNotification finished.");
  16. }

Since we call the TaskNotifierService, we should add a Service reference to this project as well. The WCF client proxy is configured dynamically from code.

  1. private static TaskNotifierServiceClient TaskNotifierProxy
  2. {
  3.   get
  4.   {
  5.     {
  6.       var binding = new BasicHttpBinding
  7.       {
  8.         Name = "taskNotifierBinding",
  9.         HostNameComparisonMode = HostNameComparisonMode.StrongWildcard,
  10.         MessageEncoding = WSMessageEncoding.Text,
  11.         UseDefaultWebProxy = true,
  12.         AllowCookies = false,
  13.         BypassProxyOnLocal = false,
  14.         Security =
  15.         {
  16.           Mode = BasicHttpSecurityMode.TransportCredentialOnly,
  17.           Transport =
  18.           {
  19.             ClientCredentialType = HttpClientCredentialType.None,
  20.             ProxyCredentialType = HttpProxyCredentialType.None
  21.           },
  22.           Message =
  23.           {
  24.             ClientCredentialType = BasicHttpMessageCredentialType.UserName
  25.           }
  26.         }
  27.       };
  28.  
  29.       // TODO update the URL to match your current configuration
  30.       EndpointAddress remoteAddress = new EndpointAddress("http://localhost:57800/TaskNotifierService.svc&quot;);
  31.  
  32.       TaskNotifierServiceClient client = new TaskNotifierServiceClient(binding, remoteAddress);
  33.       client.ClientCredentials.Windows.ClientCredential = CredentialCache.DefaultNetworkCredentials;
  34.  
  35.       return client;
  36.     }
  37.   }
  38. }

The SharePoint project includes not only the event receiver, but two JavaScript files as well. We reference the first one, the TaskNotifierLoader.js from a CustomAction element.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.   <CustomAction Location="ScriptLink" ScriptSrc="/_layouts/SPTaskNotifier/TaskNotifierLoader.js" Sequence="900"/>
  4. </Elements>

TaskNotifierLoader.js support dynamic loading of the necessary jQuery and SignalR libraries as well as our other .js file, the TaskNotifier.js.

  1. // TODO update the URL to match your current configuration
  2. var hostURLSignalR = 'http://localhost:57800/&#039;;
  3.  
  4. // Script references.
  5. // Reference the jQuery library.
  6. document.write('<script type="text/javascript" src="' + hostURLSignalR + 'Scripts/jquery-1.6.4.min.js"></script>');
  7. // Reference the SignalR library.
  8. document.write('<script type="text/javascript" src="' + hostURLSignalR + 'Scripts/jquery.signalR-2.0.2.min.js"></script>');
  9. // Reference the autogenerated SignalR hub script.
  10. document.write('<script type="text/javascript" src="' + hostURLSignalR + 'signalr/hubs"></script>');
  11.  
  12. // load our custom script as well
  13. document.write('<script type="text/javascript" src="/_layouts/SPTaskNotifier/TaskNotifier.js"></script>');

TaskNotifier.js performs the same SignalR hub-related tasks as we saw at the default.htm of the  TaskNotifierTestWeb project above, but in this case we display the notification as a standard SharePoint status message.

  1. $(function () {
  2.     $.connection.hub.url = hostURLSignalR + "/signalr";
  3.     // Declare a proxy to reference the hub.
  4.     var taskNotifier = $.connection.taskNotifierHub;
  5.     // Create a function that the hub can call to broadcast messages.
  6.     taskNotifier.client.addTaskMessage = function (title, url) {
  7.         var anchor = $('<a />').attr('href', url).attr('target', '_blank').text(title);
  8.         var anchorText = $('<span />').append(anchor).html();
  9.         var text = "A task called '" + anchorText + "' was assigned to you.";
  10.         var statusId = SP.UI.Status.addStatus("New task:", text);
  11.         makeStatusClosable(statusId);
  12.     };
  13.     // Start the connection.
  14.     $.connection.hub.start();
  15. });
  16.  
  17. function makeStatusClosable(statusId) {
  18.     var status = $('#' + statusId);
  19.     $(status).html('<a title="Close status message" href="" onclick="javascript:SP.UI.Status.removeStatus(\'' + statusId + '\');javascript:return false;">x</a>&nbsp;' + $(status).html());
  20. }

Testing the Solution

Be sure you start the TaskNotifierService project first. As I mentioned, it is the core, without that the other components won’t work.

Next, you can start the UI components, that means opening either SharePoint or the test web project in the browser. If you start the UI first, the client can’t register itself at the SignalR hub so it won’t receive any notifications later.

You can test the SignalR hub and the web UI (either the test project or the SharePoint solution) using the WCF Test Client.

image

You can select the TaskCreated method, set the parameter values you would like to pass to the method, and invoke the method.

A few important things to note regarding this tool:

You should use a double backslash to separate the domain and user names in the LoginName parameter, as there is a bug in the WCF Test Client.

You might get an error message when you start the WCF Test Client:

Error: Cannot obtain Metadata

In this case you should read the full error message as it may have a rather trivial solution like the lack of free memory in this case:

Memory gates checking failed because the free memory (xxxxxxxx bytes) is less than 5% of total memory. As a result, the service will not be available for incoming requests. To resolve this, either reduce the load on the machine or adjust the value of minFreeMemoryPercentageToActivateService on the serviceHostingEnvironment config element.

Here is a sample output of calling the TaskCreated method from the WCF Test Client with the parameters displayed on the screenshot above using the test web site (see TaskNotifierTestWeb project):

image

And the result of the same call in SharePoint:

image

Alternatively, you can test the functionality from the TaskNotifierTestConsole project as well. The following screenshots display the result of calling the TaskCreated method from the TaskNotifierTestConsole project in the case of the test web project:

image

And in the case of the SharePoint solution:

image

Note: The user can close the SharePoint status messages as described in my former post.

Of course, the main goal of this post and the sample is to illustrate, how to use SignalR 2.0 from SharePoint, so let’s see how it works. In this case, I suggest you to start two separate browser tabs, one for the Task list and an other one with an arbitrary page on the same SharePoint site. The reason of this is that when you create a new task, the page is reloaded and you might miss the notification. You should receive the notification in the other browser window.

So let’s create a new task and assign it to the current user.

image

A new status message will be displayed in the other browser tab.

image

Clicking on the hyperlink will open the display form of the task just created.

image

Hopefully you can apply the techniques illustrated here to add real-time functionality easily to your SharePoint applications via the SignalR libraries.

You can download the source code of the sample application from here.

March 23, 2014

HTTP Error: ‘401–Unauthorized’ When Accessing Exchange Web Services via PowerShell 2.0

Filed under: Exchange, PowerShell — Tags: , — Peter Holpar @ 20:25

Last week I had to create a tool to automate the synchronization of an Exchange 2010 folder with a SharePoint 2010 list. Formerly I had some experience with Exchange Web Services and its Managed API, and downloaded the code samples for Exchange 2013 to re-use a few classes of the examples. As my developer account had no access to Exchange, I used explicit credentials of my standard account via the NetworkCredential class in the test (user name hardcoded, password read from the command line). The C# code in Visual Studio 2012 of the proof-of-concept application was running without error, but I thought it’s a good idea to migrate the code to PowerShell, as if the requirements happened to change (that is rather likely in this case), it would be easier to modify the functionality without a recompilation. This idea cost me a few hours debugging later.

I found a few samples on the web, like this one, that illustrate the basic steps in PowerShell. I used the EWS Managed API version 2.1, and PowerShell 2.0, that support .NET CLR version 2 (handy when we want to use the SharePoint 2010 server side object library in the same process). Rewriting the code in PowerShell seemed a trivial task, however when I started the tool I got a 401 Unauthorized error on the first command that tried to access an Exchange resource. I double checked the user name in the code, but it was OK. I found no significant difference when comparing the network traffic generated by the .NET version vs. the PowerShell version up to the point of the error message. I altered the code to use the three-string-parameter constructor of the NetworkCredential class (user name, password, domain name) instead of the two-string-parameter constructor version (domainname\username, password), as I had previously issues from using the two-string-parameter version. But the error remained, so I altered the code back. When I logged in with my other user, that has access to Exchange, and used the default credentials (service.UseDefaultCredentials = $true) the PowerShell code gave no error more.

I read the password from the console using the Read-Host Cmdlet as described here (to tell the truth I simply copied the code and did not check how it works and what that might cause):

$password = Read-Host -assecurestring "Please enter your password"

I assumed that there might be a problem with the password I typed in, so decided to echo back it via Write-Host $password. To my great surprise instead of my password (or a mistyped version of that) the result was: “System.Net.NetworkCredential”. Then I realized the switch assecurestring used with Read-Host, that means not only that the text typed in will be replaced with asterisks, but also that it will be represented internally as SecureString, and not as String. After reading the password without assecurestring, the password was stored as string, but the error remained until I changed the NetworkCredential constructor to the three-string-parameter version again. So as usually, the mystic error was a result of a dual mistake, using SecureString instead of String, and using the wrong version of the NetworkCredential constructor.

The sample application from Microsoft (mentioned above) are working flawlessly with a NetworkCredential constructor that accepts a String for domainname\username, and a SecureString parameter for password, but these samples using the .NET version 4.0. After changing the .NET version of the project to 3.5 in Visual Studio, I had the same issues, but at least the IDE gave me a compilation error because of using a wrong constructor parameter. “Thanks” to the flexibility of PowerShell, it did not warned me because of the SecureString type (I assume it used the “System.Net.NetworkCredential” string as password) and did its best to perform the authentication and failed first there.

March 22, 2014

Where is the Content of my Program Files Folder?!

Filed under: Bugs, Windows — Tags: , — Peter Holpar @ 21:53

Recently I worked with Visual Studio 2012 and was to add a reference to a SharePoint 2010 assembly. Previously I added a link called 14 to the Favorites that points to the 14 hive of SharePoint 2010 (see its properties below). My goal with this link was to help the easy navigation when browsing files to open them from various applications.

image

I wanted to browse for the assembly in a subfolder of the 14 hive to reference it in my SharePoint project. When clicking on the Browse button in Visual Studio the following dialog was displayed:

image

I selected the link 14 on the left side under the Favorites, and was navigated to the folder displayed below. Hmmm… I expected something else, where are my folders, like ISAPI or Logs?

image

Highlighting the location gave me the answer. It was the same location as I wish, but instead of the Program Files folder it was under Program Files (x86).

image

I double-checked the link once again, but it referred to the right location.

First I thought that it is a bug in Visual Studio, but after a short test I discovered that the problem affects all of the 32-bit applications, including Office (I have the 32-bit version of that), so I assume it should be a bug in Windows itself. I tested it with Windows 2008 R2 and Windows 7, and was able to reproduce the same issue.

If we create a folder (let’s call it Test – assuming a folder having the same name does not exist in the Program Files (x86) folder) in the Program Files folder, and add it to the Favorites, then try to open it using a 32-bit application we get a warning that states that “Location is not available”:

image

The ‘Date modified’ of my .eml Files got Changed When I First Select The File. Why?

Filed under: Alternate Data Streams, Mails, NTFS, Windows — Tags: , , , — Peter Holpar @ 20:44

In my last post I wrote about a rather odd behavior one can experience when working with e-mail files (.eml), namely, that if you select the file the first time in Windows Explorer (either by a mouse click or by navigation via the keyboard) its ‘Date modified’ gets updated with the current date. In this short post I plan to show you the reason behind and a few nice-to-knows around that.

As far as I understood, it’s all about the Alternate Data Streams (ADS) feature of the NTFS file system. As you might know besides the main data stream of a file, NTFS supports these alternate streams, for example, to store extra properties of the file, that are intended to be edited automatically by the system or via alternate editors, but not editable by altering the binary data of the file itself.

You can access these streams easily via PowerShell 3.0, but if you have no PS 3 yet, no problem, you can use standard Windows Command prompt tools as well. There are other tools to manipulate the stream as well, like streams from Sysinternals.

The .eml files may have an alternate stream of type Outlook Express Properties Stream which looks like sample.eml:OECustomProperty:$DATA. However, this stream is not automatically created as you save the .eml file, just after selecting one that does not have yet any, like selecting it the first time. To tell the truth, I don’t see the design decision behind this behavior, it smells rather a bug to me. As the new data stream gets created, the last modified date of the file is updated as well.

For example, if you list the .eml files with their data streams using the “dir /r” you might get something like this:

image

As you see, most of the files (the ones I’ve already selected in Windows Explorer) already have the OECustomProperty:$DATA stream, although a couple of them (like c5a6593e-806a-43dc-8509-bd4072a26f6b.eml and c6b834a1-b1d8-4148-a9df-0c32b75fd206.eml) haven’t yet any. If you select those latter files in Windows Explorer, their alternate stream gets created and the last modified date will be updated.

But what is within OECustomProperty:$DATA stream?

You can display it, for example from Command Prompt:

more < youremlfile.eml:OECustomProperty:$DATA

You will see several lines of binary information, but should be able to recognize the subject of your mail. If you check the properties of your file, you will see the same information on the Details tab, in the Name property.

image

If you open the .eml file, change the subject of the mail, and save the file, then check the properties again, you will see, that the Name property has not been changed. It is just another evidence that this information is stored in the alternate stream. If we delete the stream (for example using stream.exe –d or via PowerShell 3.0 cmdlet Remove-Item –Stream), and select the file again in Windows Explorer, the stream will be regenerated, and thus the ‘Date modified’ will be set to the current time and the Name property will be refreshed according to the new subject of the mail.

Although you could disable this behavior by inactivating the property handler for the .eml files (see here how to achieve that by deleting the corresponding registry key), I don’t think it is a good idea as it might have other side effects as well.

Older Posts »

The Shocking Blue Green Theme. Create a free website or blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 50 other followers