Tellme Studio

Home MyStudio About Tellme
Abstract CGI (IRCGI) API markup

The VoiceXML 2.x Implementation Reports contains assertions that require server-side support to verify HTTP headers and submitted values. To allow disparate test environments to use different server-side technologies, tests should describe the server-side processing using a syntax that is independent from any particular server-side framework. The abstract syntax should easily transformed into a test environment's desired format (e.g. Perl, JSP).

This document describes a server-agnostic XML API that can be transformed easily using XSLT into any particular server-side framework. The API supports the following:

  • Verification of the method used to make the HTTP request (GET, POST, etc).
  • Verification of the presence and value of one or more HTTP headers.
  • Verification of the presence and value of one ore more submitted parameters.
  • Verification of the presence and value of method, headers, and parameters.
  • Returning a specific HTTP response status code.
  • Returning VoiceXML containing the next ".vxml" document to be visited.
  • Sleeping for a specified number of seconds before returning a response.
  • Setting an Expires response header based on a specified delta.

The XML API does not require the following:

  • State management.
  • Indicating test pass or failure.

A document conforming to this specification, heretofore referred to as an "IRCGI document", indicates the checks to be made on an HTTP request. Supported checks include:

  • HTTP method
  • Request headers
  • Parameters (including file uploads)

Checks may be nested. The result of the checks determines the next document to be executed. The next document must continue the test and must determine the success or failure of the test. Once the IRCGI document determines the next document, a response is sent, and the IRCGI document terminates.

An IRCGI document consists of the following elements. The DTD can be found below.

ircgi
The root document element. This element must appear as the root document element for all IRCGI documents.
comment
Contains comments that may be accumulated during CGI execution. The comments may be returned in one or more log elements in the body of the HTTP response. This may be used as a primitive debugging facility.
if-header
Checks for the presence and value of an HTTP request header. Supports the following attributes:
name Required. The name of the header. If only the name attribute is specified, the CGI must execute the if-header content only if the named header is present in the request.
value Optional. The expected value of the header. If this attribute is specified, the CGI must execute the if-header content only if the named header is present in the request and its value matches that of the value attribute. This attribute and the starts-with attribute are mutually exclusive.
starts-with Optional. The expected value with which the header begins. If this attribute is specified, the CGI must execute the if-header content only if the named header is present in the request and its value begins with the value of the starts-with attribute. This attribute and the value attribute are mutually exclusive.
ignore-case Optional. The value match is case-sensitive if the ignore-case attribute is false, the default. The match is not case-sensitive if the attribute is true.
if-method
Checks the HTTP method used to submit request. The CGI must execute the if-method content only if the type attribute's value matches the HTTP method used to submit the request. The match is case-insensitive.
type Required. The HTTP method. Typically 'get' or 'post'.
if-parameter
Checks for the presence and value of an HTTP request parameter. Supports the following attributes:
name Required. The parameter's name. If only the name attribute is specified, the CGI must execute the if-parameter content only if the named parameter is present in the request.
value Optional. The parameter's expected value. If this attribute is specified, the CGI must execute the if-parameter content only if the named parameter is present in the request and its value matches that of the value attribute. This attribute and the starts-with attribute are mutually exclusive.
starts-with Optional. The expected value with which the parameter begins. If this attribute is specified, the CGI must execute the if-parameter content only if the named parameter is present in the request and its value begins with the value of the starts-with attribute. This attribute and the value attribute are mutually exclusive.
ignore-case Optional. The value match is case-sensitive if the ignore-case attribute is false, the default. The match is not case-sensitive if the attribute is true.
if-all-params
Checks for the presence and value of all HTTP request parameters against an expected set. Supports the following attribute:
ref Required. The id of a params element containing the set of expected CGI parameters.
params
Defines a set of expected CGI parameters via one or more contained param elements. The params tag is allowed as a child of the root document element, ircgi.
id Required. A unique identifier referenced by an if-all-params element.
param
Defines the name and value of an expected CGI parameter.
name Required. The name of the CGI request parameter.
value Required. The value of the CGI request parameter.
if-upload
Checks for the presence of a file upload. Supports the following attributes:
name Required. The name of the parameter containing the uploaded bits. If only the name attribute is specified, the CGI must execute the if-upload content only if the named upload is present.
size Optional. The expected size of the upload. If this attribute is specified, the CGI must execute the if-parameter content only if the named upload is present in the request and its size matches that of the value of the size attribute. The value of this attribute can be a static numeric value or a CGI parameter name. If a name is specified, the size of the file upload is compared to the value of the specified CGI parameter. The value of the CGI parameter must resolve to a numeric value. The size value should be specified in bytes.
next
Selects the next .vxml document and, optionally, sets the HTTP response code. The CGI must generate a response upon executing a next element and must not execute any other elements.
code Optional. The HTTP response code. If the code attribute is specified, its value must be used as the HTTP response status code. The default is 200 (Ok).
dest Optional. The URL of the next document. If this attribute is specified, its value must be used by the response to designate the next document to be visited.
include Optional. true or false If this attribute is true, the document specified by the dest attribute is returned as the result document. If this attribute is false, the default, the CGI returns a generated VoiceXML document that contains a goto to the document specified by dest. The document generated when include is false may contain the content of comment elements that were encountered.
Only files that are in the directory immediately above the cgi-bin directory (see Usage below) may be included. Attempts to include other files result in an HTTP 403 (Forbidden) response status code.
sleep Optional. The number of seconds the CGI sleeps before returning its response to the client. Do not append a time specifier to the value.
expires Optional. Sets an Expires HTTP response header to a date calculated by adding the value of the expires attribute to the current time. The value of this attribute should be a positive or negative integer in seconds.

