Using Cookies

You can think of the Tellme VoiceXML interpreter as a Web browser for the telephone. In addition to fetch resources across the Web using HTTP and HTTPS, and parsing and executing VoiceXML and JavaScript, the Tellme VoiceXML intepreter is also capable of exchanging bits of information known as cookies - just like a Web browser. This example show you how to set, retrieve, and delete cookies using the Tellme VoiceXML interpreter.

Try It!
  1. Set your Application URL to http://studio.247-inc.net/library2/code/ex-116/index.vxml
  2. Call the number shown in VXML Tools page
  3. Sign in, and play!

How it works

The example exchanges data with the server via cookies. The GetCookieValue dialog asks the user for their favorite cookie flavor - chocolate chip, graham cracker, etc. The SetCookieClient dialog creates a cookie named "favorite_flavor" in JavaScript using the setCookie function. The GetCookieCals dialog sends the cookie information to a CGI script via a subdialog element.

The CGI script, setcookie.cgi, takes a parameter, cookie_command, and expects it to be set to the value "set" or "delete". The first time the VoiceXML document calls the CGI script, it passes the value "set". The CGI uses the CGI::Cookie module to extract the value of the favorite_flavor cookie, retrieves the number of calories for the specified flavor by calling the GetCalories function, creates a cookie named "calories" with this value, and returns a simple subdialog to the VoiceXML interpreter.

Upon successful execution of the subdialog, the VoiceXML interpreter navigates to the ReadCookieCals and calls the user-defined function GetCookie to retrieve the cookies named "favorite_flavor" and "calories" from the cookie cache associated with the document. The user-defined function calls the native function getCookies implemented by the Tellme VoiceXML interpreter.

After reading back the number of calories in the cookie specified by the caller, the VoiceXML interpreter navigates to the CleanupCookies dialog which submits a second request to setcookie.cgi with the parameter cookie_command set to the value "delete". The CGI deletes both the "favorite_flavor" and "calories" cookies by setting their expires attribute to a date in the past and returns a simple subdialog to the VoiceXML intepreter.

VoiceXML 2.0 Code

The VoiceXML code for this example follows:

<?xml version="1.0"?>
<!-- 
Tellme Studio Code Example 116 
Copyright (C) 2000-2001 Tellme Networks, Inc. All Rights Reserved. 

THIS CODE IS MADE AVAILABLE SOLELY ON AN "AS IS" BASIS, WITHOUT WARRANTY 
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, 
WARRANTIES THAT THE CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A 
PARTICULAR PURPOSE OR NON-INFRINGING. 
--> 
<vxml version="2.0">

<!-- give the caller a chance to escape the app -->
<link event="event.exit">
  <grammar mode="voice"
           root="root_rule"
           tag-format="semantics/1.0"
           type="application/srgs+xml"
           version="1.0"
           xml:lang="en-US">
    <rule id="root_rule" scope="public">
      <one-of>
        <item> quit </item>
        <item> exit </item>
      </one-of>
    </rule>
  </grammar>
</link>

<catch event="event.exit">
   <exit />
</catch>

<link event="help">
  <grammar mode="dtmf"
           root="root_rule"
           tag-format="semantics/1.0"
           type="application/srgs+xml"
           version="1.0">
    <rule id="root_rule" scope="public">
      <item> 0 </item>
    </rule>
  </grammar>

  <grammar mode="voice"
           root="root_rule"
           tag-format="semantics/1.0"
           type="application/srgs+xml"
           version="1.0"
           xml:lang="en-US">
    <rule id="root_rule" scope="public">
      <item weight="0.001">
        help
      </item>
    </rule>
  </grammar>
</link>

<error>
   <audio>Catcheable error</audio>
</error>

<!-- Document scoped variable-->

<!-- stores the name of the cookie -->
<var name="cookie_name" expr="'favorite_flavor'"/>

<!-- stores the user's flavor choice to be stored as a cookie value -->
<var name="cookie_flavor" />

