Webdrawer - playing audio files

Webdrawer does not support the playing of audio files, instead you will get an error message when you preview a Record with an audio file attached.  This can be remedied by adding some HTML to the preview template. To do this, in your Webdrawer install folder:

Open the file '\Views\WDRecordPreview.cshtml'  in a text editor

Add the following HTML

if (new string[] { "MP3", "M4A", "WAV", "OGG" }.Any(ext => ext == record.Extension.Value))
{
    <audio preload="auto" autobuffer controls>
        <source src="~/Record/@record.Uri/file/document?inline=true" />
        <p>Audio playback not supported in this browser.</p>
    </audio>
}
else

Your HTML should end up looking like this:

audio.PNG

Google Docs add-on

Content Manager currently has no integration with Google Docs.  This sample explores what is possible within the constraints of the Google Docs add-on ecosystem.  It turns our quite a lot is possible, much more than with Gmail.

The Sample

The sample below demonstrates registering and updating a document from Google Docs to Content Manager.  The code for this sample is available on the community repo.

The future

If you have any thoughts on the importance of Google Docs support or the details of what a Google Docs integration should do please contact me.

Gmail addon

Today if you want to file email from Gmail to Content Manager you need to use the Email Link module.  The limitation of this is that there is not user interface so you are unable to fill in the Record data entry form.

Gmail add-ons

Gmail supports add-ons making it theoretically possible to create a client side solution which will allow the user to complete the data entry form at the time they file the email.  These add-ons are constrained in that they have to be able to function both in the Gmail web UI and also the mobile Gmail app.

Exploring the possibilities

The sample in this video demonstrates the possibilities and the limitations of a Gmail add-on.

The future

Given the current limitations of the UI components it is difficult to see how we could build a generic add-on.  Any customer or partner who wants to build a custom add-on is welcome to start with this sample, as always feel free to contact me if you either have suggestions for an 'off the shelf' Content Manager add-on or want to discuss the building of a bespoke solution.

ServiceAPI Bulk Loader

The ServiceAPI exposes a bulk loading capabilities for which a C# sample is provided in the help.  This blog post shows the actual JSON that is sent over the wire.

The first step is to create an Origin object in the client, once this is done a batch of Records can be posted to the bulk loader.  The sample below:

  • uses the Origin with Uri 9000000006,
  • does not use background processing (processing is completed within the HTTP post request),
  • creates one Location and one Record,
  • uses the Location created for the Record assignee,
  • assumes that the file "test doc.docx" has already been uploaded (using the UploadFile service), and
  • should be posted to http://MyServer/ServiceAPI/BulkLoader.
{
    "Origin": {
        "Uri":9000000006
    },
    "AutoCommitLocations": true,
    "SQLCheckConstraints": false,
    "SQLServerTableLock": true,
    "UseBulkLoaderRecordNumbering": true,
    "ProcessInBackground":true,
    "LocationsToImport":[{
        "LocationSurname":"Jones", 
        "LocationGivenNames":"Arthur",
        "LocationTypeOfLocation":"Person"
    }],
    "RecordsToImport": [
        {
            "RecordTitle": "My title from BL 5.1 AAAAA",
            "RecordAssignee":{
                "LocationSurname":"Jones", 
                "LocationGivenNames":"Arthur"
            },
            "RecordFilePath":"test doc.docx"
        }
    ]
}

Background processing

If 'ProcessInBackground' is set to true then the ServiceAPI will hand the bulk loading request off to the TRIMServiceAPIBulkLoader service (installed as a windows service).  In this case the bulk loading request will immediately return an OriginHistory object.  The Uri from this object can be used to poll the OriginHistory object to find out when the bulk loading operation has completed.  The Url to fetch the OriginHistory will look like this:

http://localhost/ServiceAPI/OriginHistory?q=uri:9000000019&properties=BulkLoaderIsRunning,OriginHistoryRecordsCreated

Demo

In the video below I demonstrate bulk loading one Record using background processing.