The following examples illustrate the use of the IRCGI API elements. The examples validate the XSLT used to generate valid CGI from the test source. These tests should all pass before the XSLT is applied to the main body of tests.

If the HTTP method used is "POST", the next document is "pass.vxml". Otherwise, the next document is "fail.vxml". The match on method name is case-insensitive.

<?xml version="1.0"?>
<!-- Check if request method was 'POST'. -->
<ircgi>
  <if-method value="post">
    <next dest="pass.vxml" /> 
  </if-method>
  <next dest="fail.vxml" /> 
</ircgi>


If the parameter "p1" was submitted, the next document is "pass.vxml". Otherwise, the next document is "fail.vxml". The value of "p1" does not matter because the value attribute was specified.

<?xml version="1.0"?>
<!--
Check if parameter p1 is present.
-->
<ircgi>
  <if-parameter name="p1">
      <next dest="../pass.vxml" /> 
  </if-parameter>
  <next dest="../fail.vxml" /> 
</ircgi>


If parameter "p1" has the value "42" and if parameter "p2" has the value "quiche", the next document is "pass.vxml". Otherwise, the next document is "fail.vxml".

This document includes comment elements whose content may be included in a log element in the response document to aid in debugging.

<?xml version="1.0"?>
<!-- Check if p1 ==  42 and p2 == 'quiche'. -->
<ircgi>
  <if-parameter name="p1" value="42">
    <comment>p1 is 42.</comment>
    <if-parameter name="p2" value="quiche">
      <comment> p2 is quiche.</comment>
      <next dest="../pass.vxml" /> 
    </if-parameter>
    <comment> p2 is not quiche.</comment>
    <next dest="../fail.vxml" />
  </if-parameter>
  <comment>p1 is not 42.</comment>
  <next dest="../fail.vxml" /> 
</ircgi>


If the "User-Agent" HTTP header is present but is empty, the next document is "fail.vxml". If the "User-Agent" header is present and not empty, the next document is "pass.vxml". If the "User-Agent" header is not present, the next document is "fail.vxml".

<?xml version="1.0" ?>
<ircgi>
  <if-header name="User-Agent">
    <if-header name="User-Agent" value="" >
      <comment>
        User-Agent header present, but empty.
      </comment>
      <next dest="../fail.vxml" />
    </if-header>
    <comment>
      User-Agent header was supplied and not empty.
    </comment>
    <next dest="../pass.vxml" />
  </if-header>
  <comment>
    User-Agent header was not supplied.
  </comment>
  <next dest="../fail.vxml" />
</ircgi>

If the parameter "p1" was submitted, the next document is "pass.vxml". Otherwise, the HTTP response code is set to "404".

<?xml version="1.0"?>
<!-- Send 404 if p1 not present -->
<ircgi>
  <if-parameter name="p1">
    <next dest="../pass.vxml" /> 
  </if-parameter>
  <next code="404" /> 
</ircgi>


If the parameter "p1" was "include", then parameter "p2" will be checked. If parameter "p2" was "pass", then the response will be the content of the file "pass.vxml" because the next element's include attribute is true. If parameter "p2" is not "pass", the response will be the content of the file "fail.vxml" because the next element's include attribute is false.

If parameter "p1" was not "include", the response document will be generated by the ircgi and include a goto to "fail.vxml" because the include attribute was false, by default. The generated document may contain a log element containing the content of the IRCGI document's comment element.

<?xml version="1.0"?>
<!-- If p1 == 'include', include 'pass.vxml' if p2 == 'pass'. -->
<ircgi>
  <if-parameter name="p1" value="include">
    <if-parameter name="p2" value="pass">
      <next dest="../pass.vxml" include="true"/> 
    </if-parameter>
    <next dest="../fail.vxml" include="true" />
  </if-parameter>
  <comment>p1 is not 'include'.</comment>
  <next dest="../fail.vxml" /> 
</ircgi>


This example navigates to a document "fail.vxml" after sleeping for five seconds.

<?xml version="1.0"?>
<ircgi>
  <next dest="../fail.vxml" sleep="5" /> 
</ircgi>


This example checks the Content-Type header for a partial match on "multipart/form-data".

<ircgi>
  <if-header name="Content-Type">
    <if-header name="Content-Type" starts-with="multipart/form-data" ignore-case="true">
      <next dest="../pass.vxml" />
    </if-header>
    <comment>
      Content-Type header was not multipart/form-data .
    </comment>
    <next dest="../fail.vxml" />
  </if-header>
  <next dest="../fail.vxml" />
</ircgi>

The starts-with attribute is used instead of value since the boundary portion of the value cannot be controlled. An example of a Content-Type header value when the encoding is set to "multipart/form-data" follows:


multipart/form-data; boundary=---------------------------7d39216110392


The following example includes a .js document. The .js document includes a single statement that sets a variable to the value of the special variable __EPOCH__. At runtime, the CGI detects the special variable and replaces it with the server-calculated number of seconds since 'the epoch'. This feature is useful in testing to verify the caching behavior of a VoiceXML interpreter by making multiple IRCGI requests and comparing the values of __EPOCH__. If the values differ, the document was fetched from the Web. If not, the document was retrieved from the browser's local cache.

