Showing posts with label ASP. Show all posts
Showing posts with label ASP. Show all posts

Convert Atom-formatted DateTime

The following function converts ATOM (RFC4287) formatted Date/Time string into a VBScript Date variable. You’ll find it useful if you want to process Atom feeds with VBScript/ASP.

Function AtomDateTime(S)
  Dim CompArray,ValueArray,DateVal,TimeString,TimeVal,ZoneSign,ZoneString,I

  CompArray = Split(S,"T")
  If UBound(CompArray) <> 1 Then _
    Err.raise vbObjectError,"AtomDateTime","No 'T' in Atom date/time"
  ValueArray = Split(CompArray(0),"-")
  If UBound(ValueArray) <> 2 Then _
    Err.raise vbObjectError,"AtomDateTime","Atom date part is not a date"
  DateVal = DateSerial(CInt(ValueArray(0)),CInt(ValueArray(1)),CInt(ValueArray(2)))

  TimeString = CompArray(1) : I = InStr(TimeString,"Z") 
  If I > 0 Then
    TimeString = Left(TimeString,I-1)
    TimeVal = GetAtomTime(TimeString)
  Else
    I = InStr(TimeString,"-")
    If I > 0 Then 
      ZoneSign = 1 
    Else
      ZoneSign = -1 : I = InStr(TimeString,"+")
    End If
    If I = 0 Then _
      Err.raise vbObjectError,"AtomDateTime","cannot recognize timezone separator"
    ZoneString = Mid(TimeString,I+1,Len(TimeString))
    TimeString = Left(TimeString,I-1)
    TimeVal = GetAtomTime(TimeString) + ZoneSign * GetAtomTime(ZoneString)
  End If
  AtomDateTime = DateVal + TimeVal
End Function 

The DateTime is converted into GMT time value. You might need to add/subtract your local timezone offset if you want to get local time.

The GetAtomTime function converts the time portion of the DateTime field:

Function GetAtomTime(TS) 
  Dim I : I = InStr(TS,".") : If I > 0 Then TS = Left(TS,I-1)
  GetAtomTime = Timevalue(TS)
End Function

Login to Google Data API

Google Data API has numerous client-side libraries, but not an ASP/VBScript one. Here’s the first routine you need: it authenticates you with the Google API and returns a handle you can use in subsequent calls.

The HTTPCheck is a function that checks the status code before returning to the caller. You could replace if with XRQ.status < 300. You'll find the EncodeFormData function in a previous post.

Dim XRQ ' global XMLHttpRequest object

Function GoogleLogin(UN,Pwd,Svc)
  Dim RQData,I,RT

  GoogleLogin = Null
  RQData = EncodeFormData(Array("Email","Passwd","service","source"), _
    Array(UN,Pwd,Svc,"VBScript-BloggerLibrary-0.1"))

  If Not IsObject(XRQ) Then Set XRQ = CreateObject("Microsoft.XMLHTTP")
  XRQ.open "POST","https://www.google.com/accounts/ClientLogin",false
  XRQ.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
  XRQ.send RQData
  If HTTPCheck(XRQ,"Google login response", _
    "https://www.google.com/accounts/ClientLogin") Then
    RT = XRQ.responseText
    I = InStr(RT,"Auth=")
    If I = 0 Then wr "Google response has no AUTH data" : Exit Function
    RT = Mid(RT,I+5,Len(RT))
    I = InStr(RT,Chr(10)) : If I > 0 Then RT = Left(RT,I-1)
    GoogleLogin = RT
  End If
End Function

Generate HTML form data in VBscript

Sometimes you have to post form data in an AJAX call started from server-side ASP script (or WSH script). The following VBScript functions will return well-formed form data that you can use with the application/x-www-form-urlencoded MIME type:

Function URLEncoded(V) ' … private
  Dim CC,I,Enc
  Enc = ""

  For I = 1 To Len(V)
    CC = AscW(Mid(V,I,1))
    If (CC >= 48 And CC < 58) Or (CC >= 64 And CC < 90) Or (CC >= 97 And CC < 124) Or CC = 95 Or CC = 45 Or CC = 46 Then
      Enc = Enc & Mid(V,I,1)
    Else
      Enc = Enc & "%" & Hex(CC)
    End If
  Next
  URLEncoded = Enc