<!-- 
Establish the cookie's value 
-->
<form id="GetCookieValue">
   <!-- javascript array used to list available cookie flavors -->
   <var name="flavors" expr="new Array('chocolate chip', 'oreo', 
      'graham cracker', 'vanilla wafer')"/>

   <field name="flavor">
      <prompt>
      Here's a list of cookies. Tell us your favorite.
      <break time="300ms"/>
      <foreach item="cookie" array="flavors">
         <audio><value expr="cookie"/></audio>
         <break time="300ms"/>
      </foreach>
      </prompt>

      <!-- grammar choices correspond to flavor list -->
      
      <grammar mode="voice"
               root="root_rule"
               tag-format="semantics/1.0"
               type="application/srgs+xml"
               version="1.0"
               xml:lang="en-US">
        <rule id="root_rule" scope="public">
          <one-of>
            <item>
              <one-of>
                <item>
                  chocolate chip
                </item>
              </one-of>
              <tag>out.flavor = "chocolate chip";</tag>
            </item>
            <item>
              <one-of>
                <item>
                  oreo
                </item>
              </one-of>
              <tag>out.flavor = "oreo";</tag>
            </item>
            <item>
              <one-of>
                <item>
                  graham
                  <item repeat="0-1">
                    cracker
                  </item>
                </item>
              </one-of>
              <tag>out.flavor = "graham cracker";</tag>
            </item>
            <item>
              <one-of>
                <item>
                  <one-of>
                    <item>
                      vanilla
                    </item>
                    <item>
                      nilla
                    </item>
                  </one-of>
                  wafer
                </item>
              </one-of>
              <tag>out.flavor = "vanilla wafer";</tag>
            </item>
          </one-of>
        </rule>
      </grammar>


      <catch event="nomatch noinput">
         Sorry. I didn't get that. Tell me your favorite type of cookie.      
      </catch>

      <help>
         You're choosing a cookie. Please tell us your favorite.
      </help>

      <filled>
         <assign name="cookie_flavor" expr="flavor"/>
         <goto next="#SetCookieClient"/>
      </filled>
   
   </field>
</form>

<!-- 
Set the flavor cookie using the "client-side" cookie API
-->
<form id="SetCookieClient">
<!-- data to send to cgi via querystring -->
<var name="command" expr="'set'"/>
<block>
   <script>
      setCookie(escape(cookie_name) + "=" + escape(cookie_flavor));
   </script>
   <goto next="#GetCookieCals"/>
</block>
</form>

<!-- 
   The flavor pref will be sent to the server automatically as a cookie. 
   Note that we *could* have sent it via the namelist or as part of 
   the querystring instead, but then we wouldn't be demonstrating 
   setting cookies on the client, would we?
-->
<form id="GetCookieCals">
<var name="cookie_command" expr="'set'"/>
<subdialog name="oResult" src="setcookie.cgi" 
   namelist="cookie_command">
<filled>
  <if cond="oResult.code == 1">
    <goto next="#ReadCookieCals"/>
  <else/>
    <log>error setting cookie flavor</log>
    <log><value expr="oResult.msg"/></log>
    <audio>We were unable to send your flavor 
        preference to the server. Check the log.</audio>
    <exit/>
  </if>
</filled>
</subdialog>
</form>

<!-- 
Retrieve the cookies using the "client-side" cookie API
-->
<form id="ReadCookieCals">
<script>
<![CDATA[
// generally useful helper function that retrieves 
// a specific cookie by name
function GetCookie(name)
{
   var cookies = getCookies();
   if (!cookies)
   {      
      return null; // no cookies
   }
   
   var search = name + "=";
   var iLen = cookies.length;
   if (iLen > 0) 
   {
      // find where cookie begins
      var begin = cookies.indexOf(search);
      if (begin != -1)
      {          
         begin += search.length;
         // search for the end of the value
         var end = cookies.indexOf(";", begin);
         if (end == -1)
         {
            // delimiter not found, so eat rest of cookie string
            end = iLen;
         }

         return unescape(cookies.substring(begin, end));
      }
   }

   return null; // cookie not found
}
]]>
</script>

<block>
   <!-- retrieve the cookies we know about -->
   <var name="flavor" expr="GetCookie(cookie_name)"/>
   <var name="calories" expr="GetCookie('calories')"/>
   <if cond="!flavor || !calories">
      <audio>
         Unable to determine number of calories in 
         <value expr="cookie_flavor"/> cookie.
      </audio>
      <exit/>
   <else/>
      <if cond="flavor != cookie_flavor">
         <log>
         Our user's choice of cookie (<value expr="cookie_flavor"/>) 
         has been switched (<value expr="flavor"/>). 
         Hope they don't mind...</log>
      </if>
      <audio>
        There are <value expr="calories"/> calories
        in a <value expr="flavor"/> cookie.
      </audio>
      <break time="500ms"/>
      <goto next="#CleanupCookies"/>
   </if>
</block>
</form>

