Cognos Prompt API

Well, it’s been a week since I’ve returned from the IOD, and I’m almost fully recovered from the jet-lag. I had an absolute blast, and am looking forward to going back next year.

I promised more details on the various booths that I went to, and I do have the various papers that they gave me. I will eventually get to them! But before I do, this is what I started writing while I was at the IOD, but only now am I getting around to actually publishing it.

One of the most exciting new features in Cognos 10.2 is the introduction of a standardized prompt api. No longer will developers have to scramble to try to figure out how to get validate prompts at run time. Or how to repopulate them as needed through other interactions.

IBM has released a standard set of functions, and has even released a few samples containing them. The goal in this post is to learn how to use the new API and maybe to redo a few challenges I’ve had previously.

Just as previous version required a bit of JavaScript to prepare your page for playing with the prompt objects, this version is no different. What is different, is that instead of having to memorize the entire fW formWarpRequest nonsense, now it’s a much simpler:

var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

The acme namespace is to prevent function conflicts with existing Cognos functions, just preface all functions with that and you’ll be good. The “_THIS_” will automatically be replaced with the correct Cognos Namespace, so your JavaScript should still work if you’re running the report from Report Studio or the Cognos Connection. This never ever needs to change, so just copy and paste the above (although employees of Acme Inc may want to change the fictious namespace). This solution was shamelessly ripped from Rick Blackwell’s wonderful session at the IOD. All credit for anything and everything related to Cognos and JavaScript should obviously go to him now.