Generate Outlook linked folders

Background

Prior to CM 9.0 the Outlook integration had a feature to export/import linked folders, at that time linked folders were stored in the Windows registry so the import/export was required any time a user received a new machine.  This feature was also of use to those who wished to share their linked folders with their friends.

In 9.0 the architecture of the Outlook integration changed so that linked folders were stored in Checkin Style objects (in Content Manager) not in the registry, which seemed to make the import/export unnecessary, except for that sharing with friends usage.

A partial solution

A partial solution to sharing linked folders is to create Checkin Styles which have a group as the owner, this means that every member of that group will see that Checkin Style.  The gap is that a linked folder will not be auto-created for that Checkin Style.

Some sample code

I wrote a sample application that could be the basis for a utility to allow users to create linked folders from Checkin Styles that have been created for them.  This might useful in the case where the user has multiple Checkin Styles and does not wish to go through one by one to create a new linked folder for each one.  Below is a screen shot from this sample application.

linkkedfolders.PNG

The future

Clearly if the sharing of linked folders is a popular activity then it makes sense to bring it back in some form.  If you want to have input on how that should be implement lodge a support request or send me a private message on the forums.

Azure AD for Native Client

For those who wish to use Azure AD to authenticate with the native Content Manager client here are the steps.

In Azure AD create a native application, the Redirect URI must be urn:ietf:wg:oauth:2.0:oob

create native.PNG

In App Registrations select Endpoints and take note of the following two endpoints for later:

  • OAuth 2.0 Token Endpoint, and
  • OAuth 2.0 Authorization Endpoint
endpoints.PNG

In CM Enterprise Studio select your database and from the context menu choose Authentication, then go to the ADFS / Azure tab. In this tab set:

  • Authorize Endpoint URL to OAuth 2.0 Authorization Endpoint
  • Token Endpoint URL to OAuth 2.0 Token Endpoint
  • Client Id to the Application ID (in the Azure AD application you created)
  • Relying Party Trust also set to the Application ID

If you press Test Authenticate you should be able to authenticate as one of the users in Azure AD.

Authenticate the Web Client with Azure AD

I have a few posts demonstrating on-premise ADFS with the CM Web Client but so far nothing with Azure Active Directory, today I rectify that.

The Video

Steps

The steps to configure the web client to use Azure AD are:

  • configure the site to use anonymous in IIS Admin,
  • create the Azure Ad App Registration, and
  • edit the Web Client web.config.

Sample Config

configSections

<section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral,PublicKeyToken=B77A5C561934E089" />
<section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />

appSettings

<add key="ida:FederationMetadataLocation" value="https://login.windows.net/[Your Tenant ID]/FederationMetadata/2007-06/FederationMetadata.xml" />

authorization and authentication

<authorization>
  <deny users="?" />
</authorization>
<authentication mode="None" />

system.identityModel

<system.identityModel>
<identityConfiguration>
  <audienceUris>
    <add value="[APP ID URI]" />
  </audienceUris>
  <securityTokenHandlers>
    <add type="System.IdentityModel.Services.Tokens.MachineKeySessionSecurityTokenHandler, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    <remove type="System.IdentityModel.Tokens.SessionSecurityTokenHandler, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </securityTokenHandlers>
  <certificateValidation certificateValidationMode="None" />
  <issuerNameRegistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry">
    <authority name="https://sts.windows.net/[Tenant ID]/">   
      <validIssuers>
        <add name="https://sts.windows.net/[Tenant ID]/" />
      </validIssuers>
    </authority>
  </issuerNameRegistry>
</identityConfiguration>
</system.identityModel>
<system.identityModel.services>
<federationConfiguration>
  <cookieHandler requireSsl="true" />
  <wsFederation passiveRedirectEnabled="true" issuer="https://login.windows.net/[Domain]/wsfed" realm="[APP ID URI]" requireHttps="true" />
</federationConfiguration>
</system.identityModel.services>

Use Powershell to import a folder of files