<!-- 
Demonstrate server-side cookie cleanup even though this 
will happen automatically when the session ends
-->
<form id="CleanupCookies">
   <var name="cookie_command" expr="'delete'"/>
   <block>
   <audio>Cookie cleanup...</audio>
   </block>
   <subdialog name="oResult" src="setcookie.cgi" 
      namelist="cookie_command">
	    <filled>
		<if cond="oResult.code == 1">
		    <audio> your cookies were deleted </audio>
		    <log>after delete, cookies = <value expr="getCookies()"/></log>
		    <goto next="#GetCookieValue"/>
		<else/>
		    <log>unable to delete cookies</log>
		    <log><value expr="oResult.msg"/> 
		       (<value expr="oResult.code"/>)</log>
		    <exit/>
		</if>
	    </filled>
	</subdialog>
</form>
        
</vxml>

This source code for the CGI script, setcookie.cgi, follows:

#!/usr/local/bin/perl -w

use CGI qw/:standard/;
use CGI::Cookie;	

#using CGI Cookie module, 
#see http://www.perl.com/pub/doc/manual/html/lib/CGI/Cookie.html

# use CGI module to retrieve command
my $query = new CGI;
my $command =$query->param('cookie_command');

my $cookie_cli = "favorite_flavor"; # this one's set by the client
my $cookie_svr = "calories"; # this one's set on the server (here)
my $status_code = 1; # code to send back to client; 1 = success, -1 = failure
my $status_msg = ""; # status string to send back to the client
my @cookies = ();

# If cookie command is set a cookie
if ($command eq "set")
{
   # dig the cookie that was sent by the client (VoiceXML browser)
   my %cookies = fetch CGI::Cookie;
   my $flavor = $cookies{$cookie_cli}->value;
   if (!defined($flavor))
   {
      # ERROR. Cookie wasn't set or sent
      $status_code = -1;
      $status_msg = "No flavor was specified"
   }
   else
   {
      # given the user's flavor choice, get the calorie count,
      # and return the value in a cookie
      # Note that this data could be returned via the namelist of
      # the return element
      my $cals = GetCalories($flavor);

      my $cookie = new CGI::Cookie(
         -name  => $cookie_svr,
         -value  => $cals,
         -expires =>'+1h', # or less if session/call ends
         #-domain =>'.tellme.com',
         #-path   =>'/'
         );

      $status_code = 1;
      $status_msg = "set calorie cookie";
      push @cookies, $cookie;
   }   
} 
elsif ($command eq "delete")
{
   # delete the server/calorie cookie
   my $c1 = new CGI::Cookie(
     -name    => $cookie_svr,
     -value => 0,
     -expires => 'Tue, 1 Jan 1980 08:00:00 UTC' #'-1d', # yesterday...
   #  -domain =>'.tellme.com',
   #  -path   =>'/'
     );

   push @cookies, $c1;

   # delete the client/flavor cookie
   my $c2 = new CGI::Cookie(
     -name    => $cookie_cli,
     -value => 0,
     -expires => 'Tue, 1 Jan 1980 08:00:00 UTC' # '-1d', # yesterday...
     #-domain =>'.tellme.com',
     #-path   =>'/'
     );

   push @cookies, $c2;

   $status_code = 1;
   $status_msg = "deleted cookies";
} 
else 
{
   $status_code = -1;
   if (defined($command))
   {
      $status_msg = "unexpected CGI param";
   }
   else
   {
      $status_msg = "expected CGI param set or delete";
   }
}

WriteSubdialog(\@cookies, $status_code, $status_msg);

# write some VoiceXML to the client
sub WriteSubdialog
{
   my ($raCookies, $code, $msg) = @_;

   # write http headers (including cookies)
   foreach $cookie (@$raCookies)
   {
      print "Set-Cookie: $cookie\n";
   }
   print "Content-Type: text/xml\n\n";

   # write http msg body
   print <<EOF;
<vxml version="2.0">
   <var name="code" expr="$code"/>
   <var name="msg" expr="'$msg'"/>
   
   <form>
   <block>
      <log> @$raCookies </log>
      <return namelist="code msg"/>
   </block>
   </form>
</vxml>
EOF
}

# given the cookie flavor, return the number of calories per serving
# a "real" implementation would retrieve this information from a "nutrition data server", 
# undoubtedly a "Web Service" of the not-too-distant future
# If the flavor can't be found in our simple hash table, return 0 (no calories)
sub GetCalories
{
   my($flavor) = @_;

   my $lcFlav = lc($flavor); # convert to lowercase

   my %flav2cal = (
          'chocolate chip' => 150,
          'oreo' => 175,
          'graham cracker' => 75,
          'vanilla wafer' => 60);

   return (exists($flav2cal{$lcFlav}) ? $flav2cal{$lcFlav} : 0);
}