To begin with, let’s use Rick’s standard example for Postal Code validation.

 	/*
	 *
	 * This function will associate a validation function with the prompt control called promptPostalCode
	 *
	 */
	asdf.assignValidator = function ( ) {
		// Create the report object
		oCR = cognos.Report.getReport( "_THIS_" );

		// Create an array of prompt controls
		// This object is used in both functions in this script.
		asdf.promptControlPostalCode = oCR.prompt.getControlByName("promptPostalCode");

		// Make sure we have a valid Postal Code in the format "ana nan" such as K3F 5F4
		asdf.promptControlPostalCode.setValidator(asdf.postalCodeValidator);

	};

	/*
	 *
	 * This function is called for each character entered by the user.
	 * Ensure the value is a valid Postal Code (ie. A1A 1A1 with spaces) using RegEx.
	 *
	 */
	asdf.postalCodeValidator = function(promptValue) {
		//promptValue is an array of 1 value

		// If a value has been entered by the user ensure it matches the pattern
		if (promptValue.length > 0) {
			var rePostalCodeFormat = new RegExp("[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
			if ( rePostalCodeFormat.test(promptValue[0].use) ) {
				return true;
			} else {
			    return false;
			}
		} else {
		 	// If the prompt contains no value it is valid.
			// This is important as the prompt and filter are optional
			return true;
		}
	};

His example is a bit complicated, but it’s good to demonstrate the capabilities.

The first function finds the promptPostalCode prompt, and assigns the validator. Now when a user enters a value, it will trigger the validator function, passing the entered value.
The second function will take the entered value compare it against the regex, and return a true/false, telling Cognos whether the finish button should be active, and if there should be a red underline in the prompt object.

Of course, this is not the only thing that can be done with the validator function. The validator gives us an easy to use onchange function for the prompts.

A few of my clients want tree prompts which automatically hides after a user has selects a member. Previously, there were a few was this could be accomplished, each of them more difficult than the last. But now, instead of using the JS to validate, I can use the validate function to hide the prompt after a few seconds.

First I create an HTML item containing span with an ID:

<span id="treeLabel" onclick="acme.toggleTree()">Tree Prompt</span>

Next, I wrap the tree prompt inside a span with boxtype set to none:

<span id="treePrompt" style="display:none">

</span>

First a function to directly toggle the display of the tree prompt:

acme.toggleTree = function()
{
document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none';
}

Next, the actual validation function:

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

Notice that it’s not actually doing any validation; it’s simply returning true. Notice that it’s also sending the label of the selected node to the tree label. Ultimately the new Prompt API makes it significantly easier to build complex reports.

Example XML:

<report xmlns="http://developer.cognos.com/schemas/report/9.0/" useStyleVersion="10" expressionLocale="en-us">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Cubes']/package[@name='Sales and Marketing (cube)']/model[@name='2008-07-25T15:28:38.072Z']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1">
								<style>
									<defaultStyles>
										<defaultStyle refStyle="pg"/>
									</defaultStyles>
								</style>
								<pageBody>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pb"/>
										</defaultStyles>
									</style>
									<contents><textBox parameter="Parameter1" name="textBox" multiSelect="true"/><HTMLItem description="Label">
			<dataSource>
				<staticValue>&lt;span id="treeLabel" onclick="acme.toggleTree()"&gt;Tree Prompt&lt;/span&gt;
&lt;span id="treePrompt" style="display:none"&gt;</staticValue>
			</dataSource>
		</HTMLItem><selectWithTree parameter="Time" refQuery="Time" name="Time"><selectWithTreeItem refDataItem="Time"/></selectWithTree><HTMLItem description="scripts">
			<dataSource>
				<staticValue>&lt;/span&gt;

&lt;script&gt;

/*
 * Fictious Namespace and getter. Don't change these unless IBM says so or you know what you're doing.
 * acme is now the official standard fictious namespace (thanks Rick), so make sure all functions sit there.
 * This is based on the samples, so I'm giving all credit to Rick Blackwell of IBM.
 */
var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

/*
 * Global Params
 * Paul Mendelson 2012-28-10
 * These control the behaviour of the functions below.
 */

//Time in miliseconds to wait before prompt disappears.
acme.hideTimer = 1000;

/*
 *  This will return any available methods or functions or whatever available on the referenced object.
 */
function getMethods(myObject)
{
  var funcs=[]
  for(var name in myObject)
  {
    funcs.push(name)
  }
  return  funcs.join(', ');
}

/*
 * Hide the prompt when validating
 */

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

acme.toggleTree = function()
{
  document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none'
}

/*acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( "[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
*/
acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( '^[0-9]{0,3}$', "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
acme.getControl('textBox').setValidator(acme.checkPostalCode);
acme.getControl('Time').setValidator(acme.hideTree);
&lt;/script&gt;

</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="false" output="no"/><XMLAttribute name="listSeparator" value="," output="no"/><XMLAttribute name="RS_modelModificationTime" value="2008-07-25T15:28:38.133Z" output="no"/></XMLAttributes><queries><query name="Time"><source><model/></source><selection><dataItem name="Time" aggregate="none"><expression>rootMembers([sales_and_marketing].[Time].[Time])</expression></dataItem></selection></query></queries><reportName>Paul - Tree prompt</reportName></report>

Information Security in Cognos Reports

Cognos has an extremely intricate security system. It is well suited for secure reports and confidential information. Some clients expose portions of their platform to the public, safe in the knowledge that users can only see what they’re allowed to see. However, all of this security can be for nothing through careless report design.

When building any secure reports, it is essential that security be handled in the back end. There have been countless reports built with JavaScript enforced security built on the prompt page, or with security handled with conditional blocks and parameters. Token prompts, while perfect for building dynamic reports, can create holes in which malicious users can attempt to retrieve data that would normally be hidden to them.

This document is not intended to be a guide to hacking Cognos reports. Indeed, many reports would not even require such drastic oversight. Proper usage of macros and filters will solve a majority of the potential problems. Nor does every report need such stringent security. Many times filters are simply used as a means of speeding up the report, or allowing users to narrow their focus.

In this post I will go over possible security flaws with the token prompt.

Consider the following request: A report which shows Employee, Manager, a prompt to choose product line, product type or name. This report will be available to all users to show sales ranking throughout the company.

In order to fulfill the prompt requirement, a token prompt is created with the following static values:

While the data item in the report would be a simple:
#prompt(‘Field’,’token’)#

When running the report, the user might see the following output:

But a malicious user may attempt to see each employee’s full address.

Nothing in the report was changed, and yet the user can now see confidential information. How was the user able to accomplish this?

Simply by changing the value option in the prompt with IE Developer Tools (each of the major browsers has a similar tool), the user can choose to see any item in the model. “But,” I hear you say, “I can set object level security inside Framework to deny that user group, I’ll even set it on the data layer!” That will certainly work well to prevent users from selecting items from the model, but there is still a work around.

Entering {“EMP_EMPLOYEE_DIM”.”ADDRESS1″ || ‘, ‘ || nvl(“EMP_EMPLOYEE_DIM”.”ADDRESS1″,’ ‘) || ‘, ‘ || “EMP_EMPLOYEE_DIM”.”PROV_STATE” || ‘, ‘ || “EMP_EMPLOYEE_DIM”.”POSTAL_ZONE”} into the value will work just as well, and because it doesn’t reference the data item path, Cognos won’t throw an error. The curly brackets there tell Cognos to execute the code as is. Note that this SQL doesn’t include the country. Cognos won’t attempt to build a join based on custom SQL like this, so including “Branch_region_dimension”.”COUNTRY_EN” would only result in an error.

Since adding custom SQL, like that concatenated string, works, would subqueries also work? In this example any attempt to use a subquery in the data item list would fail, as (at least in Oracle) subqueries are not supported in the Group By. But look what might happen when we remove the two measures from the list:

By adding {(SELECT rtrim (xmlagg (xmlelement (e, owner||’.’||table_name || ‘,’)).extract (‘//text()’), ‘,’) table_names FROM all_tables)} we can now see every single table available. Similar queries can be run to see the fields in each table. At this point the user has full read access to the database, limited entirely by whatever security is applied to the data source user.

The exploits are only possible because the data item contained only the token prompt. I’m not saying that token prompts should never be used; many of my reports are based on the functionality. Instead, use them wisely.

One workaround that I have found is a combination of a new table and a lookup. Build a new table called “TokenExpressions” with the fields “Key”, “Expression”, “Name”, “Report”, and “ReportGroup”. Import that into your framework. Create a parameter map (tokenLookup) based on that table with the TokenExpressions.Report+’-‘+TokenExpressions.Key as Key and TokenExpressions.Expression as Value. In the report, set up your value prompt with the source query as TokenExpressions.Key as Use and TokenExpressions.Name as Display. And finally, in the report queries, use the expression #’ReportName-‘+sq($tokenLookup{prompt(‘Field’,’integer’))}#. In addition to securing the report against unexpected inputs, you also make it significantly easier to add or remove options in the report.

The lesson learned is that using prompts as a security measure is inherently risky. Malicious users with access to web development tools (IE has them by default) may override the existing values and see data that isn’t meant for their eyes.

Finally, it is worth mentioning that using the curly brackets for subqueries can have legitimate uses. I have not found any other way to handle correlated subqueries, for instance. Oracle analytic functions can usually only be accessed through the curly brackets.

Quickie: Using JavaScript with a range prompt

You may occasionally need to reference the various prompts with JavaScript. IBM has a very nice document detailing how to access the various prompt components, unfortunately it seems to be missing a few options. Today I needed to access the “from” and “to” selects on a range prompt.

This works with a single select, range value prompt. I have not tested against other variations.

Assuming the prompt name is _test, the following will alert the selected value and caption:

<script>
var fW = (typeof getFormWarpRequest == "function" ? getFormWarpRequest() : document.forms["formWarpRequest"]);
if ( !fW || fW == undefined)
   { fW = ( formWarpRequest_THIS_ ? formWarpRequest_THIS_ : formWarpRequest_NS_ );}

fW._oLstChoicesrange_to_test.onchange = new Function ("alert(this.value);alert(fW._oLstChoicesrange_to_test[fW._oLstChoicesrange_to_test.selectedIndex].text)");
fW._oLstChoicesrange_from_test.onchange = new Function ("alert(this.value);alert(fW._oLstChoicesrange_from_test[fW._oLstChoicesrange_from_test.selectedIndex].text)");
</script>

This was tested on Cognos 10.1.1. I suspect it will work on 8.4 and 8.3.

report xml:

<report xmlns="http://developer.cognos.com/schemas/report/8.0/" useStyleVersion="10" expressionLocale="de">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Models']/package[@name='Go Sales Paul Rewrite']/model[@name='model']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1">
								<style>
									<defaultStyles>
										<defaultStyle refStyle="pg"/>
									</defaultStyles>
								</style>
								<pageBody>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pb"/>
										</defaultStyles>
									</style>
									<contents><selectValue parameter="Parameter1" required="false" range="true" name="_test"><selectOptions><selectOption useValue="1"><displayValue>a</displayValue></selectOption><selectOption useValue="2"><displayValue>b</displayValue></selectOption><selectOption useValue="3"/><selectOption useValue="4"/><selectOption useValue="5"/></selectOptions></selectValue><HTMLItem>
			<dataSource>
				<staticValue>&lt;script&gt;
var fW = (typeof getFormWarpRequest == "function" ? getFormWarpRequest() : document.forms["formWarpRequest"]);
if ( !fW || fW == undefined)
   { fW = ( formWarpRequest_THIS_ ? formWarpRequest_THIS_ : formWarpRequest_NS_ );}

fW._oLstChoicesrange_to_test.onchange = new Function ("alert(this.value);alert(fW._oLstChoicesrange_to_test[fW._oLstChoicesrange_to_test.selectedIndex].text)");
fW._oLstChoicesrange_from_test.onchange = new Function ("alert(this.value);alert(fW._oLstChoicesrange_from_test[fW._oLstChoicesrange_from_test.selectedIndex].text)");
&lt;/script&gt;</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="true" output="no"/><XMLAttribute name="listSeparator" value=";" output="no"/><XMLAttribute name="RS_modelModificationTime" value="2012-06-27T15:29:14.445Z" output="no"/></XMLAttributes></report>