End Function

Function CreateFormField(F,V) ' … private
  CreateFormField = F & "=" & URLEncoded(CStr(V))
End Function  

'
' EncodeFormData: Return form data for a POST request
'
' Input: N – array of form field names
'        V – array of form field values
'
' Return: encoded form data
'
Function EncodeFormData(N,V)
  Dim I
  For I = LBound(N) To UBound(N)
    If I <> LBound(N) Then EncodeFormData = EncodeFormData & "&"
    EncodeFormData = EncodeFormData & CreateFormField(N(I),V(I))
  Next
End Function

XSLT transformation in ASP: non-standard XML encoding

If you want to generate documents with non-standard encodings with server-side XSLT transformation in IIS/ASP environment, you should:

  • Set the Response.Charset property to the desired character set;
  • Set the Response.Codepage property to the desired code page (or use the <%@ LANGUAGE="VBScript" CODEPAGE="codepage" %> directive in your ASP script).
  • Omit XML declaration from the translated text (using omit-xml-declaration=”yes” attribute in xsl:output element) and prepend the desired XML declaration in front of the translated text.

For example, the following program generates XML (or XHTML) document encoded in windows-1250 character set (codepage: 1250) …

<%
Const DOMClass = "MSXML2.DOMDocument"
Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)
XDoc.loadXML("<root greek='&#946;' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))
Response.Clear
Response.Charset = "windows-1250"
Response.Codepage = 1250
Response.ContentType = "text/xml"
Response.Write "<?xml version='1.0' encoding='windows-1250'?>"
Response.Write XDoc.transformNode(XSLT)
%>

… when using the following XSL transformation:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" />
… rest deleted …

XSLT transformation in ASP: XML with MSXML6

Similar to the XML-to-HTML transformation with MSXML6, the results of the transformNode function do not include the encoding information; the XML text produced by the sample program with the XSL transformation described in the previous post is a perfect XML document:

<?xml version="1.0"?>
<output>
  Greek letter: β 
  EE: č
</output>

An XML document without the encoding attribute in the xml declaration is assumed to be encoded in UTF-8. The results of the transformNode function in MSXML6 are thus absolutely correct if you use UTF-8 output encoding.

XSLT transformation in ASP: XML with MSXML3

You might consider server-side XML-to-XML transformations rare, but you have to use <xsl:output method=”xml”> if you want to generate valid XHTML code from your XML data. To shorten the printouts, we’ll use a simple (non-XHTML) XSL transformation to generate the test results:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" />

<xsl:template match="root">
  <output>
    Greek letter: <xsl:value-of select="@greek" /> 
    EE: <xsl:value-of select="@ee" />
  </output>
</xsl:template> 

</xsl:stylesheet>

As with HTML, MSXML3 interferes with the output, inserting improper UTF-16 encoding directive, resulting in the following XML text:

<?xml version="1.0" encoding="UTF-16"?>
<output>
  Greek letter: β 
  EE: č
</output>

You could bypass this bug by setting the omit-xml-declaration attribute of the xsl:output element to yes …

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" omit-xml-declaration="yes" />

… rest deleted …

… resulting in the following transformation output:

<output>
  Greek letter: β 
  EE: č
</output>

However, if you want to retain XML declaration in the XML document, you have to replace UTF-16 in the output string with UTF-8, like we did in the HTML transformation case. The following modified test program produces perfect XML document when used with the original XSLT transformation (without the omit-xml-declaration attribute):

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write Replace(XDoc.transformNode(XSLT), _
  "encoding=""UTF-16""","encoding=""utf-8""")
%>

XSLT transformation in ASP: HTML with MSXML6

If your IIS platform includes MSXML6, you should use MSXML6 to transform XML into HTML instead of MSXML3 (see the Using the right version of MSXML article for proper fallback process). MSXML6 still generates the META tag (the MSXML3-related post describes problems caused by the META tag), but does not include the charset parameter in it, resulting in perfect HTML output. The sample ASP program using the simple XSLT transformation from the “XSLT transformation in ASP: HTML with MSXML3” post produces the following output when using MSXML6 (MSXML2.DomDocument.6.0 ProgID):

<html>
<head>
<META http-equiv="Content-Type" content="text/html">
<title>Sample XSLT server-side transformation</title>
</head>
<body><node type="test">Greek letter: β EE: č</node></body>
</html>

If you want to use complex XSLT transformations with MSXML6, you might have to set additional second-level DOM properties, for example AllowDocumentFunction. You might also want to set ValidateOnParse to false if the validity of your XML document is not a major concern.

XSLT transformation in ASP: HTML with MSXML3

Most commonly, you’ll use server-side XSLT transformation in ASP to transform XML data into HTML using MSXML3 (available on almost all IIS platforms). The following XSLT stylesheet is used in the HTML test:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="html" encoding="utf-8" />

<xsl:template match="root">
  <html>
    <head>
      <title>Sample XSLT server-side transformation</title>
    </head>
    <body>
      <node type="test">
        Greek letter: <xsl:value-of select="@greek" /> 
        EE: <xsl:value-of select="@ee" />
      </node>
    </body>
  </html>
</xsl:template> 

</xsl:stylesheet>

With MSXML3, the transformNode function inserts a META directive in the output stream, resulting in the following transformation result:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-16">
<title>Sample XSLT server-side transformation</title>
</head>
<body><node type="test">
Greek letter: β 
EE: č
</node>
</body>
</html>

On the other hand, the HTTP headers generated by the test ASP script claim the content is UTF-8 encoded (and the raw data dump performed with Fiddler confirms that). The response headers were taken from LiveHTTPHeaders Firefox extension:

HTTP/1.x 200 OK
Server: Microsoft-IIS/5.1
Date: Mon, 15 Sep 2008 11:20:56 GMT
X-Powered-By: ASP.NET
Content-Length: 222
Content-Type: text/html; Charset=utf-8
Cache-Control: private

Most browsers (including IE and Firefox) take the Content-type from HTTP headers, using the META directive as a fallback. They thus render the page as intended. Some browsers (including the TextView tab of Fiddler) prefer the META directive and produce garbage.

The solution

To match the META header with the HTTP headers, replace the charset=UTF-16 string in the text returned by the transformNode function with charset=UTF-8. The modified sample ASP script is included below:

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write Replace(XDoc.transformNode(XSLT),"charset=UTF-16","charset=UTF-8")
%>

XSLT transformation in ASP: The testbed

As I’ve mentioned in the previous post, MSXML functions called from ASP believe they work in UTF-16 environment. The transformNode function might insert this information in the output string based on several parameters, one of them being the version of the MSXML ActiveX control. To test behavior of various versions of MSXML, we’ll use the following test program:

<%
Const DOMClass = "MSXML2.DOMDocument"

Set XSLT = Server.CreateObject(DOMClass)
Set XDoc = Server.CreateObject(DOMClass)

XDoc.loadXML("<root greek='β' ee='č' />")
XSLT.load(Server.MapPath("SampleXSLT.xsl"))

Response.Clear
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write XDoc.transformNode(XSLT)
%>

And a stylesheet similar to this one:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
<xsl:output method="xml" encoding="utf-8" />

<xsl:template match="root">
  <output>
    <node type="test">
      Greek letter: <xsl:value-of select="@greek" /> 
      EE: <xsl:value-of select="@ee" />
    </node>
  </output>
</xsl:template> 

</xsl:stylesheet>

We’ll test two MSXML versions: MSXML3 (default with Windows XP) and MSXML6 (default with Vista, also available on Windows XP). MSXML3 should be available on almost all web servers, you might not get MSXML6 everywhere (my hosting provider did not offer it when it mattered most to me).

The test results will be published in the next few posts.

Further reading:

XSLT transformation in ASP: the principles

If you want to perform server-based XSLT transformations on IIS using classic ASP, you should keep in mind the following facts:

  • ASP uses 16-bit Unicode characters internally. MSXML ActiveX modules called from ASP therefore think they work in UTF-16 environment.
  • Due to the impression that the XSLT transformations happen in UTF-16 environment, MSXML procedures might prepend UTF-16 specific headers in transformed text (more about that in follow-up posts).
  • The transformed text returned to ASP from transformNode function is UTF-16 encoded.
  • When the ASP script sends the text to the browser (or writes it into a text file), the codepage set with Response.Codepage is used to transform UTF-16 into target character set.

Further reading:

Make Your Web Pages Mobile-Friendly

A while ago I had to develop a few mobile-oriented applications, essentially serving existing content on tiny screen resolutions. As one could expect, I approached it from the XML/XSLT perspective and designed a few XSLT stylesheets to render existing XML content (all of my recent content is in XML format) in a more mobile-friendly format.

At approximately the same time I was working a lot with Blogger APIs and Atom ... and you can see the results of these seemingly unrelated activities in my new InformIT article “Make Your Web Pages Mobile-Friendly” where I'm describing how you can take an existing Atom feed and wrap it into a mobile-friendly user interface.

UTF-8 text file generated from ASP

In the previous post, I've described how to output plain text encoded as UTF-8 from an ASP script. To save that text to a file on the client's disk drive, you can add the Content-disposition header, indicating the ASP script response is actually an attachment that has to be saved to disk. However, some Windows programs expect to see the Byte-order mark in front of the UTF-8 text. To generate this byte stream, I've used a simple trick: first I've switched the codepage to US-ASCII (ensuring there will be no character transcoding), wrote the three byte sequence and switched back to UTF-8 to write the rest of the text.
Sub OutputCSV(finalText)
  Response.Clear
  Response.Charset = "utf-8"
  Response.ContentType = "text/plain"
  Response.AddHeader "Content-disposition", "attachment; filename=savedData.csv"
  Response.Expires = 0
  Response.Codepage = 1252
  Response.Write Chr(239) & Chr(187) & Chr(191)
  Response.Codepage = 65001
  Response.Write finalText
  Response.End
End Sub

Output UTF-8 text from ASP script

I had to write an application that outputs CSV text encoded as UTF-8 from an ASP script. This is what I've ended with:
Sub OutputCSV(finalText)
  Response.Clear
  Response.Charset = "utf-8"
  Response.ContentType = "text/plain"
  Response.Codepage = 65001
  Response.Write finalText
  Response.End
End Sub
In my application, the complete CSV data was stored in the finalText variable. If that's not the case, replace the Response.Write statement with the CSV-generating logic.

Max function in ASP

The VBScript (at least its ASP dialect) is not very strong in math functions. For example, the simple Max function is missing. It's not hard to write, though:
Function Max(A,B)
  Max = A : If B > A Then Max = B
End Function
Things get a bit more complex if you want to take in account the NULL values (assuming non-NULL is always greater than NULL):
Function Max(A,B)
  Max = A : If IsNull(B) Then Exit Function
  If B > A Or IsNull(A) Then Max = B
End Function
However, I prefer the multi-value function that can take as many arguments as you like (unfortunately in an array, as VBScript functions can't take variable number of arguments):
Function MaxArray(A)
  Dim I
  MaxArray = A(LBound(A))
  For I = LBound(A) + 1 To UBound(A)
    If A(I) > MaxArray Or IsNull(MaxArray) Then MaxArray = A(I)
  Next
End Function

The code uses a VBScript quirk: function name without parenthesis when used within the function refers to the current function's value, not a recursive call

To call the MaxArray function, you don't have to construct an array beforehand, you can also use the array VBScript function, for example:

mxv = MaxArray(array(A,B,C,D))

IP address to physical location mapping

On every web site where I can configure the web server parameters (sadly not available in all hosting environments), I catch the ASP errors (500;100 error page), acknowledge them to the web user and report them in an e-mail to the webmaster (if you can't debug every possible code path, it makes sense to fix the bugs once the first visitor hits them).

Sometimes I would also like to figure out where the visitor causing the ASP error is coming from (more so if the errors are due to lack of resources, indicating unexpectedly high load). The usual tools (nslookup) fail if the ISPs don't enter their IP address space in reverse DNS lookup zones. In the past I've tried to use various Internet registries to figure out who the address is allocated to … and finally I've stumbled on the IP2Location web site, which does exactly what I was looking for: I enter the IP address and it tells me where the visitor is coming from (including a nice overview map for those who have no clue about geography).

How do you write utf-8 data from ASP?

Obviously this is too simple, so it's hard to find an explicit answer in the online documentation:

  • To select the output encoding you want to use in the ASP script, you have to set the response.codepage property or change the per-page default with the @Codepage directive. To use utf-8 encoding, set the response.codepage to 65001.
  • The HTTP Content-type header should match the encoding you're using. Set the response.charset (second part of the content-type header) to "utf-8".
  • If you use META tag in your HTML to set the content-type or encoding attribute in the xml pseudo-instruction, these have to match the HTTP encoding as well. You don't have to specify utf-8 encoding in XML, as it's the default XML encoding.
  • Instead of setting response.codepage in every response, you can set session.codepage at the start of the session.
  • Important: If you use HTML forms, you have to set the @Codepage in all ASP scripts processing the forms or change the AspCodepage metabase property, otherwise the script might misinterpret the input data.

This post is part of You've asked for it series of articles.

Output XML data from ASP

These are the steps you need to take to output XML data from ASP script:

  • Clear the output buffer to ensure no HTML tags are sent to the client;
  • Set the content-type to text/xml;
  • Optionally (but highly recommended) set the character set to utf-8 and change the ASP code page;
  • Output the XML data (for example, writing a DOM object xml property to the output stream);
  • Stop ASP script processing.

The sample ASP code is included below and you can get more in-depth description of the necessary operations in my InformIT article Optimized presentation of XML content.

Response.Clear
Response.ContentType = contentType
Response.Charset = "utf-8"
Response.Codepage = 65001
Response.Write txt
Response.End

Note: this post is part of You've asked for it series of articles.

Detect included libraries in ASP

My ASP applications rely a lot on included libraries. As I usually end up with a complex hierarchy of libraries, it's not always possible to include the required modules in the dependent library. This makes debugging a bit more interesting, as you cannot be sure all the prerequisites have been included in the main .asp page unless you test all code paths.

To work around this problem, I wrote a small routine that checks for existence of prerequisite functions and throws an exception documenting what went wrong.
'
' CheckIncludedLibrary - checks if the incRoutine is available,
' otherwise throws an exception documenting that srcLibrary
' needs incLibrary
'
Sub CheckIncludedLibrary(incRoutine,srcLibrary,incLibrary)
Dim routineRef

On Error Resume Next
Set routineRef = GetRef(incRoutine)
If Err.Number <> 0 Then On Error Goto 0 : _
Err.Raise vbObjectError+1,srcLibrary, _
"You have to include " & incLibrary
End Sub

You would use this routine in a way similar to the example below:
'
' This library requires inclusion of /forms/xmlLibrary.asp,
' /forms/lib/editLibrary.asp and /asp/incPostingPreview.asp
'

CheckIncludedLibrary "NewXMLTextElement", _
"listRoutesLibrary","/forms/xmlLibrary"
CheckIncludedLibrary "EnrichElementText", _
"listRoutesLibrary","/forms/lib/editLibrary"
CheckIncludedLibrary "PostToPreview", _
"listRoutesLibrary","/asp/incPostingPreview"

MSXML firstChild property in VBScript

To complement a bit vague Microsoft MSDN document: firstChild property returns nothing (not null) if the XML node has no children. To test whether an XML node has children, use:
If Not (node.firstChild Is Nothing) Then ...
or, alternatively
If node.childNodes.length > 0 Then ...

Setting text property of MSXML Node object destroys its children

The Microsoft's documentation of the IXMLDOMNode.text property skips an important detail: if the node has non-text children and you set its text property, it loses all children. The node.text = string statement deletes all children and creates a single IXMLDOMText child.