There are a variety of ways to import files to Content Manager.  If you want granular control you may choose to write some code.  This Powershell script uses a CheckinStyle to import all EML files from a folder.

Add-Type -Path "c:\[CM Binary Path]\HP.HPTRIM.SDK.dll"
$database = New-Object HP.HPTRIM.SDK.Database
$database.Id = "L1"
$database.WorkgroupServerName = "local"
$database.Connect()

$checkinStyle = New-Object HP.HPTRIM.SDK.CheckinStyle($database, "test sec");

$files = [System.IO.Directory]::GetFiles("c:\\junk\\testimport", "*.eml", [System.IO.SearchOption]::TopDirectoryOnly);

foreach ($file in $files)
{
Try
{
    $inputDoc = New-Object HP.HPTRIM.SDK.InputDocument($file);
    $rec = $checkinStyle.SetupNewRecord($inputDoc)
    $rec.Save()

    [System.IO.File]::Move($file, [System.IO.Path]::Combine("c:\\junk\\imported\\", [System.IO.Path]::GetFileName($file)))

    Write-Host "Imported: " $rec.Title + " / " + $rec.Uri
    }
    Catch 
    {
        $ErrorMessage = $_.Exception.Message
        Write-Host "Error: " $ErrorMessage + " / " + $file

    }
}

$database.Dispose()

To use this script:

  • set the location of your Content Manager binaries in the first line,
  • use your database Id in the $database,
  • if your work-group server is not on the local machine set the WorkGroupServerName,
  • replace the name in the Checkin Style  Style constructor,
  • if you want to import files other than EML change the '*.eml' to something else,
  • set the source folder name, also the destination folder in the Move method (ensure this destination folder exists), then
  • run the script from Powershell.

Stream a document in the .Net SDK

The standard methods of getting a document from in the SDK (e.g. Record.GetDocument()) fetch the entire document from the document store before giving you access to the document.  DownloadNotifier allows you to:

  • not write a file to the file system but to a stream,
  • fetch the document in chunks rather than waiting for the entire document, and
  • start downloading part way through a file.

I just updated the SDK docs to include some information on using it, also, here is a video if you want to watch me use it.

Checkin Style and Email deletion

This is a note to clarify what happens to emails processed by CM EmailLink.

Content Manager 9.1

 In CM 9.1 there are two check boxes which impact what happens to the email after it has been filed in CM.

In CM 9.1 there are two check boxes which impact what happens to the email after it has been filed in CM.

Content Manager 9.2

 In CM 9.2 there is one combo-box to choose what happens to the email after filing.

In CM 9.2 there is one combo-box to choose what happens to the email after filing.

Mapping 9.1 to 9.2

The check boxes uses in 9.1 map to the (I believe) simpler UI in 9.2 like this:

  • Permanent Delete - no check boxes selected.
  • Move to Deleted Items - 'Keep email in the mail system' NOT selected and 'Move to deleted' selected.
  • Retain in Mail System - 'Keep email in the mail system' selected and 'Move to deleted' NOT selected.

What actually happens

After the email has been filed in Content Manager one of three things happens:

  • Permanent delete - the mail is hard deleted and is not available in the deleted items folder
  • Move to deleted items - if a mail prefix is set in System Options it is pre-pended to the subject and the unique Id of the Record is set in a custom property on the email.  Then the email is moved to the Deleted items folder.
  • Retain in Mail System - same as 'Move to deleted items' except the email is not moved to the Deleted Items folder.

Recursive searching in WebDrawer and ServiceAPI

The CM search syntax allows for recursive searching, for example returning results from folders contained within folders.  The operator that specifies recursion is the plus sign (+), the problem is that in a URL the plus sign is reserved to indicate white space.

To overcome this the ServiceAPI (and WebDrawer) use the tilde (~) instead of the plus sign, so instead of this:

container:[title:asia]+

Do this:

container:[title:asia]~