The IRCGI follows:

<ircgi>
  <next sleep="2" dest="../epoch.js" include="true"/>
</ircgi>

The .js document follows:

var epoch = __EPOCH__;

This example returns the document "cache_me.vxml" along with an Expires header set to 60 seconds after the CGI is requested.

<ircgi>
  <next dest="../cache_me.vxml" include="true" expires="60"/>
</ircgi>

If the CGI parameter named "recording" is a valid file upload and its size is equal to the value of the CGI parameter 'recsize' the next document is "pass.vxml". Otherwise, the next document is "fail.vxml".

<?xml version="1.0"?>
<!--
Check if upload was of a specified size.
-->
<ircgi>
  <!-- 
    @name represents the upload parameter name
    @size can refer to a static numeric value or, 
      as shown here, to a CGI parameter name
  -->
  <if-upload name="recording" size="recsize">
      <next dest="../pass.vxml" /> 
  </if-upload>
  <next dest="../fail.vxml" /> 
</ircgi>


The following IRCGI expects precisely 3 CGI parameters the names and values of which are designated by the contents of the params element referenced by the if-all-params element.

<?xml version="1.0"?>
<!-- Check if submitted parameters are in the set plist1 -->
<ircgi>
  <params id="plist1">
  <param name="p1" value="42"/>
  <param name="p2.dinner.main" value="quiche"/>
  <param name="p3" value="a 1 c"/>
  </params>
  <if-all-params ref="plist1">
      <next dest="../pass.vxml" /> 
  </if-all-params>
  <next dest="../fail.vxml" />
</ircgi>


For security purposes, transformed IRCGI documents are deployed to an isolated directory, named "cgi-bin" under the assertion directory. Other, non-IRCGI, server-side programs that may be needed are also located in the "cgi-bin" directory.

The source IRCGI documents must be transformed into files that are executable in the test environment, such as Perl or JSP files. The output files must reside in the "cgi-bin" directory and must have the ".ircgi" file extension. The file extension must be ".ircgi" because this is how other test documents (.txml) will refer to them. The .txml to .vxml transformation process cannot automatically change ".ircgi" ".pl" or ".jsp" because some tests may use ECMAScript expressions to build the reference to the ".ircgi" document.

Two sample XSL stylesheets are provided.

The following XSLT document can be used to transform an IRCGI document to JSP:

