title: A CFML variable name cannot end with a "." character error
body: <p>I get this error when posting a form. The strangest thing, though, is that this error only occurs in Chrome and Safari. FF, IE and Opera all post the form without a problem.</p>
  
  <p>The Stack Trace does not point to a file to where this error occurs. A cfdump of cfcatch gives me some insight as to what the problem is, but I can't find any instance of where the problem actually exists. Here's the partial dump:</p>
  
  <pre>
  Column     1
  Detail     The variable attributes. ends with a "." character. You must supply an additional structure key or delete the "." character.
  KnownColumn  -1
  KnownLine   -1
  KnownText   "unknown"
  Line         1
  Message   A CFML variable name cannot end with a "." character.
  </pre>
  
  <p>Here's the code handling the posted data. Everything is wrapped inside cftransaction and there's a loop, not sure why it's not being displayed. (thanks Peter Boughton for clearing that up)</p>
  
  <pre><code>&lt;!--- Delete out the old category ties ---&gt;
  &lt;cfquery name="deleteCategory" datasource="#request.dsnWrite#"&gt;
  DELETE FROM
      ProductListings_categories
  WHERE
      listingID = &lt;cfqueryparam value="#attributes.listingID#"&gt;
  &lt;/cfquery&gt;
  
  &lt;!--- Loop through the list ---&gt;
  &lt;cfloop list="#attributes.taginput#" index="idx" delimiters="."&gt;
  
  &lt;!--- check to see if tag exists ---&gt;
      &lt;cfquery name="checkTag" datasource="#request.dsnWrite#"&gt;
      SELECT
          categoryID
      FROM
          categories
      WHERE
          CategoryName = &lt;cfqueryparam value="#idx#" cfsqltype="cf_sql_varchar"&gt;
      &lt;/cfquery&gt;
  
      &lt;!--- If it does not then add the tag ---&gt;
      &lt;cfif not(checkTag.recordCount)&gt;
          &lt;cfquery name="insertTag" datasource="#request.dsnWrite#"&gt;
          INSERT into Categories
          (
          categoryname,
          dateCreated
          )
          VALUES
          (
          &lt;cfqueryparam value="#idx#" cfsqltype="cf_sql_varchar"&gt;,
          &lt;cfqueryparam value="#now()#" cfsqltype="cf_sql_timestamp"&gt;
          )
          &lt;/cfquery&gt;
  
          &lt;cfquery name="insertTag" datasource="#request.dsnWrite#"&gt;
          SELECT
              LAST_INSERT_ID() as newID
          FROM
              Categories
          &lt;/cfquery&gt;
  
          &lt;cfset variables.categoryID = insertTag.newID&gt;
      &lt;cfelse&gt;
          &lt;cfset variables.categoryID = checkTag.categoryID&gt;
      &lt;/cfif&gt;
  
      &lt;cftry&gt;
          &lt;!--- Tie the tag to the listing ---&gt;
          &lt;cfquery name="insertCategory" datasource="#request.dsnWrite#"&gt;
          INSERT into ProductListings_categories
          (
          listingID,
          CategoryID
          )
          VALUES
          (
          &lt;cfqueryparam value="#attributes.listingID#" cfsqltype="cf_sql_bigint"&gt;,
          &lt;cfqueryparam value="#variables.categoryID#" cfsqltype="cf_sql_bigint"&gt;
          )
          &lt;/cfquery&gt;
          &lt;cfcatch&gt;&lt;/cfcatch&gt;
      &lt;/cftry&gt;
  &lt;/cfloop&gt;
  
  &lt;cflocation url="/sell/add_listing/step/3/listingID/#attributes.listingID#" addtoken="false"&gt;
  </code></pre>
  
  <p>Any insight would be great. Thanks!</p>
  
  <p>Here's the form and the Javascript. I haven't had a chance to rewrite the code by the previous developer (up until this point, it was working, so there was no need to visit the code in the first place), but CFFORM isn't used, nor are other CF form items. Various JS functions are used for AJAX calls and are included as well.</p>
  
  <pre><code>&lt;form action="/sell/add_listing/step/2/listingID/#attributes.listingId#" method="post"&gt;
      &lt;div id="formFields"&gt;&lt;input name="tagInput" id="tagInput" value="#variables.tagInput#" type="hidden"/&gt;&lt;/div&gt;
  
      &lt;h3&gt;Step 2: &lt;span id="instructions"&gt;First, choose a top-level category&lt;/span&gt;&lt;/h3&gt;
      &lt;p id="instructions2"&gt;This category will show up as the first tag on your listing.&lt;/p&gt;
  
      &lt;div id="tagLand"&gt;
          &lt;div&gt;
              1. &lt;select onchange="mainCategorySelector(this.value)"&gt;
                  &lt;cfloop query="getTopCats"&gt;
                  &lt;option value="#getTopCats.categoryName#" &lt;cfif ListFirst(variables.tagInput,".") EQ getTopCats.categoryName&gt;selected="selected"&lt;/cfif&gt;&gt;#capFirstTitle(ReplaceNoCase(getTopCats.categoryName, "_"," ", "all"))#&lt;/option&gt;
                  &lt;/cfloop&gt;
              &lt;/select&gt;
          &lt;/div&gt;
  
          &lt;div id="inputDiv" style="visibility: hidden;"&gt;
              &lt;div&gt;Add a tag&lt;/div&gt;
  
              &lt;div&gt;2.
                  &lt;input type="text" onkeypress="return disableEnterKey(event)" name="newTag" id="newTag" maxlength="18"/&gt;
                  &lt;input type="button" value="add" onclick="addTag(document.getElementById('newTag').value)" class="small_button" /&gt;
              &lt;/div&gt;
  
              &lt;div class="error"&gt;&lt;/div&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  
      &lt;a href="/sell/add_listing/step/1/listingID/#attributes.listingId#"&gt;&lt;img src="/resources/img/layoutV3/button_prev.gif" alt="prev"/&gt;&lt;/a&gt;
      &lt;input type="image" name="btnSubmit" src="/resources/img/layoutV3/button_next.gif" /&gt;
  &lt;/form&gt;
  
  &lt;script src="/resources/js/listing_2.js" type="text/javascript"&gt;&lt;/script&gt;
  
  //some variables
  var listCount=1;
  var tagLimit=14;
  var maxSuggestions=100;
  var allTags=new Array();
  var allTags=new Array();
  var allTagPointers=new Array();
  var currentTags=0;
  
  // XML document
  var xmlDoc;
  var req;
  
  //this function will run anything upon page load
  function addLoadEvent(func)
  {
      var oldonload = window.onload;
      if (typeof window.onload != 'function')
      {
          if(func)window.onload = func;
      }
      else
      {
          window.onload = function() 
          {
              oldonload();
              func();
          }
      }
  }
  
  //let's rebuild the page!
  addLoadEvent(rebuildTags());
  
  function rebuildTags()
  {
      //grab the tag tree left for us by PHP
      var passedTags=document.getElementById('tagInput').value;
  
      //only run if we got a value
      if(passedTags.replace(/^\s+|\s+$/g, ""))
      {
          //split the string into an array
          passedTags=passedTags.split(".");
  
          //run functions to rebuild the world
          mainCategorySelector(passedTags[0]);
          for(var i=1;i&lt;passedTags.length;i++)
          {
              addTag(passedTags[i]);
          }
      }
  }
  
  function addTag(tagName)
  {
      tagName=trim(tagName);
      tagName=tagName.toLowerCase();
      if(tagName)
      {
          //remove underscores from tags, replace with spaces so we can validate
          tagName=tagName.replace(/_/g," ");
  
          //clear out error message if it's there
          var errorDiv=document.getElementById('errorDiv');
          errorDiv.innerHTML="";
  
          //only run if we're not at the limit and tag has not been used already
          if(currentTags&lt;=tagLimit &amp;&amp; !getArrayIndex(allTags,tagName))
          {
              //if not alphanumeric, error
              var myRegxp = /^[0-9a-zA-Z\s]*$/;
              if(myRegxp.test(tagName)==false)
              {
                  var errorDiv=document.getElementById('errorDiv');
                  errorDiv.innerHTML="You may only use letters and numbers in your tags.";
              }
              //if it error checks fine, move on
              else
              {
                  //let's replace all spaces with underscores for DB storage
                  //tagName=tagName.replace(/ /g,"_");
  
                  //query server and get list of related tags
  
                  //random number to kill the cache
                  var cacheKiller=Math.random();
                  //get all children tags
                  xmlDoc=ajaxRequest("/sell/get_categories_xml/tag/"+tagName.replace(/ /g,"_")+"/random/"+cacheKiller);
                  relatedTags=new Array;
  
                  var root=xmlDoc.getElementsByTagName('root')[0];
                  var tags=root.getElementsByTagName('tag');
  
                  //now get all sibling tags
                  xmlDoc=ajaxRequest("/sell/get_categories_siblings_xml/tag/"+tagName.replace(/ /g,"_")+"/random/"+cacheKiller);
                  root=xmlDoc.getElementsByTagName('root')[0];
  
                  var siblingTags=root.getElementsByTagName('tag');
  
                  //first compile child tags into an array
                  for(var i=0;(i&lt;tags.length &amp;&amp; i&lt;maxSuggestions);i++)
                  {
                      relatedTags[i]=tags[i].firstChild.nodeValue;
                  }
  
                  //now add sibling tags to the same array
                  tags=root.getElementsByTagName('tag');
                  for(i;(i&lt;tags.length &amp;&amp; i&lt;maxSuggestions);i++)
                  {    
                      relatedTags[i]=tags[i].firstChild.nodeValue;
                  }
  
  
                  var tagLand=document.getElementById('tagLand');
                  var newNumberDiv=document.createElement('div');
                  var newDiv=document.createElement('div');
  
                  //add to counter and master tag array
                  listCount++;
                  allTags[allTags.length]=tagName.replace(/ /g,"_");
                  allTagPointers[allTagPointers.length]=listCount;
                  updateForm();
  
                  newNumberDiv.setAttribute('id','number_'+listCount);
                  newNumberDiv.className='listing_number';
                  newNumberDiv.innerHTML=listCount+".";
  
                  newDiv.innerHTML=tagName+' &lt;span  onclick="removeTag(\''+listCount+'\');" class="list_dynamic_link"&gt;x&lt;/span&gt;';
                  newDiv.className='list_tag';
  
                  var newReccomendDiv=makeRelatedDiv(relatedTags);
  
                  //let's give IDs to all of the new divs so we can't kill 'em later
                  newDiv.setAttribute('id','tagDiv'+listCount);
                  newReccomendDiv.setAttribute('id','reccomendDiv'+listCount);
  
                  //add new divs to the master list
                  tagLand.appendChild(newNumberDiv);
                  tagLand.appendChild(newDiv);
                  tagLand.appendChild(newReccomendDiv);
  
                  //remove and re-append the input div to keep it at the end
                  var inputDiv=document.getElementById('inputDiv');
                  tagLand.removeChild(inputDiv);
                  tagLand.appendChild(inputDiv);
  
  
                  //make the inputDiv visible if it is not already
                  inputDiv.style.visibility='visible';
  
                  //run the reorderizer 
                  reorderizer();
  
                  //clear input field
                  document.getElementById('newTag').value="";
  
                  document.getElementById('newTag').focus();
              }
          }
      }
  }
  
  
  //removes a tag from the list -- called through the "x" link on each tag
  function removeTag(tagNumber)
  {
      //get master div
      var tagLand=document.getElementById('tagLand');
  
      //get reference to all three divs that make up a tag listing
      var deathRowNumberDiv=document.getElementById('number_'+tagNumber);
      var deathRowTagDiv=document.getElementById('tagDiv'+tagNumber);
      var deathRowReccomendDiv=document.getElementById('reccomendDiv'+tagNumber);
  
      //any last words, boys?
      tagLand.removeChild(deathRowNumberDiv);
      tagLand.removeChild(deathRowTagDiv);
      tagLand.removeChild(deathRowReccomendDiv);
  
      //find where we are in the master array
      var tagIndex=getArrayIndex(allTagPointers,tagNumber);
      //splice this tag out of master tag array
      allTags.splice(tagIndex,1);
      allTagPointers.splice(tagIndex,1);
      updateForm();
  
      //alert(allTags.join("."));
  
      //since we just changed the page structure, let's run reorderizer
      //run the reorderizer 
      reorderizer();
  
      //make the inputDiv visible if we're below the tag limit
      var inputDiv=document.getElementById('inputDiv');
      if(currentTags&lt;=tagLimit)
      {
          inputDiv.style.visibility='visible';
      }
  }
  
  
  //this function displays the formatted div for related tags
  function makeRelatedDiv(relatedTags)
  {
      //let's prepare the recommended tags div
      var newReccomendDiv=document.createElement('div');
      newReccomendDiv.className='list_suggested_tags';
      newReccomendDiv.innerHTML='&lt;span&gt;Add related tags: &lt;/span&gt; ';
      var numTags=0;
  
      //loop through suggested tag array
      for ( keyVar in relatedTags )
      {
          //add comma if necessary
          if(numTags)
          {
              newReccomendDiv.innerHTML+=", ";
          }
          newReccomendDiv.innerHTML+='&lt;span  onclick="addTag(\''+relatedTags[keyVar]+'\');" class="list_dynamic_link"&gt;'+relatedTags[keyVar]+'&lt;/span&gt;';
          numTags++;
      }
  
      return newReccomendDiv;
  }
  
  
  function mainCategorySelector(tag)
  {
      //only run if we're not the dead selection
      if(tag!="- - -")
      {
          //query server and get list of related tags
  
          //random number to kill the cache
          var cacheKiller=Math.random();
          xmlDoc=ajaxRequest("/sell/get_categories_xml/tag/"+tag+"/random/"+cacheKiller);
  
          relatedTags=new Array;
  
          var root=xmlDoc.getElementsByTagName('root')[0];
          var tags=root.getElementsByTagName('tag');
  
          for(var i=0;(i&lt;tags.length &amp;&amp; i&lt;maxSuggestions);i++)
          {
              relatedTags[i]=tags[i].firstChild.nodeValue;
          }
  
          var tagLand=document.getElementById('tagLand');
  
          var newReccomendDiv=makeRelatedDiv(relatedTags);
  
          //replace old reccomend list if it exists
          if(document.getElementById('mainCategoryReccomendations'))
          {
              var mainCategoryReccomendations=document.getElementById('mainCategoryReccomendations');
              tagLand.appendChild(newReccomendDiv);
              tagLand.insertBefore(newReccomendDiv , mainCategoryReccomendations);
              tagLand.removeChild(mainCategoryReccomendations);
          }
          else
          {
              tagLand.appendChild(newReccomendDiv);
              //add to counter if we added a new tag
              listCount++;
          }
  
          newReccomendDiv.setAttribute('id' , 'mainCategoryReccomendations');
  
          //alert(allTags.join("."));
  
          //add master tag array    
          allTags[0]=tag;
          allTagPointers[0]=1;
          updateForm()
  
          //alert(allTags.join("."));
  
          //remove and re-append the input div to keep it at the end
          var inputDiv=document.getElementById('inputDiv');
          tagLand.removeChild(inputDiv);
          tagLand.appendChild(inputDiv);
  
          //make the inputDiv visible if we're below the tag limit
          if(currentTags&lt;=tagLimit)
          {
              inputDiv.style.visibility='visible';
  
              //focus on the new field
              document.getElementById('newTag').focus();
          }
  
          //change up the instructions
          changeInstructions("Now, add other tags to sort your listing","You can either click the related tags or enter your own")
      }
  }
  
  //this function changes the content of the instructional div
  function changeInstructions(top, bottom)
  {
      var instructions=document.getElementById('instructions');
      var instructions2=document.getElementById('instructions2');
      instructions.innerHTML=top;
      instructions2.innerHTML=bottom;
  }
  
  //this function reassigns all list numbers to their proper value
  function reorderizer()
  {
      /*
      Here we run through all number div IDs...
      remember, the div ID number may not match the display number, due to 
      additions/removals. That's why we have a separate variable for displayNumber!
      */
  
      var tagLand=document.getElementById('tagLand');
  
      //another counting var, for the actual display number
      var displayNumber=1;
  
      for(var i=1; i &lt;= listCount; i++)
      {
          if(document.getElementById('number_'+i))
          {
              var b=document.getElementById('number_'+i);
              b.innerHTML=displayNumber+".";
  
              //ony increment displayNumber if we've actually printed a number
              displayNumber++;
          }
      }
  
      //update global tag count to most current and accurate number
      currentTags=displayNumber;
  
      //have we hit the tag limit? If so, hidezorz input
      if(displayNumber&gt;tagLimit)
      {
          var inputDiv=document.getElementById('inputDiv');
          inputDiv.style.visibility='hidden';
      }
      else
      {
          //after looping through dynamic list entries, let's change the submit field's number too
          var number_last=document.getElementById('number_last');
          if(number_last)
          {
              number_last.innerHTML=displayNumber+".";
          }
      }
  }
  
  function pausecomp(millis) 
  {
      date = new Date();
      var curDate = null;
  
      do { var curDate = new Date(); } 
      while(curDate-date &lt; millis);
  }
  
  function ajaxRequest(requestURL)
  {
      var req;
      if (window.XMLHttpRequest) {
          req = new XMLHttpRequest();
      } else if (window.ActiveXObject) {
          req = new ActiveXObject("Microsoft.XMLHTTP");
      }
  
      req.open("GET", requestURL, false);
      req.send(null);
      var xmlDocument = req.responseXML;
      return(xmlDocument);
  }
  
  function disableEnterKey(e)
  {
      var key;
  
      if(window.event)
          key = window.event.keyCode;     //IE
      else
          key = e.which;     //firefox
  
      if(key == 13)
      {
          addTag(document.getElementById('newTag').value);
          return false;
      }
      else
      {
          return true;
      }
  }
  
  
  function getArrayIndex(arr, val) 
  {
      for (i = 0; i &lt; arr.length; i++) 
      {
          if (arr[i] == val) { return i; }
      }
  }
  
  function updateForm()
  {
      //this function updates the hidden field that will actually send the tag data upon form submission
      document.getElementById('tagInput').value=allTags.join(".");
  }
  </code></pre>