If you would rather use a character (or sequence of characters) other than the tilde you can customise this in the hptrim.config.  The Web Client, for example, uses _$_ instead of tilde, which is set in the  'searching' element in its hprmServiceAPI.config.

<searching pageSize="30" searchRecursiveOption="_$_"/>

The 'All' property set in the ServiceAPI

Overview

In the ServiceAPI there is a concept of a propertySet, this allows for the retrieval of a pre-defined set of properties, for example:

/Record?q=all&propertySets=Grid&format=json

The problem with 'All'

One of the property sets is 'All' which means that the above URL could look like this:

/Record?pageSize=20&q=all&propertySets=All&format=json

Fetching all properties for many object types (such as Classification, SavedSearch, Activity etc) can be a good idea, being simpler than pre-determining which properties you need.  Using 'All' for Records is problematic, two problems being:

  • some Record properties are very expensive so response times will be unnecessarily slow, and
  • some properties will throw an error if the related feature is not enabled (e.g. NextTaskDue requires the Vital Records feature be enabled).

The solution

The solution is not to use 'All'. The alternatives are:

  • use the 'ViewPane' property set instead, which returns a list of all properties that might be interesting to an end user, or
  • construct a custom property set in hptrim.config which can contain all the properties that are of interest to your application.

Creating a child object (Record 'Alt in' Relationship)

The code

To add a new object to one of the child object collections on a TrimMainObject, for example adding a RecordRelationship to a Record, requires sending an array containing one or more objects to the appropriate collection. For Example, posting the code below to the Record endpoint will cause Record 9000000002 to be 'Alternately contained within' Record 9000008050.

{
    "Uri":9000000002, 
    "ChildRelationships":[
    {
        "RecordRelationshipRelatedRecord":9000008050,
        "RecordRelationshipRelationType": "IsAltIn"
    }]
}

And a video...

Using a search query instead of a Uri in a URL

Recently I wanted to link to a Record using the external ID, not the Uri or the number.  This was from a customised WeDrawer page so I could have done a search to find the Uri and then used that, but I didn't want that extra step.  A little reflection (and poking around in the code) and I realised that I could use the external id in the URL after all.

If it was a search

A search by external id would look like this, problem was I want to open the actual record page, not a list of one record.

/WebDrawer/record?q=recExternal:abc123

As a URL

As long as a search returns only one result then you can substitute the query for the Uri in the URL, as long as you can compose the query without using a colon (colons are reserved characters in URLs).  So below I use equals instead of colon to either open the details page or go directly to the document via the external id.

http://localhost/WebDrawer/record/recExternal=abc123
http://localhost/WebDrawer/record/recExternal=abc123/file/document

Note

As is my habit I use the internal name for the search clause (recExternal) not the name (external) to minimise the possibility of breakages due to either custom captions or use of different languages.

SAML signout

Overview

In a previous post I show how to use Component Space to add SAML support to the Web Client, here I add a sign-out button.

Steps

useADFS == true

Set useADFS to true in hprmServiceAPI.config, for example:

<setup databaseId="J1"  searchAhead="false" advancedSearch="false" workpath="C:\HP Records Manager\ServiceAPIWorkpath\Uploads" useADFS="true"/>

Setup signing certificate

If you plan to sign your logout request then you will need a certificate, either encrypt the password or store the certificate in the Windows certificate store as described in this Component Space document.

Add keep-alive

To allow for notification when the user's session has expired add the keep alive loop.  To do this edit _Initialisation.cshtml (or _Shared.cshtml in later versions) and comment out the RMStayALive function, then add this new version:

var RMStayALive2 = function () {
    var makeRequest = function () {
        $.getJSON(HP.HPTRIM.TrimClient.getServiceAPIUrl() + "/Location/me", function (data, status, xhr) {
                
        }).fail(function () { 
            if (confirm("Your session has expired, do you wish to re-authenticate?")) {
                top.location = HPRMWebConfig.virtualDirectory;
            }
            
        });
    }
    setInterval(makeRequest, (60 * 1000));

}();

Video

The process above is shown in this video.