<?xml version="1.0"?>
<!-- Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved. See http://www.w3.org/Consortium/Legal/. -->
<!-- Transforms an ircgi test file into a Java Server Page.-->
<!-- 
    NOTE: This JSP requires the com.oreilly.servlet package available at http://www.servlets.com/cos/
    The source code, object code, and documentation in the com.oreilly.servlet package
    (http://www.servlets.com/cos/) is copyright and owned by Jason Hunter.
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>

<xsl:template match="/" >
  <xsl:apply-templates />
</xsl:template>

<xsl:template match="ircgi">
  <xsl:call-template name="header" />
  <xsl:call-template name="declarations" />
  <xsl:call-template name="determineResult" />
</xsl:template>

<!--   Official contentType is 'application/voicexml+xml'.
  Change contentType to 'text/xml' to view generated JSP in IE.

   NOTE: The "header" template content below must be on a single line so that whitespace is not
   introduced at the beginning of the resulting XML document. XML parsers will complain. 
   Line breaks are included here for readability in the documentation.
 -->
<xsl:template name="header" >&lt;%@ page language="java" contentType="text/xml" %>
&lt;%@ page import="java.io.*" %>
&lt;%@ page import="java.net.*" %>
&lt;%@ page import="java.util.*" %>
&lt;%@ page import="com.oreilly.servlet.multipart.*" %>
</xsl:template>

<!--   Define a Result class to hold the destination, comments, and
  HTTP status code that will be set by 'determineResult' method.
-->
<xsl:template name="declarations">&lt;%!
    
    // Handles server side includes so they can be parsed
    private class JSPIncluder
    {
        void readInput(HttpServletRequest request, String strIncludePath) throws JspException
        {
            URLConnection conn;

            // Get URL
            StringBuffer strUrl = request.getRequestURL();
            String strUri = strUrl.toString();
            int nFindSlash = strUri.lastIndexOf("/");
            if (nFindSlash != -1)
            {
                strUri = strUri.substring(0, nFindSlash + 1);              
            }
            strUri += strIncludePath;
            // Open connection
            try
            {
                conn = (new URL(strUri)).openConnection();
                conn.setDoInput(true);
                conn.setDoOutput(false);
                conn.connect();
            }
            catch (Exception e)
            {
                throw new JspException(e.toString());
            }

            // Read in contents
            try
            {
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                StringBuffer buff = new StringBuffer();
                char[] chars = new char[2048];
                int nLen;

                while ((nLen = in.read(chars, 0, chars.length)) &gt;= 0)
                {
                    buff.append(chars, 0, nLen);
                }
                m_strBuffer = buff.toString();
                in.close();
            }
            catch (Exception e)
            {
                throw new JspException(e.toString());
            }
        }

        boolean replace(String strFind, String strReplace)
        {
            boolean bFound = false;
            if (m_strBuffer != null &amp;&amp; m_strBuffer.length() > 0)
            {
                int a = 0;
                int b = 0;
                while (true)
                {
                    a = m_strBuffer.indexOf(strFind, b);
                    if (a != -1)
                    {
                        m_strBuffer = m_strBuffer.substring(0, a) + strReplace + m_strBuffer.substring(a + strFind.length());
                        b = a + strReplace.length();
                        bFound = true;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            return bFound;
        }
        
        void doOutput(PageContext context) throws JspException
        {
            JspWriter out = context.getOut();
            try
            {
                out.print(m_strBuffer.toString());
            }
            catch (Exception e)
            {
                throw new JspException(e.toString());
            }   
        }
        private String m_strBuffer;
    }
    
    // Handles multipart/form-data 
    private class MultiPartHandler 
    {
        HttpServletRequest request;
        
        public MultiPartHandler(HttpServletRequest req)
        {
            request = req;
        }
        
        public boolean find(String strFind, int nOfSize) throws JspException 
        {            
            MultipartParser parser;
            Part part;
            String strName;
            if((request.getContentType() != null)&amp;&amp;(request.getContentType().startsWith("multipart/form-data")))
            {
                try
                {
                    parser = new MultipartParser(request, request.getContentLength());
                    while ((part = parser.readNextPart()) != null)
                    {
                        strName = part.getName();
                        if(strName.equals(strFind))
                        {
                            if (nOfSize == -1)
                            {
                                return true;
                            }
                            else
                            {
                                if (part.isFile())
                                {
                                    InputStream stream = ((FilePart)part).getInputStream();
                                    if (getSizeOfStream(stream) == nOfSize)
                                    {
                                        return true;
                                    }
                                }
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    throw new JspException(e.toString());
                }
            }
            
            return false;        
         }
         
         private long getSizeOfStream(InputStream stream)
         {
            if (null == stream) return 0;
            
            int nRead = 0;
            int nSize = 0;
            byte[] temp = new byte[1024];
            try 
            {
                while ((nRead = stream.read(temp)) != -1)
                {
                    nSize += nRead;
                }  
            }
            catch (IOException e) {}
            
            return nSize;         
         }
     }
    
  private class Result {
    String dest;
    long sleep = 0;
    boolean expiresHeaderSet = false;
    long expires = 0;
    boolean include = false;
    StringBuffer comments = new StringBuffer();
    int statusCode = 200;
  }
  private final String NL = System.getProperty("line.separator");
  private void determineResult(HttpServletRequest request, Result result, MultiPartHandler multipart) throws JspException
  {
    <xsl:apply-templates />
  }
%&gt;</xsl:template>


<!--  Create Result object and call 'determineResult' to set its fields.
  Return VoiceXML document only if HTTP response status code is 200.
  Otherwise, return just the status code.
-->
<xsl:template name="determineResult" >&lt;%
    Result myResult = new Result();
    MultiPartHandler myMultiPart = new MultiPartHandler(request);
    determineResult(request, myResult, myMultiPart);
    response.setStatus(myResult.statusCode);
    
    if (myResult.sleep &gt; 0)
    {
        try
        {
            Thread.sleep(myResult.sleep * 1000);
        }
        catch (InterruptedException e)
        {
            throw new JspException(e.toString());
        }
    }
    
    if (myResult.expiresHeaderSet)
    {
        Date now = new Date();
        long nMillis = now.getTime();
        response.setDateHeader("Expires", nMillis + myResult.expires*1000);
    }
    
    if (myResult.include) 
    {
        Date now = new Date();
        long nMillis = now.getTime();
        String strEpoch = String.valueOf(nMillis);
        JSPIncluder includer = new JSPIncluder();
        includer.readInput(request, myResult.dest);
        includer.replace("__EPOCH__", strEpoch);
        includer.doOutput(pageContext);
    }
    else {%&gt;<xsl:call-template name="vxml" />&lt;%}%&gt;
</xsl:template>


<xsl:template match="if-parameter" >
  if (<xsl:call-template name="genIfExpr">
        <xsl:with-param name="type" select="'Parameter'" />
      </xsl:call-template>) {
    <xsl:apply-templates />
  }
</xsl:template>


<xsl:template match="if-header" >
  if (<xsl:call-template name="genIfExpr">
        <xsl:with-param name="type" select="'Header'" />
      </xsl:call-template>) {
    <xsl:apply-templates />
  }
</xsl:template>


<xsl:template match="if-all-params">
<xsl:choose>
<xsl:when test="/ircgi/params[@id=current()/@ref][not(param)]">
  if (request.getParameterMap().isEmpty()) {
      <xsl:apply-templates />
  }
</xsl:when>
<xsl:otherwise>
  if (<xsl:for-each select="/ircgi/params[@id=current()/@ref]/param">
        <xsl:call-template name="genIfExpr">
          <xsl:with-param name="type" select="'Parameter'" />
        </xsl:call-template>
        <xsl:if test="position() != last()">&amp;&amp;</xsl:if>
      </xsl:for-each>) {
    <xsl:apply-templates />
  }
</xsl:otherwise>
</xsl:choose>
</xsl:template>


<xsl:template match="if-upload" >
  int nSize = -1;
  <xsl:if test="@size">
    if (null != request.getParameter("<xsl:value-of select='@size'/>"))
    {
      try {
        nSize = Integer.parseInt(request.getParameter("<xsl:value-of select='@size'/>"));
      }
      catch (NumberFormatException e) { }
    }
    else
    {
      try {
        nSize = Integer.parseInt("<xsl:value-of select='@size'/>");
      }
      catch (NumberFormatException e) { }
    }
  </xsl:if>  
  if (multipart.find("<xsl:value-of select="@name"/>", nSize))
  {
    <xsl:apply-templates />
  } 
</xsl:template>


<!--  Generate an expression that determines when the condition of an if-parameter / if-header
  element is true.  The 'type' parameter determines the
  HttpServletRequest method to use to get the value to be checked.
-->
<xsl:template name="genIfExpr" >
  <xsl:param name="type" />
  <xsl:param name="size" />
  <xsl:variable name="method">
    <xsl:choose>
      <xsl:when test="@ignore-case = 'true'" >equalsIgnoreCase</xsl:when>
      <xsl:otherwise>equals</xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <xsl:choose>
    <xsl:when test="@value">
      request.get<xsl:value-of select="$type"/>
        ("<xsl:value-of select="@name"/>") != null &amp;&amp;
      request.get<xsl:value-of select="$type"/>
        ("<xsl:value-of select="@name"/>").<xsl:value-of select="$method"/>
        ("<xsl:value-of select="@value" />")
    </xsl:when>
    <xsl:when test="@starts-with">
       request.get<xsl:value-of select="$type"/>
        ("<xsl:value-of select="@name"/>") != null &amp;&amp;
       request.get<xsl:value-of select="$type"/>
        ("<xsl:value-of select="@name"/>").startsWith("<xsl:value-of select="@starts-with"/>")
    </xsl:when>     
    <xsl:otherwise>
      request.get<xsl:value-of select="$type"/>("<xsl:value-of select="@name"/>") != null 
        || multipart.find("<xsl:value-of select="@name"/>", -1)
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>


<xsl:template match="if-method" >
  if (request.getMethod().equalsIgnoreCase("<xsl:value-of select="@type" />")) {
    <xsl:apply-templates />
  }
</xsl:template>


<!--  Disarm double quotes and newline characters from comments, then
  add to result's comment buffer for use later in 'log' element.
-->
<xsl:template match="comment" >
  result.comments.append
    ("<xsl:value-of select="translate(., '&#034;&#013;&#010;', '   ')" />".trim());
  result.comments.append(NL);
</xsl:template>


<xsl:template match="next" >
  <xsl:if test="@code" >
    result.statusCode = <xsl:value-of select="@code" />;
  </xsl:if>
  <xsl:if test="@dest" >
      result.dest = "<xsl:value-of select="@dest" />";
  </xsl:if>
  <xsl:if test="@include = 'true'">
    result.include = true;
  </xsl:if>  
  <xsl:if test="@sleep">
      result.sleep = <xsl:value-of select="@sleep" />;
  </xsl:if>  
  <xsl:if test="@expires">
      result.expiresHeaderSet = true;
      result.expires = <xsl:value-of select="@expires" />;
  </xsl:if>
    return;
</xsl:template>


<!--  Generate VoiceXML document that does a 'goto' to the document
  indicated in myResult.  If comment buffer is not empty, include
  a 'log' element to aid in debugging.
-->
<xsl:template name="vxml" ><![CDATA[<?xml version="1.0" ?>
<vxml version="2.1" xmlns="http://www.w3.org/2001/vxml">
  <form>
    <block><% String comments = myResult.comments.toString();
   if (comments.length()>0) {%>
      <log>]]>
        <xsl:text disable-output-escaping="yes">
          &lt;![CDATA[&lt;%= comments %&gt;]]&gt;
        </xsl:text><![CDATA[
      </log><%}%>
      <goto next="<%= myResult.dest %>"/>
    </block>
  </form>
</vxml> ]]>
</xsl:template>

</xsl:stylesheet>

The following XSLT document can be used to transform an IRCGI document to Perl:

<?xml version="1.0"?>
<!-- Copyright 1998-2005 W3C (MIT, ERCIM, Keio), All Rights Reserved. See http://www.w3.org/Consortium/Legal/. -->
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>

<xsl:template match="/" >
  <xsl:apply-templates />
</xsl:template>

<!-- root document element -->
<xsl:template match="ircgi">
  <xsl:call-template name="header" />
  <xsl:apply-templates />
  <xsl:call-template name="footer" />
</xsl:template>

<!-- handle CGI parameter checks -->
<xsl:template match="if-parameter" >
  $val = param("<xsl:value-of select="@name"/>");
  <xsl:call-template name="check-value">
    <xsl:with-param name="value" select="@value"/>
    <xsl:with-param name="starts-with" select="@starts-with"/>
    <xsl:with-param name="ignore-case" select="@ignore-case"/>
  </xsl:call-template>
</xsl:template>

<xsl:template match="if-all-params[@ref]" >
  if (CheckAllParams({<xsl:for-each select="/ircgi/params[@id=current()/@ref]/param">'<xsl:value-of select="@name"/>' 
        => '<xsl:value-of select="@value"/>'<xsl:if test="position() != last()">,</xsl:if></xsl:for-each>}, \@comments)) {
    <xsl:apply-templates/>
  }
</xsl:template>


<!-- 
if-upload - perform checks on a file upload
@name - does an upload exist with the given name
@size - (optional) check its size. if the value is a number, compare directly. 
       if it starts with alpha or '_', see if there's a param with that name, and compare against the value
@type - (optional) verify that the uploaded data is the specified type
   <if-upload name="recording" size="recsize">
      <next dest="../pass.vxml"/>
   </if-upload>
-->
<xsl:template match="if-upload">
  if (CheckUpload('<xsl:value-of select="@name"/>', 
    <xsl:choose><xsl:when test="@size">'<xsl:value-of select="@size"/>'</xsl:when>
    <xsl:otherwise>undef</xsl:otherwise></xsl:choose>, \@comments)) {
    <xsl:apply-templates/>
  }
</xsl:template>

<!-- handle HTTP Request header checks -->
<xsl:template match="if-header">
  $val = $ENV{<xsl:call-template name="map-header"><xsl:with-param name="header" select="@name"/></xsl:call-template>};
  <xsl:call-template name="check-value">
    <xsl:with-param name="value" select="@value"/>
    <xsl:with-param name="starts-with" select="@starts-with"/>
    <xsl:with-param name="ignore-case" select="@ignore-case"/>
  </xsl:call-template>
</xsl:template>

<!-- in Perl, the Request-Method is just another HTTP Request header -->
<xsl:template match="if-method">
 $val = $ENV{REQUEST_METHOD};
  <xsl:call-template name="check-value">
    <xsl:with-param name="value">
      <xsl:choose>
        <xsl:when test="@type"><xsl:value-of select="@type"/></xsl:when>
        <xsl:otherwise>get</xsl:otherwise> <!-- default http request method -->
      </xsl:choose>
    </xsl:with-param>
    <xsl:with-param name="ignore-case" select="'true'"/>
  </xsl:call-template>
</xsl:template>

<!-- strip comment elements -->
<xsl:template match="comment">
  push @comments, qq {<xsl:value-of select="."/>};
</xsl:template>

<!-- handle next elements -->
<xsl:template match="next">
    return {
  <xsl:choose>
    <xsl:when test="not(@code) and not(@dest)">
      status => "200",
    </xsl:when>
  <xsl:otherwise>
    <xsl:if test="@dest">
      next => "<xsl:value-of select="@dest" />", 
      <xsl:if test="@include = 'true'">
        include => 1,
      </xsl:if>
    </xsl:if>
    <xsl:if test="@code">
      status => "<xsl:value-of select="@code" />",
    </xsl:if>    
  </xsl:otherwise>
  </xsl:choose>
  <xsl:if test="@sleep">
    sleep => "<xsl:value-of select="@sleep"/>",    
  </xsl:if>
  <xsl:if test="@expires">
    expires => int(<xsl:value-of select="@expires"/>),
  </xsl:if>
    comments => \@comments};
</xsl:template>

<!-- check the value, if any, and continue processing child elements -->
<xsl:template name="check-value">
<xsl:param name="value"/>
<xsl:param name="starts-with"/>
<xsl:param name="ignore-case"/>
<xsl:param name="equals-upload-size"/>
<xsl:choose>
<xsl:when test="$starts-with">
  my $starts = '<xsl:value-of select="$starts-with"/>';
  if (defined($val) &amp;&amp; ($val =~ /^$starts/<xsl:if test="$ignore-case='true'">i</xsl:if>)) {
    <xsl:apply-templates/>
  }
</xsl:when>
<xsl:when test="$value"> <!-- XSLT 1.0 11.2: empty attr == missing attr -->
  <xsl:choose>
  <xsl:when test="$value=''">
    if (defined($val) &amp;&amp; ($val =~ /^\s*$/)) {
      <xsl:apply-templates/>
    }
  </xsl:when>
  <xsl:otherwise>
    my $match = '<xsl:value-of select="$value"/>';
    if (defined($val) &amp;&amp; ($val =~ /^$match$/<xsl:if test="$ignore-case='true'">i</xsl:if>)) {
      <xsl:apply-templates/>
    }
  </xsl:otherwise>
  </xsl:choose>
</xsl:when>
<xsl:otherwise>
  if (defined($val)) {
    <xsl:apply-templates/>
  }
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<xsl:template name="header">#!/usr/local/bin/perl -w
use strict;
use CGI qw(param);
use CGI::Util qw(expires);
use IO::Handle ();

# limit sleep time to 1 minute to prevent DOS attack
use constant SLEEP_LIMIT => 60;

# forward decls
sub GetStatusText;
sub Run;
sub JumpTo;
sub GetContentType;
sub ExpiresFromDelta;
sub GetFileSize;
sub CheckUpload;
sub CheckAllParams;

my $rhRetval = Run();
my $next = $rhRetval->{next}; # where to Mr. Magoo?
my $statusCode = $rhRetval->{status};
my $statusText = "unknown status";
my $ctype = GetContentType($next);
my $raComments = $rhRetval->{comments};
my $bInclude = $rhRetval->{include};
my $expires_delta = $rhRetval->{expires};
my $epoch = time;

if (defined($next) &amp;&amp; defined($bInclude) &amp;&amp; 1 == $bInclude) {
  # restrict paths when allowing source inclusion
  if (($next =~ /^\//) || ($next =~ /\/\.\./)) {
    $statusCode = 403;
  }
}

my $sleep = $rhRetval->{sleep};
if (defined($sleep)) {
  if (($sleep =~ /^\d+$/) &amp;&amp; ($sleep &lt;= SLEEP_LIMIT)) {
    sleep $sleep;
  } else {
    push @$raComments, "Bad sleep interval $sleep";
  }
}

print "Content-Type: $ctype\n";
if (defined($expires_delta)) {
  print ExpiresFromDelta($expires_delta) . "\n";
}
if(defined($statusCode)) {
  $statusText = GetStatusText($statusCode);
  print "Status: $statusCode $statusText\n\n";
} else {
  print "\n";
}

if (!defined($next)) {
  print "$statusText\n";
} else {
  my $content;
  if ($bInclude) {
    $! = 0; # clear i/o errs
    open HINCLUDE, $next;
    if ($! != 0) {
      push @$raComments, "Unable to open $next";
      $content = JumpTo($next, $raComments);
      print STDERR "Unable to open $next\n";
    } else {
      my $eor = $/;
      undef $/;
      $content = &lt;HINCLUDE&gt;;
      # allow caching tests to be performed by interpolating __EPOCH__
      $content =~ s/__EPOCH__/$epoch/g;
      close HINCLUDE;
      $/ = $eor;  
    }
  } else {
    $content = JumpTo($next, $raComments);
  }

  print $content;
}

# Return a simple VoiceXML document that navigates 
#   to the URI specified by $next
# Dump the comments in the array $raComments to the call log
sub JumpTo
{
  my($next, $raComments) = @_;

<![CDATA[
  my $content = <<EOF;
<?xml version="1.0"?>
<vxml version="2.1"
  xmlns="http://www.w3.org/2001/vxml"
>
<form>
  <block>
EOF
]]>
foreach my $comment (@$raComments) {
  $content .= qq{&lt;log>$comment &lt;/log>\n};
}
<![CDATA[
$content .= <<EOF;
    <goto next="$next"/>
  </block>
</form>
</vxml>
EOF
]]>

  $content;
}


# Determine what to do next
# Return a hash containing one or more of the following keys:
#   next - the next document to navigate to 
#   code - the HTTP response code
#   comments - a reference to an array of comments to aid in debugging
sub Run
{
  my $val; # temp var to stash param or header value
  my @comments = (); # array of comments obtained while processing
</xsl:template>

<xsl:template name="footer" >
}

# Map a status code to an informative string
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
sub GetStatusText
{
  my($code) = @_;

  my $rhCodes = {100 => "Continue",
  101 => "Switching Protocols",
  200 => "OK",
  201 => "Created",
  202 => "Accepted",
  203 => "Non-Authoritative Information",
  204 => "No Content",
  205 => "Reset Content",
  206 => "Partial Content",
  300 => "Multiple Choices",
  301 => "Moved Permanently",
  302 => "Found",
  303 => "See Other",
  304 => "Not Modified",
  305 => "Use Proxy",
  307 => "Temporary Redirect",
  400 => "Bad Request",
  401 => "Unauthorized",
  402 => "Payment Required",
  403 => "Forbidden",
  404 => "Not Found",
  405 => "Method Not Allowed",
  406 => "Not Acceptable",
  407 => "Proxy Authentication Required",
  408 => "Request Time-out",
  409 => "Conflict",
  410 => "Gone",
  411 => "Length Required",
  412 => "Precondition Failed",
  413 => "Request Entity Too Large",
  414 => "Request-URI Too Large",
  415 => "Unsupported Media Type",
  416 => "Requested range not satisfiable",
  417 => "Expectation Failed",
  500 => "Internal Server Error",
  501 => "Not Implemented",
  502 => "Bad Gateway",
  503 => "Service Unavailable",
  504 => "Gateway Time-out",
  505 => "HTTP Version not supported extension-code"};

  return (exists($rhCodes->{$code}) ? $rhCodes->{$code} : "invalid status code");
}

sub GetContentType
{
  my($next) = @_;

  my $ctype = "text/plain";
  if (defined($next)) {
    my $rhTypes = {'txml' => 'text/xml', 'vxml' => 'text/xml', 
      'xml' => 'text/xml', 'srgs' => 'text/xml'};
    my @parts = split /\./, $next;    
    my $ext = $parts[0];
    if (exists($rhTypes->{$ext})) {
      $ctype = $rhTypes->{$ext};
    }    
  }
  
  $ctype;
}

# return an expires header given seconds since epoch
sub ExpiresFromDelta
{
  my($delta) = @_;
  $delta = (($delta >= 0 &amp;&amp; $delta !~ /^\+/) ? "+" : "") . $delta . "s";
  "Expires: " . expires($delta);
}

# check the size of the file referenced by $fh
sub GetFileSize
{
  my($fh) = @_;
  my $size = 0;
  if (defined($fh)) {
    my $io = IO::Handle->new_from_fd(fileno($fh), 'r');
    if (defined($io)) {
      my @stats = $io->stat();
      $size = $stats[7];
    }
  }
  $size;
}

# check the validity of the named file upload
# optionally validate its size
sub CheckUpload
{
  my($name, $compsize, $raComments) = @_;
  my $retval = 0;
  my $size = GetFileSize(param($name));
  if (defined($compsize)) {
    if ($compsize =~ /^\d+$/) {
      # if all digits, assume static value for comparison
      $compsize = int($compsize);
    } elsif ($compsize =~ /^[a-zA-Z_]/) {   
      # assume a reference to another parameter
      my $psize = param($compsize);
      if (defined($psize)) {
        # we expect digits
        if ($psize =~ /^\d+$/) {
          $compsize = int($psize);
        } else {
          push @$raComments, 'invalid if-upload/@size ' . $compsize . '; expected number';
        }
      } else {
        push @$raComments, 'unable to retrieve if-upload/@size ' . $compsize;
      }
    } else {
      # yikes. bad variable name
      push @$raComments, 'bad if-upload/@size ' . $compsize;
    }
    if ($size == $compsize) {
      $retval = 1;
    } else {
      push @$raComments, 'file size ' . $size . ' did not match expected size ' . $compsize;
    }
  } else {
    # just check if we got an upload with this name
    if ($size &gt; 0) {
      $retval = 1;
    } else {
      push @$raComments, 'no file upload found for ' . $name;
    }
  }

  $retval;
}

# loop through the CGI params received in the HTTP request and verify that their names/values
# exist in the hash reference. also verify that all of the expected values were passed in the request
sub CheckAllParams
{
  my($rhExpectedParams, $raComments) = @_;
  my $ret = 1;
  my @params = param();
  # loop through what we got in the HTTP request, and see if we expected it
  foreach my $name (@params) {
    my $vSubmitted = param($name);
    my $vExpected = $rhExpectedParams->{$name};
    if (!defined($vExpected)) { # if the values aren't equal, we'll catch it during the next loop
      push @$raComments, "unexpected '$name' in request'";
      $ret = 0;
    }
  }

  # loop through what we expected to get, and see if we received it
  foreach my $expected (keys %$rhExpectedParams) {
    my $vExpected = $rhExpectedParams->{$expected};
    my $vSubmitted = param($expected);
    if (!defined($vSubmitted)) {
      push @$raComments, "expected '$expected' not found";
      $ret = 0;
    } elsif ($vExpected ne $vSubmitted) {
      push @$raComments, "expected '$expected' to be '$vExpected' not '$vSubmitted'";
      $ret = 0;
    }
  }

  $ret;
}
</xsl:template>

<!-- 
  the headers we're willing to expose.
  allowing arbitrary header requests is a security risk
-->
<xsl:template name="map-header">
<xsl:param name="header"/>
<xsl:choose>
  <xsl:when test="$header = 'User-Agent'">HTTP_USER_AGENT</xsl:when>
  <xsl:when test="$header = 'Request-Method'">REQUEST_METHOD</xsl:when>
  <xsl:when test="$header = 'Content-Type'">CONTENT_TYPE</xsl:when>
  <xsl:otherwise>__UNKNOWN__<xsl:value-of select="$header"/></xsl:otherwise>
</xsl:choose>
</xsl:template>

</xsl:stylesheet>

The directory convention requires test developers to ensure that paths from and to other test documents follow the convention.

The following example contains a reference to an IRCGI document from a VoiceXML document:

<goto next="cgi-bin/ua.ircgi" />

The following example contains a reference to a VoiceXML document from an IRCGI document:

<next dest="../pass.vxml"/>

Given the directory convention, Web server administrators must do the following:

  • Allow CGI execution within the "cgi-bin" directory below each assertion directory.
  • Associate the ".ircgi" suffix with the preferred server-side script engine.

The following example configures Apache to execute documents with a .ircgi extension as a Perl CGI:

Addhandler cgi-script .ircgi

The following example configures Tomcat to run transformed files with a .ircgi extension as a JSP:

<servlet-mapping>
  <servlet-name>jsp</servlet-name>
  <url-pattern>*.ircgi</url-pattern>
</servlet-mapping>

The following DTD succintly declares the IRCGI API markup elements, their attributes, and the legal values for those attributes if applicable.

<!ENTITY % ir-checks "if-header | if-method | if-parameter | if-upload | if-all-params" >

<!ELEMENT ircgi (params | comment | %ir-checks; | next)*>

<!ELEMENT comment (#PCDATA) >

<!ELEMENT if-parameter (comment | %ir-checks; | next)* >
<!ATTLIST if-parameter 
  name CDATA #REQUIRED
  value CDATA #IMPLIED
  starts-with CDATA #IMPLIED
  ignore-case (true|false) "false" >

<!ELEMENT if-all-params (comment | %ir-checks; | next)* >
<!ATTLIST if-all-params
  ref IDREF #REQUIRED>

<!ELEMENT params (param+)>
<!ATTLIST params
  id ID #REQUIRED>

<!ELEMENT param EMPTY>
<!ATTLIST param 
  name CDATA #REQUIRED
  value CDATA #REQUIRED
>

<!ELEMENT if-upload (comment | %ir-checks; | next)* >
<!ATTLIST if-upload
  name CDATA #REQUIRED
  size CDATA #IMPLIED>

<!ELEMENT if-header (comment | %ir-checks; | next)* >
<!ATTLIST if-header
  name CDATA #REQUIRED
  value CDATA #IMPLIED
  starts-with CDATA #IMPLIED
  ignore-case (true|false) "false" >

<!ELEMENT if-method (comment | %ir-checks; | next)* >
<!ATTLIST if-method
  type (get|post) "get" >

<!ELEMENT next EMPTY>
<!ATTLIST next
  code CDATA "200"
  dest CDATA #IMPLIED
  include (true|false) "false"
  sleep CDATA #IMPLIED
  expires CDATA #IMPLIED>

Tellme Networks, Inc.Terms of ServicePrivacy PolicyGeneral Disclaimers