<% ' Copyright (c) 2003 soup-and-croutons@agresticism.org ' Licensed under the MIT license: ' http://www.opensource.org/licenses/mit-license.php ' This file is utterly hideous. Really. Enter at your own risk. Dim RSS Set RSS = New RSSClass Class RSSClass ' Container class/namespace. Public XMLNS, XMLNSRDF Public FeedLink, RestrictLengths, UTCOffset, ConditionalGet Public Channel, Image, Item, TextInput Public CC Public Document Private Sub Class_Initialize() XMLNS = "http://purl.org/rss/1.0/" XMLNSRDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" FeedLink = "" RestrictLengths = False UTCOffset = 0 ConditionalGet = False Set Channel = New ChannelClass Set Image = New ImageClass Set Item = New ItemClass Set TextInput = New TextInputClass Set CC = New CreativeCommonsClass Set Document = Server.CreateObject("Microsoft.XMLDOM") ' Add the RDF element so everything else can be done in the wrong order. Document.insertBefore Document.createNode(1, "rdf:RDF", XMLNSRDF), Document.lastChild ' Add namespaces to the RDF element. Originally this was done conditionally, but that got real hairy real fast. I don't think superfluous namespaces are that much of a problem. AddAttribute "rdf:RDF", "xmlns:wfw", "http://wellformedweb.org/CommentAPI/" AddAttribute "rdf:RDF", "xmlns:dc", Channel.DC.XMLNS AddAttribute "rdf:RDF", "xmlns:sy", Channel.Sy.XMLNS AddAttribute "rdf:RDF", "xmlns:admin", Channel.Admin.XMLNS AddAttribute "rdf:RDF", "xmlns:cc", Channel.CC.XMLNS AddAttribute "rdf:RDF", "xmlns:content", "http://purl.org/rss/1.0/modules/content/" AddAttribute "rdf:RDF", "xmlns", XMLNS End Sub Private Sub Class_Terminate() Set Channel = Nothing Set Image = Nothing Set Item = Nothing Set TextInput = Nothing Set CC = Nothing Set Document = Nothing End Sub ' Subs to make adding and manipulating elements take less typing. Public Sub AddAttribute(Element, Name, Value) Call Document.selectSingleNode(Element).setAttribute(Name, Value) End Sub Public Sub AddElement(Parent, Name, NameSpace) Call Document.selectSingleNode(Parent).appendChild(Document.createNode(1, Name, NameSpace)) End Sub Public Sub AddText(Element, Value) Call Document.selectSingleNode(Element).appendChild(Document.createTextNode(Value)) End Sub Public Function Write() Channel.AddToFeed ' I can't remember why I put this last, but I'm sure I had a reason. It still works, anyway. Document.insertBefore Document.createProcessingInstruction("xml", " version=""1.0"" encoding=""utf-8"""), Document.firstChild Write = Document.XML End Function End Class Class ChannelClass ' The channel element. Public Title, Link, Description, Image, Items, TextInput Public ItemCount Public DC, Sy, Admin,CC Private Sub Class_Initialize() Title = "" Link = "" Description = "" Image = "" Items = Array() TextInput = "" ItemCount = 0 Set DC = New DublinCoreClass Set CC = New CreativeCommonsClass Set Admin = New AdminClass Set Sy = New SyndicationClass End Sub Private Sub Class_Terminate() Set DC = Nothing Set CC = Nothing Set Admin = Nothing Set Sy = Nothing End Sub Public Sub PushItem(Link) ' Why don't VBScript arrays have an easier way of pushing items to them? ReDim Preserve Items(ItemCount + 1) ItemCount = UBound(Items) Items(ItemCount) = Link End Sub Public Sub AddToFeed() Dim I ' Validate input. Title = Model(Title, "PCDATA", 40) Link = Model(Link, "PCDATA", 500) Description = Model(Description, "PCDATA", 500) Image = Model(Image, "PCDATA", 500) TextInput = Model(TextInput, "PCDATA", 500) ItemCount = UBound(Items) With RSS ' Put this before the item elements which have gone before. This may not be necssary, but it can't hurt. .Document.documentElement.insertBefore RSS.Document.createElement("channel"), RSS.Document.selectSingleNode("rdf:RDF/item") ' Set up the main channel elements. .AddAttribute "rdf:RDF/channel", "rdf:about", RSS.FeedLink .AddElement "rdf:RDF/channel", "title", "" .AddText "rdf:RDF/channel/title", Title .AddElement "rdf:RDF/channel", "link", "" .AddText "rdf:RDF/channel/link", Link .AddElement "rdf:RDF/channel", "description", "" .AddText "rdf:RDF/channel/description", Description .AddElement "rdf:RDF/channel", "items", "" .AddElement "rdf:RDF/channel/items", "rdf:Seq", RSS.XMLNSRDF .AddElement "rdf:RDF/channel", "admin:generatorAgent", Admin.XMLNS .AddAttribute "rdf:RDF/channel/admin:generatorAgent", "rdf:resource", Admin.GeneratorAgent End With ' If we've an image, let's include it. If Len(Image) > 0 Then RSS.AddElement "rdf:RDF/channel", "image", "" RSS.AddAttribute "rdf:RDF/channel/image", "rdf:resource", Image RSS.Image.AddToFeed End If ' Ditto for a textinput. If Len(TextInput) > 0 Then RSS.AddElement "rdf:RDF/channel", "textinput", "" RSS.AddAttribute "rdf:RDF/channel/textinput", "rdf:resource", Image RSS.TextInput.AddToFeed End If ' Leave this in; I want people to be able to tell me about problems with Saltshaker. RSS.AddElement "rdf:RDF/channel", "admin:errorReportsTo", Admin.XMLNS RSS.AddAttribute "rdf:RDF/channel/admin:errorReportsTo", "rdf:resource", Admin.ErrorReportsTo ' Loop through the items in the array. For I = 0 To ItemCount ' I'm really bad at dealing with arrays, so this acts as a filter of sorts. If Len(Items(I)) > 0 Then Items(I) = Model(Items(I), "", 500) RSS.AddElement "rdf:RDF/channel/items/rdf:Seq", "rdf:li", RSS.XMLNSRDF ' Can't do this the usual way, need to specifically get the last element. If we don't do this it just replaces the resource attribute of the first element over and over again. ' Note to self: there's an XPath function called last(). Guess what it does. RSS.Document.selectSingleNode("rdf:RDF/channel/items/rdf:Seq").lastChild.setAttribute "rdf:resource", Items(I) End If Next ' Syndication. If Sy.Use Then Sy.AddToFeed ' This is a special case. mod_cc requires the license to be referenced in its channel/item/image/whatever, and also in rdf:RDF/cc:License. You'll see this in a couple of other places, too. If CC.Use Then RSS.AddElement "rdf:RDF/channel", "cc:license", CC.XMLNS RSS.AddAttribute "rdf:RDF/channel/cc:license", "rdf:resource", CC.License CC.PushLicense End If If CC.Use Then RSS.CC.AddToFeed ' Dublin Core's far simpler. If DC.Use Then DC.AddToFeed "rdf:RDF/channel" ' Reset properties. Class_Initialize End Sub End Class Class ImageClass ' Images are really simple, no explanation required really. Public Title, URL, LinksTo Public CC Private Sub Class_Initialize() Title = "" URL = "" LinksTo = "" Set CC = New CreativeCommonsClass End Sub Private Sub Class_Terminate() Set CC = Nothing End Sub Public Sub AddToFeed() Title = Model(Title, "PCDATA", 40) URL = Model(RSS.Channel.Image, "PCDATA", 500) LinksTo = Model(LinksTo, "PCDATA", 500) With RSS .AddElement "rdf:RDF", "image", "" .AddAttribute "rdf:RDF/image", "rdf:about", URL .AddElement "rdf:RDF/image", "title", "" .AddText "rdf:RDF/image/title", Title .AddElement "rdf:RDF/image", "link", "" .AddText "rdf:RDF/image/link", LinksTo .AddElement "rdf:RDF/image", "url", "" .AddText "rdf:RDF/image/url", URL End With If CC.Use Then RSS.AddElement "rdf:RDF/image", "cc:license", CC.XMLNS RSS.AddAttribute "rdf:RDF/image/cc:license", "rdf:resource", CC.License CC.PushLicense End If End Sub End Class Class ItemClass Public Title, Link, Description, Content, CommentURI Public DC, CC Private Sub Class_Initialize() Title = "" Link = "" Description = "" Content = "" CommentURI = "" Set DC = New DublinCoreClass Set CC = New CreativeCommonsClass End Sub Private Sub Class_Terminate() Set DC = Nothing Set CC = Nothing End Sub Public Sub AddToFeed() Dim LastModified, ETag, IfModifiedSince, IfNoneMatch Title = Model(Title, "PCDATA", 100) Link = Model(Link, "PCDATA", 500) Description = Model(Description, "PCDATA", 500) Content = Model(Content, "CDATA", 0) ' Do headers. If RSS.ConditionalGet And RSS.Channel.ItemCount = 1 Then LastModified = Model(DC.Date, "HTTP date", 0) ETag = """" & LastModified & """" IfModifiedSince = Request.ServerVariables("HTTP_IF_MODIFIED_SINCE") IfNoneMatch = Request.ServerVariables("HTTP_IF_NONE_MATCH") ' Check for If-Modified-Since and If-None-Match, both that they exist and that they are equal to our values. ' ...why bother? If they're equal to our values then /clearly/ they exist. Doi. If IfModifiedSince = LastModified And IfNoneMatch = ETag Then With Response .Clear .Status = "304 Not Modified" .Write(vbNewLine & vbNewLine) .End End With End If ' Send the headers, then, and a Content-Type. With Response .AddHeader "Last-Modified", LastModified .AddHeader "ETag", ETag .ContentType = "application/rss+xml" End With End If With RSS .Channel.PushItem Link .AddElement "rdf:RDF", "item", "" ' See the bits where we loop over the items in channel. .Document.selectSingleNode("rdf:RDF").lastChild.setAttribute "rdf:about", Link ' Smooth ride from here on in. Just have to make sure we get the right item back. .AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "title", "" .AddText "rdf:RDF/item[@rdf:about='" & Link & "']/title", Title .AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "link", "" .AddText "rdf:RDF/item[@rdf:about='" & Link & "']/link", Link .AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "description", "" .AddText "rdf:RDF/item[@rdf:about='" & Link & "']/description", Description End With ' Support for content:encoded... If Len(Content) > 0 Then RSS.AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "content:encoded", "http://purl.org/rss/1.0/modules/content/" RSS.Document.selectSingleNode("rdf:RDF/item[@rdf:about='" & Link & "']/content:encoded").appendChild(RSS.Document.createCDATASection(Content)) End If ' ...and CommentAPI auto-discovery... If Len(CommentURI) > 0 Then RSS.AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "wfw:comment", "http://wellformedweb.org/CommentAPI/" RSS.AddText "rdf:RDF/item[@rdf:about='" & Link & "']/wfw:comment", CommentURI End If ' ...and Creative Commons licensing... If CC.Use Then RSS.AddElement "rdf:RDF/item[@rdf:about='" & Link & "']", "cc:license", CC.XMLNS RSS.AddAttribute "rdf:RDF/item[@rdf:about='" & Link & "']/cc:license", "rdf:resource", CC.License CC.PushLicense End If ' ...and Dublin Core metadata. If DC.Use Then DC.AddToFeed "rdf:RDF/item[@rdf:about='" & Link & "']" Class_Initialize End Sub End Class Class TextInputClass ' Like image, this is really easy. Public Title, Description, Name, Link Private Sub Class_Initialize() Title = "" Description = "" Name = "" Link = "" End Sub Public Sub AddToFeed() Title = Model(Title, "PCDATA", 40) Description = Model(Description, "PCDATA", 100) Name = Model(Name, "PCDATA", 500) Link = Model(RSS.Channel.TextInput, "PCDATA", 500) With RSS .AddElement "rdf:RDF", "textinput", "" .AddAttribute "rdf:RDF/textinput", "rdf:about", Link .AddElement "rdf:RDF/textinput", "title", "" .AddText "rdf:RDF/textinput/title", Title .AddElement "rdf:RDF/textinput", "description", "" .AddText "rdf:RDF/textinput/description", Description .AddElement "rdf:RDF/textinput", "name", "" .AddText "rdf:RDF/textinput/name", Name .AddElement "rdf:RDF/textinput", "link", "" .AddText "rdf:RDF/textinput/link", Link End With End Sub End Class Class AdminClass Public XMLNS Public GeneratorAgent, ErrorReportsTo Private Sub Class_Initialize() XMLNS = "http://webns.net/mvcb/" ' Leave this in; I want people to be able to tell me about problems. GeneratorAgent = "http://www.agresticism.org/kitchen/saltshaker/" ErrorReportsTo = "mailto:saltshaker-error@agresticism.org" End Sub End Class Class CreativeCommonsClass Public Use Public XMLNS Public License, Permits, Requires, Prohibits Public Licenses, LicenseCount Private Sub Class_Initialize() Use = False XMLNS = "http://web.resource.org/cc/" License = "" Permits = "" Requires = "" Prohibits = "" Licenses = Array() LicenseCount = 0 End Sub Public Sub Morph(Size) ReDim Preserve Licenses(Size) End Sub Public Sub PushLicense() Dim I ' Die motherfucker die. License = Model(License, "PCDATA", 0) Permits = Model(Permits, "PCDATA", 0) Requires = Model(Requires, "PCDATA", 0) Prohibits = Model(Prohibits, "PCDATA", 0) LicenseCount = RSS.CC.LicenseCount ' We are using mod_cc. RSS.CC.Use = True ' Have we already added this license? For I = 0 To LicenseCount If VarType(RSS.CC.Licenses) = 9 Then If RSS.CC.Licenses(I).Item("License") = License Then Exit Sub Else If I > 0 Then Exit Sub End If Next LicenseCount = UBound(RSS.CC.Licenses) + 1 ' Grow array. RSS.CC.Morph LicenseCount Set RSS.CC.Licenses(LicenseCount) = Server.CreateObject("Scripting.Dictionary") RSS.CC.Licenses(LicenseCount).Add "License", License RSS.CC.Licenses(LicenseCount).Add "Permits", Permits ' I read somewhere Dictionaries have trouble with zero-length strings. If Len(Requires) > 0 Then RSS.CC.Licenses(LicenseCount).Add "Requires", Requires If Len(Prohibits) > 0 Then RSS.CC.Licenses(LicenseCount).Add "Prohibits", Prohibits RSS.CC.LicenseCount = LicenseCount End Sub Public Sub AddToFeed() Dim I, J For I = 1 To LicenseCount License = Licenses(I).Item("License") Permits = Licenses(I).Item("Permits") Requires = Licenses(I).Item("Requires") Prohibits = Licenses(I).Item("Prohibits") Permits = Split(Permits, "|") Requires = Split(Requires, "|") RSS.AddElement "rdf:RDF", "cc:License", XMLNS ' See the bits where we loop over the items in channel. RSS.Document.selectSingleNode("rdf:RDF").lastChild.setAttribute "rdf:about", License For J = 0 To UBound(Permits) ' Again, I do this because I'm bad with arrays. If Len(Permits(J)) > 0 Then RSS.AddElement "rdf:RDF/cc:License[@rdf:about='" & License & "']", "cc:permits", XMLNS RSS.Document.selectSingleNode("rdf:RDF/cc:License[@rdf:about='" & License & "']").lastChild.setAttribute "rdf:resource", "http://web.resource.org/cc/" & Permits(J) End If Next For J = 0 To UBound(Requires) If Len(Requires(J)) > 0 Then RSS.AddElement "rdf:RDF/cc:License[@rdf:about='" & License & "']", "cc:requires", XMLNS RSS.Document.selectSingleNode("rdf:RDF/cc:License[@rdf:about='" & License & "']").lastChild.setAttribute "rdf:resource", "http://web.resource.org/cc/" & Requires(J) End If Next If Len(Prohibits) > 0 Then RSS.AddElement "rdf:RDF/cc:License[@rdf:about='" & License & "']", "cc:prohibits", XMLNS RSS.Document.selectSingleNode("rdf:RDF/cc:License[@rdf:about='" & License & "']").lastChild.setAttribute "rdf:resource", "http://web.resource.org/cc/" & Prohibits End If Next End Sub End Class Class DublinCoreClass Public Use Public XMLNS Public Title, Creator, Subject, Description, Publisher, Contributor, Date, Format, Identifier, Source, Language, Relation, Coverage, Rights ' Why no type? It's reserved in VBScript. You could add it with a DC prefix or something I guess, but most of this stuff is in here just completeness' sake anyway. Private Sub Class_Initialize() Use = False XMLNS = "http://purl.org/dc/elements/1.1/" Title = "" Creator = "" Subject = "" Description = "" Publisher = "" Contributor = "" Date = "" Format = "" Source = "" Language = "" Relation = "" Coverage = "" Rights = "" End Sub Public Sub AddToFeed(Parent) Dim I, Elements, Values Elements = Array("title", "creator", "subject", "description", "publisher", "contributor", "date", "format", "identifier", "source", "language", "relation", "coverage", "rights") Values = Array(Model(Title, "PCDATA", 0), Model(Creator, "PCDATA", 0), Model(Subject, "PCDATA", 0), Model(Description, "PCDATA", 0), Model(Publisher, "PCDATA", 0), Model(Contributor, "PCDATA", 0), Model(Date, "W3CDTF", 0), Model(Format, "PCDATA", 0), Model(Identifier, "PCDATA", 0), Model(Source, "PCDATA", 0), Model(Language, "PCDATA", 0), Model(Relation, "PCDATA", 0), Model(Coverage, "PCDATA", 0), Model(Rights, "PCDATA", 0)) For I = 0 To UBound(Elements) If Len(Values(I)) > 0 Then RSS.AddElement Parent, "dc:" & Elements(I), XMLNS RSS.AddText Parent & "/dc:" & Elements(I), Values(I) End If Next End Sub End Class Class SyndicationClass Public Use Public XMLNS Public UpdatePeriod, UpdateFrequency, UpdateBase Private Sub Class_Initialize() Use = False XMLNS = "http://purl.org/rss/1.0/modules/syndication/" UpdatePeriod = "" UpdateFrequency = "" UpdateBase = "" End Sub Public Sub AddToFeed() UpdatePeriod = Model(UpdatePeriod, Array("hourly", "daily", "weekly", "monthly", "yearly"), 0) UpdateFrequency = Model(UpdateFrequency, "Int", 0) UpdateBase = Model(UpdateBase, "W3CDTF", 0) With RSS .AddElement "rdf:RDF/channel", "sy:updatePeriod", XMLNS .AddText "rdf:RDF/channel/sy:updatePeriod", UpdatePeriod .AddElement "rdf:RDF/channel", "sy:updateFrequency", XMLNS .AddText "rdf:RDF/channel/sy:updateFrequency", UpdateFrequency .AddElement "rdf:RDF/channel", "sy:updateBase", XMLNS .AddText "rdf:RDF/channel/sy:updateBase", UpdateBase End With End Sub End Class Private Function Model(Data, DataType, MaxLength) ' Data models for elements/attributes. Dim I, Checked Dim SmartyPants, Drainpipes, Offset If IsArray(DataType) Then ' We've a list of options to choose from. ' Loop through the options. For I = 0 To UBound(DataType) ' Could this be the one? If Data = DataType(I) Then Checked = True Next ' Didn't match any of the options. If Not Checked Then Data = DataType(1) Else ' We've a model to check it against. ' Choose the model. Select Case DataType Case "PCDATA": ' Have to pay to get the spec! Use this instead: ' http://www.voxpilot.com/html/tagref/pcdata.html ' MSXML seems to do all of this automagically, but we need to de-entify some things, like fancy quotes. Enter the 'raw' character and the ReplaceMap function will replace both that version and the entified version. ' Reference: ' http://daringfireball.net/projects/smartypants/ SmartyPants = Array("?", "?", "?", "?", "?", "?", "?") Drainpipes = Array("'", "'", """", """", "-", "--", "...") Data = ReplaceMap(Data, SmartyPants, Drainpipes) ' Some other characters I've used. SmartyPants = Array("?", "?") Drainpipes = Array("A", "e") Data = ReplaceMap(Data, SmartyPants, Drainpipes) Case "CDATA": If Len(Data) > 0 Then ' Anything except the CDATA delimiters. ' Escape those delimiters. Data = Replace(Data, "", "]]>") End If Case "W3CDTF": ' http://www.w3.org/TR/NOTE-datetime If IsDate(Data) Then ' It's a date, so just churn out the correct string--now with offset! Offset = TryHarder(RSS.UTCOffset) Data = Year(Data) & "-" & LeadingZero(Month(Data)) & "-" & LeadingZero(Day(Data)) & "T" & LeadingZero(Hour(Data)) & ":" & LeadingZero(Minute(Data)) & ":" & LeadingZero(Second(Data)) If Offset = 0 Then Data = Data & "Z" Else If Offset < 0 Then Data = Data & "-" Else Data = Data & "+" Offset = Sqr(Offset * Offset) ' Support fractional timezones. If CInt(Offset) = Offset Then Data = Data & LeadingZero(Offset) & ":00" ElseIf CInt(Offset) > Offset Then Data = Data & LeadingZero(CInt(Offset) - 1) & ":" & LeadingZero(Minute(#00:00:00# + Offset)) Else Data = Data & LeadingZero(CInt(Offset)) & ":" & LeadingZero(Minute(#00:00:00# + Offset)) End If End If Else ' Hit and hope; Data is as it was. Data = Data End If Case "Int": ' Convert Data to an integer. Data = CInt(Data) Case "HTTP Date": ' Sort UTC offset. Data = Data + (RSS.UTCOffset / 24) Data = WeekDayName(WeekDay(Data), True) & ", " & Day(Data) & " " & MonthName(Month(Data), True) & " " & Year(Data) & " " & Hour(Data) & ":" & Minute(Data) & ":" & Second(Data) & " UTC" End Select End If ' Trim the data to its maximum allowed length if MaxLength is set and the data's longer than it should be. If RSS.RestrictLengths And MaxLength > 0 And Len(Data) > MaxLength Then Data = Left(Data, MaxLength) Model = Data End Function Private Function LeadingZero(Number) ' Self-evident, as far as I'm concerned. If Number < 10 Then LeadingZero = "0" & CStr(Number) Else LeadingZero = CStr(Number) End If End Function Private Function ReplaceMap(String, Replaceables, Replacements) ' Makes multiple replacements easier. Dim I ReplaceMap = String ' If the arrays aren't the same size, do nussink. If UBound(Replaceables) <> UBound(Replacements) Then Exit Function For I = 0 To UBound(Replaceables) ReplaceMap = Replace(ReplaceMap, Replaceables(I), Replacements(I)) ReplaceMap = Replace(ReplaceMap, Server.HTMLEncode(Replaceables(I)), Replacements(I)) Next End Function %>